From eaf8415df129c852c17756180f04f48d5ec6a186 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Mon, 24 Aug 2026 16:50:06 -0700 Subject: [PATCH 01/10] test --- .../db/compaction/LeveledManifest.java | 30 ++++++- .../LeveledCompactionStrategyTest.java | 90 +++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java index 5e85511726a2..043b1bc21ca6 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.function.Function; import com.google.common.annotations.VisibleForTesting; @@ -450,7 +451,8 @@ private static Set overlapping(Collection candidat return overlapping(first, last, others); } - private static Set overlappingWithBounds(SSTableReader sstable, Map> others) + @VisibleForTesting + static Set overlappingWithBounds(SSTableReader sstable, Map> others) { return overlappingWithBounds(sstable.getFirst().getToken(), sstable.getLast().getToken(), others); } @@ -573,14 +575,16 @@ private Collection getCandidatesFor(int level) return candidates; } + // We know that because we are in level L0+, this is a tree set with disjoint SSTables + TreeSet sstablesNextLevel = (TreeSet) generations.get(level + 1); + // look for a non-suspect keyspace to compact with, starting with where we left off last time, // and wrapping back to the beginning of the generation if necessary - Map> sstablesNextLevel = genBounds(generations.get(level + 1)); Iterator levelIterator = generations.wrappingIterator(level, lastCompactedSSTables[level]); while (levelIterator.hasNext()) { SSTableReader sstable = levelIterator.next(); - Set candidates = Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, sstablesNextLevel)); + Set candidates = getIntersectingSSTablesFromTreeSet(sstable, sstablesNextLevel); if (Iterables.any(candidates, SSTableReader::isMarkedSuspect)) continue; @@ -592,6 +596,26 @@ private Collection getCandidatesFor(int level) return Collections.emptyList(); } + public static Set getIntersectingSSTablesFromTreeSet(SSTableReader sstable, TreeSet sstablesNextLevel) + { + Set candidates = new HashSet<>(); + candidates.add(sstable); + + if (sstablesNextLevel.isEmpty()) + return candidates; + + SSTableReader start = sstablesNextLevel.floor(sstable); + Iterator it = sstablesNextLevel.tailSet(start != null ? start : sstablesNextLevel.first(), true).iterator(); + + while (it.hasNext()) { + SSTableReader s = it.next(); + if (s.getFirst().compareTo(sstable.getLast()) > 0) break; + if (s.getLast().compareTo(sstable.getFirst()) >= 0) candidates.add(s); + } + + return candidates; + } + private Set getCompactingL0() { Set sstables = new HashSet<>(); diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java index 0aeab98f7959..c7ebd7a0ffff 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; @@ -76,6 +77,8 @@ import org.apache.cassandra.utils.TimeUUID; import static java.util.Collections.singleton; +import static org.apache.cassandra.db.compaction.LeveledManifest.getIntersectingSSTablesFromTreeSet; +import static org.apache.cassandra.db.compaction.LeveledManifest.overlapping; import static org.apache.cassandra.schema.MockSchema.readerBounds; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; import static org.assertj.core.api.Assertions.assertThat; @@ -595,6 +598,93 @@ public void testDisableSTCSInL0() throws IOException } } + @Test + public void testLinearScanTreeSetIntersectionEquivalence() throws IOException + { + ColumnFamilyStore cfs = MockSchema.newCFS(); + List sstables = new ArrayList<>(); + + long seed = System.currentTimeMillis(); + Random r = new Random(seed); + + // Generates disjoint sorted SSTables that match what we see in L1 + int i = 0; + int lower_bound = 10; + while (i < 10000) + { + int start = lower_bound + 1 + r.nextInt(30); + int end = start + 10 + r.nextInt(30); + lower_bound = end; + SSTableReader l1sstable = MockSchema.sstableWithLevel(i, start, end, 1, cfs); + sstables.add(l1sstable); + i++; + } + + LeveledGenerations generations = new LeveledGenerations(); + generations.addAll(sstables); + + SSTableReader sstableinL1; + Token newStart; + Token newEnd; + + int kind = r.nextInt(7); + + switch (kind) + { + // No overlap + case 0: + sstableinL1 = sstables.get(r.nextInt(sstables.size())); + newStart = sstableinL1.getLast().getToken().increaseSlightly(); + newEnd = newStart.getToken().increaseSlightly(); + break; + // Overlaps on the left end + case 1: + sstableinL1 = sstables.get(r.nextInt(sstables.size())); + newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); + newEnd = sstableinL1.getFirst().getToken().increaseSlightly(); + break; + // Overlaps on the right end + case 2: + sstableinL1 = sstables.get(r.nextInt(sstables.size())); + newStart = sstableinL1.getLast().getToken().decreaseSlightly(); + newEnd = sstableinL1.getLast().getToken().increaseSlightly(); + break; + // Subset of entire SSTable + case 3: + sstableinL1 = sstables.get(r.nextInt(sstables.size())); + newStart = sstableinL1.getFirst().getToken().increaseSlightly(); + newEnd = sstableinL1.getLast().getToken().decreaseSlightly(); + break; + // Superset of entire SSTable + case 4: + sstableinL1 = sstables.get(0); + newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); + newEnd = sstableinL1.getLast().getToken().increaseSlightly(); + break; + // Exact SSTable match + case 5: + sstableinL1 = sstables.get(0); + newStart = sstableinL1.getFirst().getToken(); + newEnd = sstableinL1.getLast().getToken(); + break; + // Spans the whole L1 + case 6: + newStart = sstables.get(0).getFirst().getToken(); + newEnd = sstables.get(sstables.size() - 1).getLast().getToken(); + break; + default: + throw new IllegalStateException("Unhandled kind " + kind); + } + + SSTableReader sstable = MockSchema.sstableWithLevel(i, newStart.getLongValue(), newEnd.getLongValue(), 0, cfs); + Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, (TreeSet) generations.get(1)); + Collection linearScanIntersectionSSTables = overlapping(sstable.getFirst().getToken(), sstable.getLast().getToken(), generations.get(1)); + linearScanIntersectionSSTables.add(sstable); + + // The results should be equivalent to doing a linear scan + assertTrue(treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); + } + private int getTaskLevel(ColumnFamilyStore cfs) { int level = -1; From fa8ebe5e63338da80601b75c2a2d3184d6a96a63 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Wed, 26 Aug 2026 12:05:50 -0700 Subject: [PATCH 02/10] fix nits --- .../cassandra/db/compaction/LeveledGenerations.java | 7 +++++++ .../cassandra/db/compaction/LeveledManifest.java | 10 ++++++---- .../db/compaction/LeveledCompactionStrategyTest.java | 9 ++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java index 513e02aad99e..d4d76ebc9a5a 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java @@ -96,6 +96,13 @@ Set get(int level) return levels[level - 1]; } + TreeSet getSortedLevel(int level) + { + if (level > levelCount() - 1 || level <= 0) + throw new ArrayIndexOutOfBoundsException("Invalid generation " + level + " - maximum is " + (levelCount() - 1)); + return levels[level - 1]; + } + int levelCount() { return levels.length + 1; diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java index 043b1bc21ca6..874020ccba72 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java @@ -576,7 +576,7 @@ private Collection getCandidatesFor(int level) } // We know that because we are in level L0+, this is a tree set with disjoint SSTables - TreeSet sstablesNextLevel = (TreeSet) generations.get(level + 1); + TreeSet sstablesNextLevel = generations.getSortedLevel(level + 1); // look for a non-suspect keyspace to compact with, starting with where we left off last time, // and wrapping back to the beginning of the generation if necessary @@ -585,6 +585,7 @@ private Collection getCandidatesFor(int level) { SSTableReader sstable = levelIterator.next(); Set candidates = getIntersectingSSTablesFromTreeSet(sstable, sstablesNextLevel); + candidates.add(sstable); if (Iterables.any(candidates, SSTableReader::isMarkedSuspect)) continue; @@ -596,10 +597,10 @@ private Collection getCandidatesFor(int level) return Collections.emptyList(); } - public static Set getIntersectingSSTablesFromTreeSet(SSTableReader sstable, TreeSet sstablesNextLevel) + @VisibleForTesting + protected static Set getIntersectingSSTablesFromTreeSet(SSTableReader sstable, TreeSet sstablesNextLevel) { Set candidates = new HashSet<>(); - candidates.add(sstable); if (sstablesNextLevel.isEmpty()) return candidates; @@ -607,7 +608,8 @@ public static Set getIntersectingSSTablesFromTreeSet(SSTableReade SSTableReader start = sstablesNextLevel.floor(sstable); Iterator it = sstablesNextLevel.tailSet(start != null ? start : sstablesNextLevel.first(), true).iterator(); - while (it.hasNext()) { + while (it.hasNext()) + { SSTableReader s = it.next(); if (s.getFirst().compareTo(sstable.getLast()) > 0) break; if (s.getLast().compareTo(sstable.getFirst()) >= 0) candidates.add(s); diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java index c7ebd7a0ffff..80e37180e05b 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java @@ -609,12 +609,12 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException // Generates disjoint sorted SSTables that match what we see in L1 int i = 0; - int lower_bound = 10; + int lowerBound = 10; while (i < 10000) { - int start = lower_bound + 1 + r.nextInt(30); + int start = lowerBound + 1 + r.nextInt(30); int end = start + 10 + r.nextInt(30); - lower_bound = end; + lowerBound = end; SSTableReader l1sstable = MockSchema.sstableWithLevel(i, start, end, 1, cfs); sstables.add(l1sstable); i++; @@ -679,10 +679,9 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException SSTableReader sstable = MockSchema.sstableWithLevel(i, newStart.getLongValue(), newEnd.getLongValue(), 0, cfs); Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, (TreeSet) generations.get(1)); Collection linearScanIntersectionSSTables = overlapping(sstable.getFirst().getToken(), sstable.getLast().getToken(), generations.get(1)); - linearScanIntersectionSSTables.add(sstable); // The results should be equivalent to doing a linear scan - assertTrue(treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); + assertTrue("[seed = " + seed + " tree set and linear scan produce different results]", treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); } private int getTaskLevel(ColumnFamilyStore cfs) From 081050131145a34fbdeea4b6d7ebe6020330cb5d Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Wed, 26 Aug 2026 12:14:10 -0700 Subject: [PATCH 03/10] modify tests --- .../LeveledCompactionStrategyTest.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java index 80e37180e05b..0bf02c77eadb 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java @@ -612,8 +612,8 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException int lowerBound = 10; while (i < 10000) { - int start = lowerBound + 1 + r.nextInt(30); - int end = start + 10 + r.nextInt(30); + int start = lowerBound + 1 + r.nextInt(15); + int end = start + r.nextInt(15); lowerBound = end; SSTableReader l1sstable = MockSchema.sstableWithLevel(i, start, end, 1, cfs); sstables.add(l1sstable); @@ -627,7 +627,7 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException Token newStart; Token newEnd; - int kind = r.nextInt(7); + int kind = r.nextInt(8); switch (kind) { @@ -657,13 +657,13 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException break; // Superset of entire SSTable case 4: - sstableinL1 = sstables.get(0); + sstableinL1 = sstables.get(r.nextInt(sstables.size())); newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); newEnd = sstableinL1.getLast().getToken().increaseSlightly(); break; // Exact SSTable match case 5: - sstableinL1 = sstables.get(0); + sstableinL1 = sstables.get(r.nextInt(sstables.size())); newStart = sstableinL1.getFirst().getToken(); newEnd = sstableinL1.getLast().getToken(); break; @@ -672,6 +672,12 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException newStart = sstables.get(0).getFirst().getToken(); newEnd = sstables.get(sstables.size() - 1).getLast().getToken(); break; + // Single token SSTables + case 7: + sstableinL1 = sstables.get(r.nextInt(sstables.size())); + newStart = sstableinL1.getFirst().getToken(); + newEnd = sstableinL1.getFirst().getToken(); + break; default: throw new IllegalStateException("Unhandled kind " + kind); } From acbaa86cc0f8510f155318b5da6f2adae9c1d2e8 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Wed, 26 Aug 2026 12:15:26 -0700 Subject: [PATCH 04/10] remove cast from test --- .../db/compaction/LeveledCompactionStrategyTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java index 0bf02c77eadb..90e48f927ea2 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java @@ -30,7 +30,6 @@ import java.util.Map; import java.util.Random; import java.util.Set; -import java.util.TreeSet; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; @@ -683,11 +682,11 @@ public void testLinearScanTreeSetIntersectionEquivalence() throws IOException } SSTableReader sstable = MockSchema.sstableWithLevel(i, newStart.getLongValue(), newEnd.getLongValue(), 0, cfs); - Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, (TreeSet) generations.get(1)); - Collection linearScanIntersectionSSTables = overlapping(sstable.getFirst().getToken(), sstable.getLast().getToken(), generations.get(1)); + Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, generations.getSortedLevel(1)); + Collection linearScanIntersectionSSTables = overlapping(sstable.getFirst().getToken(), sstable.getLast().getToken(), generations.getSortedLevel(1)); // The results should be equivalent to doing a linear scan - assertTrue("[seed = " + seed + " tree set and linear scan produce different results]", treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); + assertTrue("[seed = " + seed + " treeSet and linear scan produce different results]", treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); } private int getTaskLevel(ColumnFamilyStore cfs) From 752a7d33529b91c5def59a3714ddd239ca50b165 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Wed, 26 Aug 2026 23:15:08 -0700 Subject: [PATCH 05/10] fix comment --- .../org/apache/cassandra/db/compaction/LeveledGenerations.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java index d4d76ebc9a5a..28ac9bbb9e85 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java @@ -99,7 +99,7 @@ Set get(int level) TreeSet getSortedLevel(int level) { if (level > levelCount() - 1 || level <= 0) - throw new ArrayIndexOutOfBoundsException("Invalid generation " + level + " - maximum is " + (levelCount() - 1)); + throw new ArrayIndexOutOfBoundsException("Invalid sorted generation " + level + " - maximum is " + (levelCount() - 1) + " and minimum is 1"); return levels[level - 1]; } From 8201829fb4cac604d60101d5353f3f9a35700c97 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Mon, 31 Aug 2026 14:00:06 -0700 Subject: [PATCH 06/10] change visibility --- .../org/apache/cassandra/db/compaction/LeveledManifest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java index 874020ccba72..8d7705ab7fe4 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java @@ -451,8 +451,7 @@ private static Set overlapping(Collection candidat return overlapping(first, last, others); } - @VisibleForTesting - static Set overlappingWithBounds(SSTableReader sstable, Map> others) + private static Set overlappingWithBounds(SSTableReader sstable, Map> others) { return overlappingWithBounds(sstable.getFirst().getToken(), sstable.getLast().getToken(), others); } From 4b4d06d0bbedcfd60098b103cd6cebdff0f0dd51 Mon Sep 17 00:00:00 2001 From: David Capwell Date: Mon, 31 Aug 2026 14:54:25 -0700 Subject: [PATCH 07/10] review feedback: reuse Property.qt and made it so we cover all 8 cases each run --- .../LeveledCompactionStrategyTest.java | 165 +++++++++--------- 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java index 90e48f927ea2..c8dc2319378d 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java @@ -75,6 +75,7 @@ import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.TimeUUID; +import static accord.utils.Property.qt; import static java.util.Collections.singleton; import static org.apache.cassandra.db.compaction.LeveledManifest.getIntersectingSSTablesFromTreeSet; import static org.apache.cassandra.db.compaction.LeveledManifest.overlapping; @@ -598,95 +599,95 @@ public void testDisableSTCSInL0() throws IOException } @Test - public void testLinearScanTreeSetIntersectionEquivalence() throws IOException + public void testLinearScanTreeSetIntersectionEquivalence() { - ColumnFamilyStore cfs = MockSchema.newCFS(); - List sstables = new ArrayList<>(); - - long seed = System.currentTimeMillis(); - Random r = new Random(seed); - - // Generates disjoint sorted SSTables that match what we see in L1 - int i = 0; - int lowerBound = 10; - while (i < 10000) - { - int start = lowerBound + 1 + r.nextInt(15); - int end = start + r.nextInt(15); - lowerBound = end; - SSTableReader l1sstable = MockSchema.sstableWithLevel(i, start, end, 1, cfs); - sstables.add(l1sstable); - i++; - } + qt().withExamples(10).check(rs -> { + ColumnFamilyStore cfs = MockSchema.newCFS(); + List sstables = new ArrayList<>(); + + // Generates disjoint sorted SSTables that match what we see in L1 + int i = 0; + int lowerBound = 10; + while (i < 10000) + { + int start = lowerBound + 1 + rs.nextInt(15); + int end = start + rs.nextInt(15); + lowerBound = end; + sstables.add(MockSchema.sstableWithLevel(i++, start, end, 1, cfs)); + } - LeveledGenerations generations = new LeveledGenerations(); - generations.addAll(sstables); + LeveledGenerations generations = new LeveledGenerations(); + generations.addAll(sstables); - SSTableReader sstableinL1; - Token newStart; - Token newEnd; + // Every kind of overlap gets tested; randomness only selects the tokens + for (Overlap overlap : Overlap.values()) + { + SSTableReader sstableinL1 = rs.pick(sstables); + Token newStart; + Token newEnd; - int kind = r.nextInt(8); + switch (overlap) + { + case NONE: + newStart = sstableinL1.getLast().getToken().increaseSlightly(); + newEnd = newStart.getToken().increaseSlightly(); + break; + case LEFT_END: + newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); + newEnd = sstableinL1.getFirst().getToken().increaseSlightly(); + break; + case RIGHT_END: + newStart = sstableinL1.getLast().getToken().decreaseSlightly(); + newEnd = sstableinL1.getLast().getToken().increaseSlightly(); + break; + case SUBSET: + newStart = sstableinL1.getFirst().getToken().increaseSlightly(); + newEnd = sstableinL1.getLast().getToken().decreaseSlightly(); + break; + case SUPERSET: + newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); + newEnd = sstableinL1.getLast().getToken().increaseSlightly(); + break; + case EXACT_MATCH: + newStart = sstableinL1.getFirst().getToken(); + newEnd = sstableinL1.getLast().getToken(); + break; + case WHOLE_LEVEL: + newStart = sstables.get(0).getFirst().getToken(); + newEnd = sstables.get(sstables.size() - 1).getLast().getToken(); + break; + case SINGLE_TOKEN: + newStart = sstableinL1.getFirst().getToken(); + newEnd = sstableinL1.getFirst().getToken(); + break; + default: + throw new IllegalStateException("Unhandled overlap " + overlap); + } - switch (kind) - { - // No overlap - case 0: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getLast().getToken().increaseSlightly(); - newEnd = newStart.getToken().increaseSlightly(); - break; - // Overlaps on the left end - case 1: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); - newEnd = sstableinL1.getFirst().getToken().increaseSlightly(); - break; - // Overlaps on the right end - case 2: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getLast().getToken().decreaseSlightly(); - newEnd = sstableinL1.getLast().getToken().increaseSlightly(); - break; - // Subset of entire SSTable - case 3: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getFirst().getToken().increaseSlightly(); - newEnd = sstableinL1.getLast().getToken().decreaseSlightly(); - break; - // Superset of entire SSTable - case 4: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getFirst().getToken().decreaseSlightly(); - newEnd = sstableinL1.getLast().getToken().increaseSlightly(); - break; - // Exact SSTable match - case 5: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getFirst().getToken(); - newEnd = sstableinL1.getLast().getToken(); - break; - // Spans the whole L1 - case 6: - newStart = sstables.get(0).getFirst().getToken(); - newEnd = sstables.get(sstables.size() - 1).getLast().getToken(); - break; - // Single token SSTables - case 7: - sstableinL1 = sstables.get(r.nextInt(sstables.size())); - newStart = sstableinL1.getFirst().getToken(); - newEnd = sstableinL1.getFirst().getToken(); - break; - default: - throw new IllegalStateException("Unhandled kind " + kind); - } + SSTableReader sstable = MockSchema.sstableWithLevel(i++, newStart.getLongValue(), newEnd.getLongValue(), 0, cfs); + Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, generations.getSortedLevel(1)); + Collection linearScanIntersectionSSTables = overlapping(sstable.getFirst().getToken(), sstable.getLast().getToken(), generations.getSortedLevel(1)); - SSTableReader sstable = MockSchema.sstableWithLevel(i, newStart.getLongValue(), newEnd.getLongValue(), 0, cfs); - Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, generations.getSortedLevel(1)); - Collection linearScanIntersectionSSTables = overlapping(sstable.getFirst().getToken(), sstable.getLast().getToken(), generations.getSortedLevel(1)); + // The results should be equivalent to doing a linear scan + assertTrue("treeSet and linear scan produce different results for overlap " + overlap, + treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); + } + }); + } - // The results should be equivalent to doing a linear scan - assertTrue("[seed = " + seed + " treeSet and linear scan produce different results]", treeSetIntersectingSSTables.containsAll(linearScanIntersectionSSTables) && linearScanIntersectionSSTables.containsAll(treeSetIntersectingSSTables)); + /** + * The ways a new L0 SSTable can overlap the SSTables in L1. + */ + private enum Overlap + { + NONE, + LEFT_END, + RIGHT_END, + SUBSET, + SUPERSET, + EXACT_MATCH, + WHOLE_LEVEL, + SINGLE_TOKEN } private int getTaskLevel(ColumnFamilyStore cfs) From ba70a336e4838b2f2446fa51f3943b3bc408b073 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Mon, 31 Aug 2026 15:12:35 -0700 Subject: [PATCH 08/10] fix test --- .../LeveledCompactionStrategyTest.java | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java index c8dc2319378d..d352fd612560 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java @@ -607,12 +607,17 @@ public void testLinearScanTreeSetIntersectionEquivalence() // Generates disjoint sorted SSTables that match what we see in L1 int i = 0; - int lowerBound = 10; - while (i < 10000) + int start; + int end = 10; + while (i < 1000) { - int start = lowerBound + 1 + rs.nextInt(15); - int end = start + rs.nextInt(15); - lowerBound = end; + // Start with at least offset 1 to ensure that SSTables are disjoint + start = end + rs.nextInt(1, 15); + + // Include space in between SSTables, so we don't hit the case where when + // going through Overlap.SUBSET we generate a SSTable that has start > end + end = start + rs.nextInt(5, 20); + sstables.add(MockSchema.sstableWithLevel(i++, start, end, 1, cfs)); } @@ -675,6 +680,19 @@ public void testLinearScanTreeSetIntersectionEquivalence() }); } + @Test + public void testTreeSetIntersectionForEmptyNextLevelIsEmpty() + { + ColumnFamilyStore cfs = MockSchema.newCFS(); + LeveledGenerations generations = new LeveledGenerations(); + + SSTableReader sstable = MockSchema.sstableWithLevel(0, 1, 10, 0, cfs); + Collection treeSetIntersectingSSTables = getIntersectingSSTablesFromTreeSet(sstable, generations.getSortedLevel(1)); + + assertTrue("treeSet and linear scan produce different results", + treeSetIntersectingSSTables.isEmpty()); + } + /** * The ways a new L0 SSTable can overlap the SSTables in L1. */ From 203f1e4ec66786a15e3110a2debdd845839dbee8 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Mon, 31 Aug 2026 15:14:28 -0700 Subject: [PATCH 09/10] include comment --- src/java/org/apache/cassandra/db/compaction/LeveledManifest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java index 8d7705ab7fe4..2abbc8b2598b 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java @@ -596,6 +596,7 @@ private Collection getCandidatesFor(int level) return Collections.emptyList(); } + // Precondition: sstables in sstablesNextLevel must be disjoint @VisibleForTesting protected static Set getIntersectingSSTablesFromTreeSet(SSTableReader sstable, TreeSet sstablesNextLevel) { From 9fe75b72b8e0cae5aac710587399c7088e0e51b4 Mon Sep 17 00:00:00 2001 From: Alan Wang Date: Mon, 31 Aug 2026 15:32:54 -0700 Subject: [PATCH 10/10] fix comment type --- .../org/apache/cassandra/db/compaction/LeveledManifest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java index 2abbc8b2598b..021604ea50a6 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledManifest.java @@ -596,7 +596,9 @@ private Collection getCandidatesFor(int level) return Collections.emptyList(); } - // Precondition: sstables in sstablesNextLevel must be disjoint + /** + * Precondition: SSTables in sstablesNextLevel must be disjoint + */ @VisibleForTesting protected static Set getIntersectingSSTablesFromTreeSet(SSTableReader sstable, TreeSet sstablesNextLevel) {