Pin open-ended range tombstones; fix wall-clock flake in the harness

Open-ended (single-sided) range tombstones were covered only
probabilistically by the randomized soak. Two deterministic scenarios
now pin them: markers open to TOP and from BOTTOM, nested open ranges,
a partition containing only an open range tombstone, resurrection
inside open-deleted ranges, interleaving with bounded tombstones, and
a DESC variant where the bound direction inverts in on-disk order.
Unbounded bounds are zero-component clustering prefixes — the same
empty-prefix comparison region as the reversed-column finding.

Separately, the materialized-view suite exposed a harness hole:
JsonTransformer computes its "expired" fields from the wall clock,
ignoring the nowInSec parameter the harness fixes for deterministic
rendering. The two paths' captures run seconds apart, so a
localExpirationTime falling between them renders expired=false on one
side and expired=true on the other over byte-identical sstables —
materialized-view expired-liveness rows sit permanently on that
boundary because their expiration is the write second. The harness now
normalizes the flag out of the dump before comparing; it is derived
from expires_at, which is still compared. (Upstream observation for
the eventual ticket: sstabledump's "expired" ignores the tool's own
time parameter.)
This commit is contained in:
Jon Haddad 2026-06-10 20:03:51 -07:00
parent 5e9bc61d22
commit 277c4059d3
2 changed files with 69 additions and 1 deletions

View File

@ -380,7 +380,14 @@ public abstract class DifferentialCompactionTester extends CQLTester
JsonTransformer.toJsonLines(scanner, Util.iterToStream(scanner), true, false,
sstable.metadata(), DUMP_NOW_SEC, baos);
}
String json = baos.toString(StandardCharsets.UTF_8);
// JsonTransformer's "expired" fields are computed from WALL CLOCK
// (currentTimeMillis), ignoring the fixed nowInSec passed above so byte-identical
// outputs can render differently when a localExpirationTime falls between the two
// paths' captures, which run seconds apart (materialized-view expired-liveness rows
// sit permanently on that boundary: their expiration IS the write second). The flag
// is derived from expires_at, which is still compared, so normalize it out.
String json = baos.toString(StandardCharsets.UTF_8)
.replaceAll("\"expired\"\\s*:\\s*(true|false)", "\"expired\":\"normalized\"");
// 3. stats spot-check summary
StatsMetadata stats = sstable.getSSTableMetadata();

View File

@ -613,6 +613,67 @@ public class EdgeCaseDifferentialCompactionTest extends DifferentialCompactionTe
assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST);
}
/**
* OPEN-ENDED (single-sided) range tombstones: DELETE with only a lower or upper
* clustering bound produces markers whose other side is the unbounded partition edge
* zero-component TOP/BOTTOM bounds, the same empty-prefix region bound-kind comparisons
* and covered-clustering stats exercise. Open RTs nest with each other, overlap bounded
* RTs and rows across sstables, and one partition is open-RT-only.
* Previously covered only probabilistically by the widened soak.
*/
@Test
public void openEndedRangeTombstones() throws Exception
{
createTable("CREATE TABLE %s (pk bigint, ck bigint, v text, PRIMARY KEY (pk, ck)) " +
"WITH gc_grace_seconds = 864000");
ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
cfs.disableAutoCompaction();
for (long pk = 1; pk <= 2; pk++)
for (long ck = 0; ck < 30; ck++)
execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", pk, ck, "v" + ck);
execute("DELETE FROM %s WHERE pk = ? AND ck >= ? AND ck < ?", 1L, 10L, 20L); // bounded, for interleave
flush();
// open-ended deletes: up to TOP, down from BOTTOM, nested opens, and an RT-only partition
execute("DELETE FROM %s WHERE pk = ? AND ck >= ?", 1L, 25L);
execute("DELETE FROM %s WHERE pk = ? AND ck > ?", 1L, 27L); // nests inside the >= 25 open range
execute("DELETE FROM %s WHERE pk = ? AND ck <= ?", 2L, 4L);
execute("DELETE FROM %s WHERE pk = ? AND ck >= ?", 3L, 0L); // partition with ONLY an open RT
flush();
// resurrection inside open-deleted ranges with newer timestamps
execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 1L, 26L, "resurrected");
execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 2L, 2L, "resurrected");
flush();
assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST);
}
/** DESC counterpart: single-sided bounds invert in on-disk clustering order, so the
* open edge swaps between TOP and BOTTOM relative to the CQL bound direction. */
@Test
public void openEndedRangeTombstonesDescending() throws Exception
{
createTable("CREATE TABLE %s (pk bigint, ck bigint, v text, PRIMARY KEY (pk, ck)) " +
"WITH CLUSTERING ORDER BY (ck DESC) AND gc_grace_seconds = 864000");
ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
cfs.disableAutoCompaction();
for (long ck = 0; ck < 30; ck++)
execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 1L, ck, "v" + ck);
flush();
execute("DELETE FROM %s WHERE pk = ? AND ck >= ?", 1L, 25L);
execute("DELETE FROM %s WHERE pk = ? AND ck <= ?", 1L, 4L);
flush();
execute("INSERT INTO %s (pk, ck, v) VALUES (?, ?, ?)", 1L, 27L, "resurrected");
flush();
assertCursorMatchesIteratorAcrossGenerations(cfs, ALLOWLIST);
}
/**
* ODD superset size at the subset-encoding mode boundary: with 71 columns, the encoder
* and decoder must agree on present-index vs missing-index