fix(wal): fix symbol value corruption when skipping replaced transactions across DDL - #7248
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
calculateSkipTransactionCount lets a future replace-range commit justify skipping an earlier data transaction. The scan previously walked past non-data transactions (structural metadata changes), so the skip decision could cross a DDL boundary. Which transactions get skipped depends on the apply lookahead window, so two appliers of the same WAL stream (e.g. replication primary and replica) can skip different sets and reach a DDL point with different table content. ALTER COLUMN ... TYPE SYMBOL seeds the new column's symbol map from the rows present at apply time, and the WAL protocol treats symbol keys below cleanSymbolCount as identical on every applier. When the conversion sees different intermediate content, the appliers assign different keys to the same strings, and rows written against one map silently resolve to wrong symbol values on the other applier. The scan now stops at the first non-data transaction, so a skip can never cross a structural change and every applier holds identical table content at each DDL point. The TRUNCATE fast path still applies when the whole window before it consists of data transactions; soft truncate keeps symbol maps, so it needs the same guard. New tests apply the same commit sequence under apply lookahead 1 and 10000 and assert identical query results and identical symbol maps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ff3f257 to
8cdd615
Compare
bluestreak01
left a comment
There was a problem hiding this comment.
Approved.
The fix is correct, complete across all transaction types, and strictly more conservative (new skip count ≤ old). It closes a real apply-lookahead-dependence bug: ALTER COLUMN ... TYPE SYMBOL (walId = STRUCTURAL_CHANGE_WAL_ID = -1) was crossed by the old else { continue; } path, letting a future replace-range justify skipping pre-conversion rows and making the converted symbol map's key assignment depend on the lookahead window. Verified end-to-end: testSkipDoesNotCrossSymbolConversion fails pre-fix with 'symbol counts differ expected:<4> but was:<3>' and passes post-fix.
Non-blocking follow-ups:
- testSkipDoesNotCrossTruncate is an always-green guard (passes pre- and post-fix) because no structural txn sits between the data run and the TRUNCATE, so old Math.min and new futureSeqTxn-initialSeqTxn compute the same value. Consider inserting a structural txn (e.g. ALTER ... TYPE SYMBOL) before the TRUNCATE to make it a genuine regression test, or re-document it as a soft-truncate invariant guard.
- No coverage for the continue->break change on MAT_VIEW_INVALIDATE / VIEW_DEFINITION.
- Two minor comment imprecisions: the walId>0 non-data branch comment lists 'structural metadata change' (those are walId=-1, handled in the else branch); the 'walId <= 0' comment omits DROP_TABLE_WAL_ID=-2.
|
/azp run macwin |
|
Azure Pipelines successfully started running 1 pipeline(s). |
[PR Coverage check]😍 pass : 2 / 2 (100.00%) file detail
|
Problem
ApplyWal2TableJob.calculateSkipTransactionCountlets a future replace-range commit justify skipping an earlier data transaction. The scan walked past non-data transactions (structural metadata changes), so a skip decision could cross a DDL boundary. Which transactions get skipped depends on the apply lookahead window, so two appliers of the same WAL stream (e.g. a replication primary and replica, or a node replaying the full log) can skip different transaction sets and reach a DDL point with different intermediate table content.ALTER COLUMN ... TYPE SYMBOLseeds the new column's symbol map from the rows present at apply time, and the WAL protocol treats symbol keys belowcleanSymbolCountas table-global keys identical on every applier (remapWalSymbolsonly remaps keys at or above it). When the conversion sees different intermediate content on two appliers, they assign different keys to the same strings, and rows written against one map silently resolve to wrong symbol values on the other applier. A replication fuzz test caught this: the same row readSKXOLon one node andTRILVFon the other.The TRUNCATE fast path had the same exposure: WAL TRUNCATE applies as a soft truncate (
removeAllPartitionskeeps symbol maps), and the oldfirstNonSkippableTxnlogic allowed the skip-to-truncate jump to be computed across an intervening structural transaction.Change
The future-transaction scan now stops at the first non-data transaction of any kind (structural metadata change or SQL). A replace-range can only justify skipping transactions within the same structural-change-free window, which guarantees every applier holds identical table content at each DDL point, restoring the invariant that symbol key assignment is independent of the WAL apply method. The TRUNCATE early exit remains, and is now only reachable when the whole window before it consists of data transactions, which makes
firstNonSkippableTxndead code (removed).Effects
ALTER COLUMN ... TYPE SYMBOLor TRUNCATE.testManyTransactionsSkippedWhenTruncateIfFoundstill passes with the same skip count).Test plan
testSkipDoesNotCrossSymbolConversion: applies the same commit sequence (data commits, STRING to SYMBOL conversion, more data, replace-range covering the early commits) under apply lookahead 1 and 10000 and asserts identical query results and identical symbol maps (key-to-string). Fails before the fix withsymbol counts differ expected:<4> but was:<3>; passes after.testSkipDoesNotCrossTruncate: same shape with TRUNCATE instead of the conversion; guards the soft-truncate path as an invariant test (it passes pre-fix because the old code computed the same skip count in this scenario).WalWriterReplaceRangeTest(45 tests),MatViewIdenticalReplaceTest(20),MatViewTestmatches (292, 1 skipped),WalTableWriterFuzzTest(23) all pass.🤖 Generated with Claude Code