Repository navigation
{180801974} Fix truncate stale cursor also fixing truncatesc rr test - #6301
Open
WalidNejmi wants to merge 1 commit into
Open
WalidNejmi wants to merge 1 commit into
WalidNejmi wants to merge 1 commit into
Conversation
roborivers
approved these changes
Oct 6, 2026
roborivers
left a comment
There was a problem hiding this comment.
Cbuild submission: Success ✓.
Regression testing: Success ✓.
The first 10 failing tests are:
timepart_retro
queuedb_rollover_noroll1_generated **quarantined**
consumer_non_atomic_default_consumer_generated **quarantined**
sc_downgrade [timeout] **quarantined**
reco-ddlk-sql [timeout] **quarantined**
With online_recovery off, a log truncate that undoes a schema change takes the bdb writelock and runs comdb2_reload_schemas, which frees every dbtable and closes every bdb handle. A select emitting rows when the writelock is requested releases all its locks, table locks included, in recover_deadlock. When it gets them back it keeps using cursors that point at the freed dbtable and the closed bdb_state, and segfaults in get_cursor_for_cursortran_flags with a NULL DB (or writes its cursor stats into the freed dbtable). Bump a generation in comdb2_reload_schemas and have recover_deadlock fail the statement with a schema-change error if it changed while the locks were released. Clear the cursors' dbtable and schema pointers so closing them is safe: cur->sc points at the dbtable's schema, which the reload frees too, and recover_deadlock_sc_cleanup() skips cursors whose db is already NULL. truncate_stale_cursor reproduces the crash on the first iteration. It waits until every select has returned a row and is still running before truncating, and fails if a select completes successfully across the reload. The client usually reports a read timeout rather than the schema-change text, so only the failure is checked. truncatesc: failexit is often called from a $(...) subshell (assert_select_all -> select_all -> assert_select_all_rep), where exit only leaves the subshell: when the physrep crashed the test kept retrying until its 40m timeout, and the failure message was swallowed. Kill the runit shell too and log to stderr. A reader query that spans an offline schema reload now fails, so only fail on consecutive reader errors rather than any 10 over the whole run. Signed-off-by: Walid Nejmi <wnejmi@bloomberg.net>
WalidNejmi
force-pushed
the
fix-truncate-stale-cursor
branch
from
October 7, 2026 14:52
9e5ac8c to
330aca2
Compare
WalidNejmi
marked this pull request as ready for review
October 7, 2026 15:41
roborivers
suggested changes
Oct 7, 2026
roborivers
left a comment
There was a problem hiding this comment.
Cbuild submission: Success ✓.
Regression testing: 3/730 tests failed ⚠.
The first 10 failing tests are:
comdb2sys **quarantined**
bind_query_plan
consumer_non_atomic_default_consumer_generated **quarantined**
sc_downgrade [timeout] **quarantined**
reco-ddlk-sql [timeout] **quarantined**
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix a crash when a running SQL statement spans an offline schema reload, and fix
truncatescso a real database crash is reported as a failure instead of eventually appearing as a 40-minute timeout.There are two separate bugs:
Database bug: with
online_recovery off, a log truncate that crosses a schema change can reload every table while a running SELECT has temporarily released its locks. The SELECT later resumes with cursors that still point at the old, freeddbtable/ schema and closed BDB handles.Test bug:
truncatesccan callfailexitfrom inside$(...). In that case,exitterminates only the command-substitution subshell, so the test keeps retrying after the physical replicant has already crashed and eventually hits the framework timeout.This change:
adds a generation counter to detect a wholesale schema reload while a statement's locks are released,
fails the affected statement instead of letting it reuse stale cursors,
clears stale cursor
dband schema pointers before teardown,adds a deterministic regression test for the crash,
strengthens the test to verify the overlapping SELECT actually fails,
and makes
truncatescfail promptly when its physical replicant dies.Background (only needed if unfamiliar)
A running SELECT has cursors that refer to both Comdb2-level and BDB-level state.
Conceptually:
Normally, the statement's table locks protect these objects.
When another thread needs the BDB writelock, a SELECT that is emitting rows can enter
recover_deadlock().That path temporarily gives up its BDB cursors and curtran, including its table locks, so the writer can proceed. Afterward it reacquires the transaction and attempts to continue.
That is safe only if the cursor's backing objects still exist.
A wholesale schema reload breaks that assumption.
Bug 1: stale cursors after an offline schema reload
With
online_recovery off, a log truncate can undo a schema change.During recovery this can call:
which:
acquires table write locks,
closes the existing BDB handles,
frees the current
dbtables and schemas,reloads the tables from the recovered state,
opens replacement BDB handles.
The failure happens when a running SELECT has released its table locks immediately before this reload.
Failing sequence
The reproduced crash reached:
The stale cursor's old
bdb_statehad already been closed, while the live table had a different newly-openedbdb_state.There is also a cleanup problem: even if the statement is aborted before the next cursor operation, its
dbandscpointers can still refer to objects that were freed by the reload.Why the existing table-version check is not enough
recover_deadlock()already has per-table schema/version checks.That does not fully protect this case.
A wholesale reload can replace all of the in-memory objects without changing the logical table version.
For example:
A comparison like:
does not tell us that the cursor's pointers are still valid.
Worse, checking:
is itself unsafe once
cur->dbmay already point to freed memory.The property we need to detect is:
Fix: detect a schema reload across the unlocked interval
Add a database-wide:
comdb2_reload_schemas()increments it after acquiring all table write locks and immediately before closing/freeing the current tables.recover_deadlock()records the generation while it still owns its table locks.The sequence is:
If the generation did not change, the normal
recover_deadlock()cursor recovery path continues as before.If it did change, the old cursors cannot safely be repaired in place because they contain more state than just a
dbtable *: BDB cursor state, schema-derived metadata, buffers, index state, and other cached information may all correspond to the old objects.The safe behavior is to abort the statement and let the client retry.
Cursor cleanup
On a generation mismatch, clear both:
for local cursors.
cur->dbpoints at the old freeddbtable.For table cursors,
cur->scpoints at a schema owned by that table and is also stale after the reload.This was verified directly.
Before the generation-mismatch handling:
With the original implementation:
recover_deadlock_sc_cleanup()did not clearsc, because it only clears the cursor whencur->dbis still non-NULL.Instrumentation of schema destruction confirmed that the stale
scaddress matched the schema address that had been freed during the reload.The current teardown path does not presently dereference that stale
sc, so this was not a second reproduced crash. Clearing it restores the intended cursor invariant and prevents future cleanup code from observing freed schema memory.The fix is kept local to this wholesale-reload path rather than changing the semantics of the shared cleanup helper for unrelated cursor types.
Bug 2:
truncatescturns a crash into a timeoutThe existing test has paths like:
that can execute inside command substitution:
In Bash, command substitution runs in a subshell.
Therefore:
inside
failexitexits only that subshell.The parent
runitprocess keeps running.The observed sequence was:
This masked the actual database crash.
Fix
failexitnow:writes its message to stderr, so command substitution does not swallow it,
terminates the top-level
runitshell as well,keeps the existing
.failexitmarker behavior.This was verified by deliberately killing the physical replicant at the same point.
Before the fix:
After the fix:
Reader behavior in
truncatescThis production fix intentionally changes what happens to a reader that spans an offline schema reload.
Previously:
Now:
Because the test deliberately causes these reloads, its background
reader_threadcan now see expected transient read failures.The reader error threshold therefore counts consecutive errors rather than accumulating every isolated error across the entire test.
This was measured during stress testing:
No unrelated reader errors were observed in those runs.
New deterministic regression test
Add:
The old failure depended heavily on timing and could take a long time to reproduce.
The new test deliberately constructs the unsafe sequence.
Setup
Create
t1and insert 60 rows:Record the current LSN.
Then do additional work after that LSN:
Immediately before truncation:
The saved LSN represents:
Force a SELECT to be active during the truncate
Set:
and run:
on every node.
The test does not rely on a fixed sleep.
For every SELECT it waits until:
Only then does the test issue the log truncate.
This proves that the SELECT is actually mid-scan when recovery starts.
Truncate
Truncate back to the saved LSN.
This should:
Assertions
For every overlapping SELECT:
The exact client error text is intentionally not checked.
The current client path usually reports:
rather than the internal schema-change error.
Afterward, verify:
So the test checks both sides of the fix:
A/B verification
The strengthened regression test was run against several configurations.
Unfixed main
Fails.
Observed behavior includes:
as well as database cores.
Some replicant SELECTs returned all 120 rows with
rc = 0, which the previous version of the test did not independently reject.Original PR
The stale-cursor crash is prevented:
but diagnostics showed
cur->scremained pointing at freed schema memory.Final version
Passes in both cluster and single-node configurations.
All deliberately overlapping SELECTs terminate nonzero after the reload.
Negative control
The generation check was temporarily disabled on the fixed branch.
The strengthened test failed again and produced cores.
This verifies that the regression test is actually protecting the generation-based stale-cursor fix rather than merely passing because of timing differences.
Targeted validation
The final code passed the following targeted tests:
sc_truncatetimed out on both the baseline and fixed builds. The schema-reload path was not entered in either case, so that timeout is not attributed to this change.The full Comdb2 regression suite was not run as part of the local investigation; normal CI/RoboMark coverage is still required.
Known follow-up
When a statement is aborted from this path while rows are already being emitted, the server internally detects the schema-change condition, but
cdb2sqlgenerally reports:after its read timeout rather than receiving a schema-change error response.
That behavior predates this fix and is not required for stale-cursor safety.
A follow-up should investigate propagating the actual schema-change error to the client so it can fail immediately instead of waiting for a read timeout.