From 277c4059d3503aa96bd268d8c7cbd7704e79f9ba Mon Sep 17 00:00:00 2001 From: Jon Haddad Date: Wed, 10 Jun 2026 20:03:51 -0700 Subject: [PATCH] Pin open-ended range tombstones; fix wall-clock flake in the harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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.) --- .../DifferentialCompactionTester.java | 9 ++- .../EdgeCaseDifferentialCompactionTest.java | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) 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 d30278d013..c08cf5fff8 100644 --- a/test/unit/org/apache/cassandra/db/compaction/differential/DifferentialCompactionTester.java +++ b/test/unit/org/apache/cassandra/db/compaction/differential/DifferentialCompactionTester.java @@ -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(); 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 2834226ea8..2b08cad562 100644 --- a/test/unit/org/apache/cassandra/db/compaction/differential/EdgeCaseDifferentialCompactionTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/differential/EdgeCaseDifferentialCompactionTest.java @@ -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