Verify cursor compaction across two generations in the edge corpus

The differential harness compared single-generation outputs: both
paths compact the same inputs once. 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 — was invisible
to it.

assertCursorMatchesIteratorAcrossGenerations runs the normal
differential, then genuinely commits a cursor-path compaction (no
restore — the live set becomes cursor-produced sstables) and runs the
differential again over those outputs. Every edge-corpus scenario now
uses it, so each scenario also proves cursor output is a correct
INPUT to both pipelines.
This commit is contained in:
Jon Haddad 2026-06-10 19:05:03 -07:00
parent 85d05c30f7
commit 5d46e672e5
2 changed files with 60 additions and 16 deletions

View File

@ -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<String> 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<SSTableReader> 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

View File

@ -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);
}
}