Repository navigation
{181186021} Sc timepart resume fix - #6304
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 7, 2026
roborivers
left a comment
There was a problem hiding this comment.
Cbuild submission: Success ✓.
Regression testing: Success ✓.
The first 10 failing tests are:
truncatesc_offline_generated **quarantined**
consumer_non_atomic_default_consumer_generated **quarantined**
sc_downgrade [timeout] **quarantined**
phys_rep_tiered_firstfile_generated [timeout]
sql_logfill_autodisable [timeout]
A multi-table schema change, such as an alter of a time partition, saves
each table's seed in its own llmeta transaction while the tables already
started write llmeta from their own threads. The seed write gave up on the
first deadlock and the table was still marked in schema change, so a new
master found it without a seed, could not resume it ("Failed to determine
host and seed!"), and stopped resuming the other shards. Retry seed writes
and deletes on deadlock, keeping the real bdberr across the abort, and fail
a schema change whose seed cannot be saved instead of starting one that
cannot be resumed.
Resuming a time partition went on using schema changes that
start_schema_change may free when it fails to start them. A placeholder
carrying the rc now stands in for them so the partition schema change
aborts cleanly, and the abort clears the in-schema-change marker of a shard
that never started, so a later master does not resume it on its own.
Fix three memory errors ASAN found on these paths: a replicant reloading a
view left tables that had just left it pointing at the freed view name,
which resume_schema_change then read and grouped shards by; free_sc read the
schema change it had just freed; llmeta list printed table parameters past
their end.
sc_timepart now sets the convert sleep on every node, as the alter takes it
from whichever node parses it, so the master is killed while the alter is
still running, and waits until every node has dropped the shard rolled out
of the partition before altering. sc_seed_faults injects seed write and
delete failures, and sc_timepart_resume injects failed resumes and kills the
master again while it resumes, using test-only tunables.
Signed-off-by: Walid Nejmi <wnejmi@bloomberg.net>
WalidNejmi
force-pushed
the
sc-timepart-resume-fix
branch
from
October 7, 2026 19:42
be5a12d to
1f7d0ef
Compare
WalidNejmi
marked this pull request as ready for review
October 7, 2026 20:12
roborivers
approved these changes
Oct 7, 2026
roborivers
left a comment
There was a problem hiding this comment.
Cbuild submission: Success ✓.
Regression testing: Success ✓.
The first 10 failing tests are:
logfill [db unavailable at finish] **quarantined**
sc_truncate_multiddl_generated [db unavailable at finish] **quarantined**
sc_truncate [db unavailable at finish]
consumer_non_atomic_default_consumer_generated **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.
This is not an easy fix--multiple bugs were found so might drop this PR, but running rr tests first and adding a summary comment for future ref:
Make time partition schema change resume safe
Summary
A time-partition schema change is a multi-table schema change: each shard runs its own schema change, but all shards must commit or abort as one logical operation.
sc_timepartwas intermittently failing after killing the master during anALTERbecause one shard could be left marked as "in schema change" without the seed required to resume it. Making the test reliably exercise failover then exposed use-after-free bugs in the time-partition resume path.This change hardens the schema-change lifecycle around failover:
schema_change_typeafterstart_schema_change()may have freed it;sc_timeparttest reliably exercise resume;Background
When a schema change starts, Comdb2 persists a seed in llmeta.
That seed is part of the durable state required to reconstruct the schema change after a master failover.
For a time partition, an
ALTERmay involve several physical shard tables:flowchart TD A["ALTER time partition"] --> B["Shard t0 schema change"] A --> C["Shard t1 schema change"] A --> D["Shard t2 schema change"] B --> E["Persist SC seed"] C --> F["Persist SC seed"] D --> G["Persist SC seed"] E --> H["Mark shard in schema change"] F --> I["Mark shard in schema change"] G --> J["Mark shard in schema change"]If the master dies while the operation is running, the new master discovers the shards that were in schema change, retrieves their saved seeds, and resumes them.
The important invariant is:
The failure that started this investigation violated that invariant.
Bug 1: schema-change seed writes gave up on the first deadlock
start_schema_change_tran()saves each table's seed throughbdb_set_sc_seed().When no transaction is supplied,
bdb_set_sc_seed()opens its own llmeta transaction.For a multi-table schema change, however, other shards may already be running asynchronously and writing llmeta at the same time.
The observed sequence was:
sequenceDiagram participant Main as SC startup participant T1 as Seed transaction for t1 participant T2 as Async SC thread for t2 participant LL as llmeta Main->>T1: Save t1 seed T1->>LL: Begin llmeta transaction T2->>LL: Update SC metadata for t2 LL-->>T1: BDBERR_DEADLOCK T1-->>Main: Failure Main->>LL: Continue and mark t1 in schema change Note over Main,LL: t1 is now marked in SC<br/>without a durable seedPreviously,
bdb_set_sc_seed()did not retry the deadlock.Its abort could also overwrite the original
bdberr, making the real cause less clear.The caller then logged:
but continued starting the schema change.
If the master died afterward, the new master saw the table marked as in schema change but could not recover its seed:
At that point the shard could not be resumed and the multi-shard resume stopped.
Fix
bdb_set_sc_seed()now follows the normal llmeta deadlock-retry pattern when it owns the transaction:flowchart TD A["Begin llmeta transaction"] --> B["Write schema-change seed"] B -->|Success| C["Commit"] B -->|Failure| D["Save original bdberr"] D --> E["Abort transaction"] E --> F["Restore original bdberr"] F --> G{"BDBERR_DEADLOCK?"} G -->|Yes| H["Optional deadlock backoff"] H --> I{"Retry limit reached?"} I -->|No| A I -->|Yes| J["Return failure"] G -->|No| JThe retry uses:
gbl_maxretriesgbl_llmeta_deadlock_pollThe original
bdberris preserved acrossbdb_tran_abort().If the caller supplied a transaction, the helper does not independently abort and restart it; the error is returned to the transaction owner.
Bug 2: a schema change could still start without its seed
Retrying transient deadlocks fixes the common failure, but it does not guarantee that a seed write will always succeed.
For example:
Previously, even a permanent seed-save failure only produced a log message and schema-change startup continued.
That still allowed the invalid state:
A future master cannot safely resume that schema change.
Fix
Saving the seed is now a prerequisite for starting a recoverable schema change.
If
bdb_set_sc_seed()ultimately fails:iq->errstatis set toFailed to save schema change seed;SC_LLMETA_ERR;Conceptually:
flowchart TD A["Start schema change"] --> B["Persist SC seed"] B --> C{"Seed persisted?"} C -->|Yes| D["Continue schema-change startup"] C -->|No| E["Clear SC running state"] E --> F["Return SC_LLMETA_ERR"] F --> G["Do not publish an unrecoverable SC"]This preserves the recovery invariant even when the failure is not transient.
Bug 3: schema-change seed deletes had the same deadlock weakness
The seed lifecycle is symmetrical:
flowchart LR A["Schema change starts"] --> B["Save seed"] B --> C["Schema change runs"] C --> D["Schema change completes or aborts"] D --> E["Delete seed"]bdb_delete_sc_seed()used the same standalone llmeta transaction pattern as the old setter, but also did not retry deadlocks.A transient deadlock during cleanup could therefore leave stale seed metadata behind.
Fix
bdb_delete_sc_seed()now uses the same rules as the setter:BDBERR_DEADLOCKwhen it owns the transaction;gbl_maxretries;gbl_llmeta_deadlock_poll;bdberracross abort;Bug 4: failed time-partition resume could use a freed schema-change object
Once
sc_timepartwas changed to reliably exercise failover, another product bug became reproducible.The resume path created a new
schema_change_type, linked it into the time-partition resume list, and then called:The problem is that
start_schema_change()does not guarantee that the passed object is still alive after failure.Several failure paths inside
start_schema_change_tran()free theschema_change_type.The old flow was effectively:
flowchart TD A["Allocate new_sc"] --> B["Link new_sc into resume list"] B --> C["start_schema_change(new_sc)"] C -->|Success| D["Continue"] C -->|Failure| E["start_schema_change may free new_sc"] E --> F["Caller writes new_sc->sc_rc"] F --> G["Finalizer later uses new_sc"] G --> H["Use-after-free / invalid mutex / double lifetime"]This was observed as:
Fix
A failed
start_schema_change()is now treated as consuming or invalidating the original pointer.The caller never touches it again.
For a shard that has to be newly restarted:
flowchart TD A["Allocate new_sc"] --> B["start_schema_change(new_sc)"] B -->|Success| C["Link real SC into resume list"] B -->|Failure| D["Do not touch original new_sc again"] D --> E["Allocate failed-resume placeholder"] E --> F["Store table, UUID and failure rc"] F --> G["Link placeholder into resume list"]The placeholder was never started. Its only purpose is to carry the failed shard's result into the common multi-DDL finalizer so the entire time-partition schema change can abort cleanly.
Bug 5: the existing-shard resume path had the same ownership problem
The same class of use-after-free existed in
verify_sc_resumed_for_all_shards().Previously it did roughly:
But
scmay already have been freed by the failed call.Fix
Before starting the schema change, the code saves the information needed afterward:
If startup fails, the original list entry is replaced with
new_failed_resume_sc().The failed pointer is never dereferenced again.
The resume list therefore contains either:
rather than a mixture of valid and potentially freed pointers.
Bug 6: a shard that never restarted could keep its in-schema-change marker
A placeholder represents a shard whose schema change never actually restarted.
Because no schema-change worker was successfully started for that shard, the normal worker cleanup path does not run.
That could leave the shard's persistent
LLMETA_IN_SCHEMA_CHANGEmarker behind.The next master could then discover that marker and try to resume the shard independently from the rest of the partition operation.
Fix
When the multi-DDL finalizer aborts, it now clears the in-schema-change marker for a shard that never started:
except when the failure was:
That case means another schema change owns the table, so its marker must not be cleared.
Conceptually:
flowchart TD A["Failed-resume placeholder"] --> B{"Did this SC ever start?"} B -->|Yes| C["Normal SC cleanup owns its state"] B -->|No| D{"SC_CANT_SET_RUNNING?"} D -->|No| E["Clear stale in-schema-change marker"] D -->|Yes| F["Leave marker for the SC that owns the table"]Test issue 1:
sc_timepartdid not reliably exercise resumeThe original test attempted to guarantee that the schema change was still running when the master was killed by setting:
However,
CONVERTSLEEPis node-local.The test set it on the master and through one default-routed connection, while the ALTER itself could be parsed on a different node.
When the ALTER landed on a node without
CONVERTSLEEP, it could finish in about one second.The test then killed the master three seconds later, after the schema change was already complete.
That meant the "resume" test sometimes passed without performing any resume at all.
Fix
CONVERTSLEEPis now set explicitly on every cluster node before the ALTER.That guarantees that whichever node parses the ALTER uses the intended delay and the master kill occurs while the schema change is still active.
Test issue 2: the ALTER could race with deletion of the retired shard
During a time-partition rollout, the view can switch to:
while the retired
t0still physically exists untildelete_lagexpires.During that window:
Because shard names cycle,
t0is also a possible future shard name._next_shard_exists()can therefore interpret the retiringt0as the next shard.An ALTER started during this window can include
t0while the rollout thread is concurrently dropping it.This is a separate product/design issue; this change does not redefine the shard-generation model.
Fix to the normal regression test
Before starting the regular resume scenario,
sc_timepartnow waits until the retired shard is absent fromsqlite_masteron every node.This separates the basic failover/resume regression from the retired-shard overlap.
A separate test intentionally exercises the overlap and verifies that the cluster remains consistent and does not crash.
Additional memory-safety bugs found while testing
Running the new paths under ASAN exposed three pre-existing memory errors.
Dangling
timepartition_nameduring replicant view reloadA table that had recently rolled out of a time partition could still have:
pointing at the old view's name after the view was freed.
If that replicant later became master,
resume_schema_change()could read the dangling pointer while grouping shards by partition.Fix
Before freeing the old view,
views_handle_replicant_reload()now clearstimepartition_nameon any table still pointing at that view name.free_sc()read the schema-change object after freeing itThe old code effectively did:
Fix
Save the value before the free:
llmeta listprinted a non-NUL-terminated value with%sLLMETA_TABLE_PARAMETERSvalues have a known length but are not guaranteed to be NUL-terminated.Printing them with
%scould read past the end of the buffer.Fix
The value is now printed with a bounded length:
Testing
sc_seed_faultsA new deterministic test exercises the seed lifecycle through test-only fault injection.
Seed write deadlocks
Inject:
Verify:
Permanent seed-write failure
Inject a permanent seed-write error.
Verify for both single-table and multi-table schema changes:
Seed-delete deadlocks
Force a schema change to fail and inject two deadlocks while its seed is being removed.
Verify:
sc_timepart_resumeA new cluster-only test directly exercises the time-partition resume lifecycle.
Failed resume modes
The test can force one shard to fail resume in four different ways:
For each failure:
The consistency rule is:
A normal partition ALTER is then run to verify that failed recovery attempts did not leave the partition unusable.
Double failover
The test also exercises:
sequenceDiagram participant A as Master A participant B as Master B participant C as Master C A->>A: Start time-partition ALTER Note over A: Conversion is still running A-->>B: A is killed B->>B: Win election B->>B: Begin resuming shard SCs Note over B: Test waits until resume is observed B-->>C: B is killed while resuming C->>C: Win election C->>C: Recover remaining SC state Note over C: Final shard schemas must be consistentThis verifies that recovery state remains valid even when the recovering master itself dies.
Retired-shard overlap
A separate case deliberately starts an ALTER while the shard rolled out of the partition is still being dropped.
The operation may complete or abort, but it must:
Test-only fault injection
The new tests use
COMDB2_TEST/internal tunables:For seed operations:
Resume modes allow deterministic testing of the important
start_schema_change()failure paths without relying on timing.Validation
The final branch was validated with:
sc_seed_faults;sc_timepart_resume;sc_timepart;COMDB2_PER_THREAD_MALLOC=OFF.After the memory fixes above, all three tests pass under ASAN with no ASAN reports.
As a negative control, restoring the old:
behavior in
verify_sc_resumed_for_all_shards()causes the new resume test to report the expected heap-use-after-free under ASAN.This confirms that the new test directly exercises the ownership bug rather than only covering the surrounding failover scenario.
Result
Before this change, a master failure could expose several inconsistent states:
After this change:
flowchart TD A["Start shard schema change"] --> B["Persist required recovery metadata"] B --> C{"Successful?"} C -->|No| D["Fail startup cleanly"] D --> E["Do not publish unrecoverable SC state"] C -->|Yes| F["Run schema change"] F --> G{"Master survives?"} G -->|Yes| H["Complete and remove recovery metadata"] G -->|No| I["New master reconstructs shard SCs"] I --> J{"Each shard restarts?"} J -->|Yes| K["Resume coordinated partition ALTER"] J -->|No| L["Replace failed SC with safe placeholder"] L --> M["Abort coordinated partition ALTER"] M --> N["Clean stale recovery markers"]The time-partition ALTER can therefore either resume successfully or abort as one coordinated operation without relying on missing recovery metadata or retaining freed schema-change objects.