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

frankgh pushed a commit to branch cep-45-mutation-tracking
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cep-45-mutation-tracking by 
this push:
     new e71bdecb62 CEP-45: Fix OpOrder.Group leak in 
ReadExecutionController.forCommand()
e71bdecb62 is described below

commit e71bdecb62b5f774304816ad79a3641223ec3d4b
Author: Francisco Guerrero <[email protected]>
AuthorDate: Thu Aug 20 16:43:54 2026 -0700

    CEP-45: Fix OpOrder.Group leak in ReadExecutionController.forCommand()
    
    patch by Francisco Guerrero; reviewed by Mick Semb Wever for CASSANDRA-21593
---
 .../cassandra/db/ReadExecutionController.java      |  29 ++-
 .../service/reads/repair/AbstractReadRepair.java   |   6 +-
 .../cassandra/db/ReadExecutionControllerTest.java  | 194 +++++++++++++++++++++
 3 files changed, 220 insertions(+), 9 deletions(-)

diff --git a/src/java/org/apache/cassandra/db/ReadExecutionController.java 
b/src/java/org/apache/cassandra/db/ReadExecutionController.java
index 513199fdd5..5d9b636dcb 100644
--- a/src/java/org/apache/cassandra/db/ReadExecutionController.java
+++ b/src/java/org/apache/cassandra/db/ReadExecutionController.java
@@ -158,17 +158,20 @@ public class ReadExecutionController implements 
AutoCloseable
 
         long createdAtNanos = baseCfs.metric.topLocalReadQueryTime.isEnabled() 
? clock.now() : NO_SAMPLING;
 
-        if (indexCfs == null)
-            return new ReadExecutionController(command, 
baseCfs.readOrdering.start(), baseCfs.metadata(), null, null, createdAtNanos, 
trackRepairedStatus);
-
         OpOrder.Group baseOp = null;
+        OpOrder.Group indexBaseOp = null;
         WriteContext writeContext = null;
         ReadExecutionController indexController = null;
         // OpOrder.start() shouldn't fail, but better safe than sorry.
         try
         {
             baseOp = baseCfs.readOrdering.start();
-            indexController = new ReadExecutionController(command, 
indexCfs.readOrdering.start(), indexCfs.metadata(), null, null, NO_SAMPLING, 
false);
+
+            if (indexCfs == null)
+                return new ReadExecutionController(command, baseOp, 
baseCfs.metadata(), null, null, createdAtNanos, trackRepairedStatus);
+
+            indexBaseOp = indexCfs.readOrdering.start();
+            indexController = new ReadExecutionController(command, 
indexBaseOp, indexCfs.metadata(), null, null, NO_SAMPLING, false);
             /*
              * TODO: this should perhaps not open and maintain a writeOp for 
the full duration, but instead only *try*
              * to delete stale entries, without blocking if there's no room
@@ -179,8 +182,6 @@ public class ReadExecutionController implements 
AutoCloseable
         }
         catch (RuntimeException e)
         {
-            // Note that must have writeContext == null since ReadOrderGroup 
ctor can't fail
-            assert writeContext == null;
             try
             {
                 if (baseOp != null)
@@ -189,7 +190,21 @@ public class ReadExecutionController implements 
AutoCloseable
             finally
             {
                 if (indexController != null)
-                    indexController.close();
+                {
+                    try
+                    {
+                        indexController.close();
+                    }
+                    finally
+                    {
+                        if (writeContext != null)
+                            writeContext.close();
+                    }
+                }
+                else if (indexBaseOp != null)
+                {
+                    indexBaseOp.close();
+                }
             }
             throw e;
         }
diff --git 
a/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java 
b/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java
index a84259ee9e..2e578c595e 100644
--- a/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java
+++ b/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java
@@ -149,9 +149,11 @@ public abstract class AbstractReadRepair<E extends 
Endpoints<E>, P extends Repli
          * pending repair session which has been committed. In addition to the 
digest, a set of ids for any pending but 
          * as yet uncommitted repair sessions is recorded and returned to the 
coordinator. This is to help reduce false 
          * positives caused by compaction lagging which can leave sstables 
from committed sessions in the pending state
-         * for a time.
+         * for a time. We don't track the repaired status for tables whose 
replication type is tracked which purely
+         * relies on the mutation-tracking log to track reconciliation of 
mutations.
          */
-        boolean trackRepairedStatus = 
DatabaseDescriptor.getRepairedDataTrackingForPartitionReadsEnabled();
+        boolean trackRepairedStatus = 
DatabaseDescriptor.getRepairedDataTrackingForPartitionReadsEnabled()
+                                      && 
!command.metadata().replicationType().isTracked();
 
         // Do a full data read to resolve the correct response (and repair 
node that need be)
         DataResolver<E, P> resolver = new DataResolver<>(coordinator, command, 
replicaPlan, this, requestTime, trackRepairedStatus);
diff --git a/test/unit/org/apache/cassandra/db/ReadExecutionControllerTest.java 
b/test/unit/org/apache/cassandra/db/ReadExecutionControllerTest.java
new file mode 100644
index 0000000000..5e836c58ac
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/ReadExecutionControllerTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.ServerTestUtils;
+import org.apache.cassandra.Util;
+import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.StubIndex;
+import org.apache.cassandra.replication.MutationJournal;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.ReplicationType;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.Tables;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.AssertUtil;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link ReadExecutionController}
+ */
+public class ReadExecutionControllerTest
+{
+    private static final String KEYSPACE = "read_execution_controller_test_ks";
+    private static final String TABLE = "table1";
+    private static final String INDEX_TABLE = "table1_index";
+
+    private static ColumnFamilyStore cfs;
+    private static ColumnFamilyStore indexCfs;
+
+    @BeforeClass
+    public static void init() throws Exception
+    {
+        ServerTestUtils.prepareServerNoRegister();
+        MutationJournal.start();
+
+        TableMetadata table = TableMetadata.builder(KEYSPACE, TABLE)
+                                           .addPartitionKeyColumn("pk", 
Int32Type.instance)
+                                           .addRegularColumn("v1", 
Int32Type.instance)
+                                           .build();
+        TableMetadata indexTable = TableMetadata.builder(KEYSPACE, INDEX_TABLE)
+                                                .addPartitionKeyColumn("pk", 
Int32Type.instance)
+                                                .addRegularColumn("v1", 
Int32Type.instance)
+                                                .build();
+        KeyspaceMetadata ks = KeyspaceMetadata.create(KEYSPACE,
+                                                      KeyspaceParams.simple(1, 
ReplicationType.tracked),
+                                                      Tables.of(table, 
indexTable));
+        ClusterMetadataTestHelper.addOrUpdateKeyspace(ks);
+        ServerTestUtils.markCMS();
+        StorageService.instance.unsafeSetInitialized();
+
+        cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE);
+        indexCfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(INDEX_TABLE);
+    }
+
+    @Test
+    public void forCommandDoesNotLeakOpOrderGroupWhenRejectingTrackedTable()
+    {
+        ReadCommand command = 
SinglePartitionReadCommand.fullPartitionRead(cfs.metadata(), 
FBUtilities.nowInSeconds(), Util.dk(1));
+
+        try
+        {
+            ReadExecutionController.forCommand(command, true);
+            fail("Expected forCommand to throw because tracking the repaired 
status is unsupported for tracked reads");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertTrue("Unexpected exception message: " + e.getMessage(),
+                       e.getMessage().contains("Tracking repaired status is 
not supported for tracked reads"));
+        }
+
+        AssertUtil.assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
+            OpOrder.Barrier barrier = cfs.readOrdering.newBarrier();
+            barrier.issue();
+            barrier.await();
+        });
+    }
+
+    @Test
+    public void 
forCommandDoesNotLeakOpOrderGroupWhenRejectingTrackedTableWithIndex()
+    {
+        Index index = new BackedStubIndex(cfs, 
IndexMetadata.fromSchemaMetadata("stub_index", IndexMetadata.Kind.CUSTOM, 
Collections.emptyMap()));
+        Index.QueryPlan queryPlan = new SingleIndexQueryPlan(index);
+
+        ReadCommand command = SinglePartitionReadCommand.create(cfs.metadata(),
+                                                                
FBUtilities.nowInSeconds(),
+                                                                
ColumnFilter.all(cfs.metadata()),
+                                                                
RowFilter.none(),
+                                                                
DataLimits.NONE,
+                                                                Util.dk(1),
+                                                                new 
ClusteringIndexSliceFilter(Slices.ALL, false),
+                                                                queryPlan);
+
+        try
+        {
+            ReadExecutionController.forCommand(command, true);
+            fail("Expected forCommand to throw because tracking the repaired 
status is unsupported for tracked reads");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertTrue("Unexpected exception message: " + e.getMessage(),
+                       e.getMessage().contains("Tracking repaired status is 
not supported for tracked reads"));
+        }
+
+        // baseOp must have been released despite the exception
+        AssertUtil.assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
+            OpOrder.Barrier barrier = cfs.readOrdering.newBarrier();
+            barrier.issue();
+            barrier.await();
+        });
+        // ensure the index's OpOrder is released too
+        AssertUtil.assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
+            OpOrder.Barrier barrier = indexCfs.readOrdering.newBarrier();
+            barrier.issue();
+            barrier.await();
+        });
+    }
+
+    private static class BackedStubIndex extends StubIndex
+    {
+        BackedStubIndex(ColumnFamilyStore baseCfs, IndexMetadata metadata)
+        {
+            super(baseCfs, metadata);
+        }
+
+        @Override
+        public Optional<ColumnFamilyStore> getBackingTable()
+        {
+            return Optional.of(indexCfs);
+        }
+    }
+
+    private static class SingleIndexQueryPlan implements Index.QueryPlan
+    {
+        private final Index index;
+
+        SingleIndexQueryPlan(Index index)
+        {
+            this.index = index;
+        }
+
+        @Override
+        public Set<Index> getIndexes()
+        {
+            return Collections.singleton(index);
+        }
+
+        @Override
+        public Index.Searcher searcherFor(ReadCommand command)
+        {
+            return index.searcherFor(command);
+        }
+
+        @Override
+        public RowFilter postIndexQueryFilter()
+        {
+            return RowFilter.none();
+        }
+    }
+}


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

Reply via email to