This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 7238b62fa2 Improve algorithmic complexity for finding intersecting 
SSTables for LCS compaction candidates
7238b62fa2 is described below

commit 7238b62fa250098fa876fa7cbeb1e3608a6d67d6
Author: Alan Wang <[email protected]>
AuthorDate: Tue Nov 3 14:30:35 2026 -0800

    Improve algorithmic complexity for finding intersecting SSTables for LCS 
compaction candidates
    
    patch by Alan Wang; reviewed by David Capwell, Marcus Eriksson for 
CASSANDRA-21605
---
 CHANGES.txt                                        |   1 +
 .../db/compaction/LeveledGenerations.java          |   7 ++
 .../cassandra/db/compaction/LeveledManifest.java   |  34 ++++++-
 .../compaction/LeveledCompactionStrategyTest.java  | 113 +++++++++++++++++++++
 4 files changed, 153 insertions(+), 2 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 85c016fde8..5c9f4f2278 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 7.0
+ * Improve algorithmic complexity for finding intersecting SSTables for LCS 
compaction candidates (CASSANDRA-21605)
  * Clarify off-heap OOM error message in JVMStabilityInspector 
(CASSANDRA-19831)
  * FQLTool Replay support for full set of login options (CASSANDRA-17559)
  * Extract reusable HttpService from cloud metadata connector (CASSANDRA-21195)
diff --git 
a/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java 
b/src/java/org/apache/cassandra/db/compaction/LeveledGenerations.java
index 513e02aad9..28ac9bbb9e 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 @@ class LeveledGenerations
         return levels[level - 1];
     }
 
+    TreeSet<SSTableReader> getSortedLevel(int level)
+    {
+        if (level > levelCount() - 1 || level <= 0)
+            throw new ArrayIndexOutOfBoundsException("Invalid sorted 
generation " + level + " - maximum is " + (levelCount() - 1) + " and minimum is 
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 5e85511726..0d7bba4c3b 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.Iterator;
 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;
@@ -573,14 +574,17 @@ public class LeveledManifest
                 return candidates;
         }
 
+        // We know that because we are in level L0+, this is a tree set with 
disjoint SSTables
+        TreeSet<SSTableReader> 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
-        Map<SSTableReader, Bounds<Token>> sstablesNextLevel = 
genBounds(generations.get(level + 1));
         Iterator<SSTableReader> levelIterator = 
generations.wrappingIterator(level, lastCompactedSSTables[level]);
         while (levelIterator.hasNext())
         {
             SSTableReader sstable = levelIterator.next();
-            Set<SSTableReader> candidates = 
Sets.union(Collections.singleton(sstable), overlappingWithBounds(sstable, 
sstablesNextLevel));
+            Set<SSTableReader> candidates = 
getIntersectingSSTablesFromTreeSet(sstable, sstablesNextLevel);
+            candidates.add(sstable);
 
             if (Iterables.any(candidates, SSTableReader::isMarkedSuspect))
                 continue;
@@ -592,6 +596,32 @@ public class LeveledManifest
         return Collections.emptyList();
     }
 
+    /**
+     * Precondition: SSTables in sstablesNextLevel must be disjoint
+     */
+    @VisibleForTesting
+    protected static Set<SSTableReader> 
getIntersectingSSTablesFromTreeSet(SSTableReader sstable, 
TreeSet<SSTableReader> sstablesNextLevel)
+    {
+        Set<SSTableReader> candidates = new HashSet<>();
+
+        SSTableReader start = sstablesNextLevel.floor(sstable);
+        Iterator<SSTableReader> it;
+
+        if (start == null)
+            it = sstablesNextLevel.iterator();
+        else
+            it =  sstablesNextLevel.tailSet(start,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<SSTableReader> getCompactingL0()
     {
         Set<SSTableReader> 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 0aeab98f79..d352fd6125 100644
--- 
a/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java
+++ 
b/test/unit/org/apache/cassandra/db/compaction/LeveledCompactionStrategyTest.java
@@ -75,7 +75,10 @@ import org.apache.cassandra.utils.FBUtilities;
 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;
 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,116 @@ public class LeveledCompactionStrategyTest
         }
     }
 
+    @Test
+    public void testLinearScanTreeSetIntersectionEquivalence()
+    {
+        qt().withExamples(10).check(rs -> {
+            ColumnFamilyStore cfs = MockSchema.newCFS();
+            List<SSTableReader> sstables = new ArrayList<>();
+
+            // Generates disjoint sorted SSTables that match what we see in L1
+            int i = 0;
+            int start;
+            int end = 10;
+            while (i < 1000)
+            {
+                // 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));
+            }
+
+            LeveledGenerations generations = new LeveledGenerations();
+            generations.addAll(sstables);
+
+            // 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;
+
+                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);
+                }
+
+                SSTableReader sstable = MockSchema.sstableWithLevel(i++, 
newStart.getLongValue(), newEnd.getLongValue(), 0, cfs);
+                Collection<SSTableReader> treeSetIntersectingSSTables = 
getIntersectingSSTablesFromTreeSet(sstable, generations.getSortedLevel(1));
+                Collection<SSTableReader> 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));
+            }
+        });
+    }
+
+    @Test
+    public void testTreeSetIntersectionForEmptyNextLevelIsEmpty()
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        LeveledGenerations generations = new LeveledGenerations();
+
+        SSTableReader sstable = MockSchema.sstableWithLevel(0, 1, 10, 0, cfs);
+        Collection<SSTableReader> 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.
+     */
+    private enum Overlap
+    {
+        NONE,
+        LEFT_END,
+        RIGHT_END,
+        SUBSET,
+        SUPERSET,
+        EXACT_MATCH,
+        WHOLE_LEVEL,
+        SINGLE_TOKEN
+    }
+
     private int getTaskLevel(ColumnFamilyStore cfs)
     {
         int level = -1;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to