diff --git a/test/unit/org/apache/cassandra/db/compaction/differential/DifferentialCompactionTester.java b/test/unit/org/apache/cassandra/db/compaction/differential/DifferentialCompactionTester.java index 62bf3a36fe..d30278d013 100644 --- a/test/unit/org/apache/cassandra/db/compaction/differential/DifferentialCompactionTester.java +++ b/test/unit/org/apache/cassandra/db/compaction/differential/DifferentialCompactionTester.java @@ -183,6 +183,46 @@ public abstract class DifferentialCompactionTester extends CQLTester } } + /** + * Differential at TWO generations: the normal differential first (gen 1), then the inputs + * are genuinely compacted through the CURSOR path and the differential runs again over the + * cursor-produced outputs (gen 2). Write-side corruption that only manifests when the next + * merge re-reads the output — a bug class where a flag is decided after its byte is + * already written, desyncing the FOLLOWING compaction rather than its own — fails gen 2 + * loudly here instead of surviving until production recompacts. + * + * Returns the GEN-1 iterator capture: scenario structural assertions target gen 1, whose + * shape the scenario controls directly. + */ + protected CapturedOutput assertCursorMatchesIteratorAcrossGenerations(ColumnFamilyStore cfs, + Set byteDiffAllowlist) throws Exception + { + CapturedOutput gen1 = assertCursorMatchesIterator(cfs, byteDiffAllowlist); + + long gcBefore = cfs.getDefaultGcBefore(FBUtilities.nowInSeconds()); + commitCompaction(cfs, cfs.getLiveSSTables(), true, gcBefore); + if (cfs.getLiveSSTables().isEmpty()) + return gen1; // gen 1 purged everything; there are no gen-2 inputs + + assertCursorMatchesIterator(cfs, byteDiffAllowlist); + return gen1; + } + + /** + * Commits one compaction over the given inputs through the selected path WITHOUT restore: + * the live set genuinely becomes the outputs. Used by the cross-generation rung so the + * second differential reads cursor-produced sstables. + */ + protected void commitCompaction(ColumnFamilyStore cfs, Set inputs, boolean cursor, long gcBefore) throws Exception + { + DatabaseDescriptor.setCursorCompactionEnabled(cursor); + if (cursor) + assertCursorPathWillRun(cfs, inputs, gcBefore); + LifecycleTransaction txn = cfs.getTracker().tryModify(inputs, OperationType.COMPACTION); + assertNotNull("unable to mark inputs compacting for commit", txn); + new CompactionTask(cfs, txn, gcBefore, false).execute(ActiveCompactionsTracker.NOOP); + } + /** * Runs one compaction path over the given input subset (non-participating live sstables * stay live and feed purge-overlap decisions), captures the outputs, and restores the live diff --git a/test/unit/org/apache/cassandra/db/compaction/differential/EdgeCaseDifferentialCompactionTest.java b/test/unit/org/apache/cassandra/db/compaction/differential/EdgeCaseDifferentialCompactionTest.java index 10c1dd71b5..6500a0afa5 100644 --- a/test/unit/org/apache/cassandra/db/compaction/differential/EdgeCaseDifferentialCompactionTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/differential/EdgeCaseDifferentialCompactionTest.java @@ -30,6 +30,10 @@ import org.apache.cassandra.utils.ByteBufferUtil; * CURRENTLY SUPPORTED cursor compaction surface (see CursorCompactor.isSupported). Every * scenario here must run the cursor path for real — the harness fails loudly on silent * fallback. Scenarios for unsupported shapes belong in CursorSupportMatrixTest instead. + * + * Every scenario runs the differential at TWO generations (see + * assertCursorMatchesIteratorAcrossGenerations): gen 2 re-compacts genuinely cursor-produced + * outputs, so write-side corruption that only the NEXT merge can see fails here. */ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTester { @@ -61,7 +65,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Reversed clustering order changes on-disk ordering and bound comparisons. */ @@ -82,7 +86,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Multi-component clusterings: mixed types, shared prefixes, per-component bounds. */ @@ -109,7 +113,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Wide partition crossing column-index block boundaries (indexed RowIndexEntry path). */ @@ -131,7 +135,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** @@ -157,7 +161,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Overlapping range tombstones across sstables: boundary markers must merge identically. */ @@ -189,7 +193,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("DELETE FROM %s WHERE pk = 1 AND ck >= 20 AND ck < 30"); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Frozen collections and tuples are single cells and inside the supported surface. */ @@ -216,7 +220,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** TTLs: live expiring cells and already-expired cells (expiry far from run boundaries). */ @@ -243,7 +247,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe Thread.sleep(2000); // let the short TTLs expire well before the first run - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Same-timestamp conflicting writes: reconciliation must tie-break identically. */ @@ -268,7 +272,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?) USING TIMESTAMP 2000", 1L, ck, "tie" + ck); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Newer partition deletion shadowing older data across several sstables. */ @@ -293,7 +297,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 3L, 0L, "alive-again"); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Single-input compaction: pure rewrite, no merge. */ @@ -310,7 +314,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("DELETE FROM %s WHERE pk = 0 AND ck >= 2 AND ck < 6"); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Many inputs: 8-way merge exercises the merge heap harder than the usual 2-4. */ @@ -332,7 +336,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Disjoint inputs: no overlapping partitions, pure concatenation. */ @@ -351,7 +355,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe flush(); } - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** Empty (zero-length) values are valid and distinct from null; both must survive merge. */ @@ -374,7 +378,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("INSERT INTO %s (pk, ck, v1, v2) VALUES (?, ?, null, null)", 1L, ck); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** @@ -413,7 +417,7 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 1L, 3L, "p1v3-overwrite"); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } /** ASC counterpart of emptyClusteringValuesDescending: empty sorts BEFORE values on a @@ -437,6 +441,6 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 1L, 3L, "p1v3-overwrite"); flush(); - assertCursorMatchesIterator(cfs, ALLOWLIST); + assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST); } }