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

jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 3154bffd068 [Subscription] Prevent unsubscribe from blocking on 
prefetch cleanup (#18742)
3154bffd068 is described below

commit 3154bffd068ee1e3d0206218add6b63ca6ecf003
Author: Caideyipi <[email protected]>
AuthorDate: Tue Sep 29 15:46:05 2026 +0800

    [Subscription] Prevent unsubscribe from blocking on prefetch cleanup 
(#18742)
---
 .../agent/SubscriptionConsumerAgent.java           | 147 +++++++++++++++------
 .../broker/ConsensusSubscriptionBroker.java        |   8 +-
 .../consensus/ConsensusPrefetchingQueue.java       |  44 +++++-
 .../agent/SubscriptionConsumerAgentTest.java       | 114 ++++++++++++++++
 .../consensus/ConsensusPrefetchingQueueTest.java   |  90 +++++++++++++
 5 files changed, 352 insertions(+), 51 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java
index a268f515a6a..79f0c59ec7e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java
@@ -34,6 +34,8 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
 
 public class SubscriptionConsumerAgent {
 
@@ -41,6 +43,9 @@ public class SubscriptionConsumerAgent {
 
   private final ConsumerGroupMetaKeeper consumerGroupMetaKeeper;
 
+  private final ConcurrentHashMap<String, ConsumerGroupOperationLock> 
consumerGroupOperationLocks =
+      new ConcurrentHashMap<>();
+
   public SubscriptionConsumerAgent() {
     this.consumerGroupMetaKeeper = new ConsumerGroupMetaKeeper();
   }
@@ -67,16 +72,19 @@ public class SubscriptionConsumerAgent {
 
   public TPushConsumerGroupMetaRespExceptionMessage 
handleSingleConsumerGroupMetaChanges(
       final ConsumerGroupMeta consumerGroupMetaFromCoordinator) {
-    acquireWriteLock();
+    final String consumerGroupId = 
consumerGroupMetaFromCoordinator.getConsumerGroupId();
     try {
-      if (consumerGroupMetaFromCoordinator.isEmpty()) {
-        
handleDropConsumerGroupInternal(consumerGroupMetaFromCoordinator.getConsumerGroupId());
-      } else {
-        
handleSingleConsumerGroupMetaChangesInternal(consumerGroupMetaFromCoordinator);
-      }
+      executeWithConsumerGroupLock(
+          consumerGroupId,
+          () -> {
+            if (consumerGroupMetaFromCoordinator.isEmpty()) {
+              handleDropConsumerGroupInternal(consumerGroupId);
+            } else {
+              
handleSingleConsumerGroupMetaChangesInternal(consumerGroupMetaFromCoordinator);
+            }
+          });
       return null;
     } catch (final Exception e) {
-      final String consumerGroupId = 
consumerGroupMetaFromCoordinator.getConsumerGroupId();
       LOGGER.warn(
           DataNodePipeMessages
               
.PIPE_LOG_EXCEPTION_OCCURRED_WHEN_HANDLING_SINGLE_CONSUMER_GROUP_META_10E7688C,
@@ -88,16 +96,13 @@ public class SubscriptionConsumerAgent {
               consumerGroupId, e);
       return new TPushConsumerGroupMetaRespExceptionMessage(
           consumerGroupId, exceptionMessage, System.currentTimeMillis());
-    } finally {
-      releaseWriteLock();
     }
   }
 
   private void handleSingleConsumerGroupMetaChangesInternal(
       final ConsumerGroupMeta metaFromCoordinator) {
     final String consumerGroupId = metaFromCoordinator.getConsumerGroupId();
-    final ConsumerGroupMeta metaInAgent =
-        consumerGroupMetaKeeper.getConsumerGroupMeta(consumerGroupId);
+    final ConsumerGroupMeta metaInAgent = 
getConsumerGroupMeta(consumerGroupId);
 
     // if consumer group meta does not exist on local agent
     if (Objects.isNull(metaInAgent)) {
@@ -106,7 +111,7 @@ public class SubscriptionConsumerAgent {
           consumerGroupId,
           metaFromCoordinator.getSubscribedTopicNames(),
           metaFromCoordinator.visibleUnder(true));
-      consumerGroupMetaKeeper.addConsumerGroupMeta(consumerGroupId, 
metaFromCoordinator);
+      replaceConsumerGroupMeta(consumerGroupId, metaFromCoordinator);
       return;
     }
 
@@ -133,8 +138,7 @@ public class SubscriptionConsumerAgent {
           consumerGroupId,
           metaFromCoordinator.getSubscribedTopicNames(),
           metaFromCoordinator.visibleUnder(true));
-      consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId);
-      consumerGroupMetaKeeper.addConsumerGroupMeta(consumerGroupId, 
metaFromCoordinator);
+      replaceConsumerGroupMeta(consumerGroupId, metaFromCoordinator);
       // no need to create broker manually
       return;
     }
@@ -182,8 +186,7 @@ public class SubscriptionConsumerAgent {
         () -> {
           // TODO: Currently we fully replace the entire ConsumerGroupMeta 
without carefully
           // checking the changes in its fields.
-          consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId);
-          consumerGroupMetaKeeper.addConsumerGroupMeta(consumerGroupId, 
metaFromCoordinator);
+          replaceConsumerGroupMeta(consumerGroupId, metaFromCoordinator);
         });
   }
 
@@ -198,38 +201,35 @@ public class SubscriptionConsumerAgent {
 
   public TPushConsumerGroupMetaRespExceptionMessage 
handleConsumerGroupMetaChanges(
       final List<ConsumerGroupMeta> consumerGroupMetasFromCoordinator) {
-    acquireWriteLock();
-    try {
-      for (final ConsumerGroupMeta consumerGroupMetaFromCoordinator :
-          consumerGroupMetasFromCoordinator) {
-        try {
-          
handleSingleConsumerGroupMetaChangesInternal(consumerGroupMetaFromCoordinator);
-        } catch (final Exception e) {
-          final String consumerGroupId = 
consumerGroupMetaFromCoordinator.getConsumerGroupId();
-          LOGGER.warn(
-              DataNodePipeMessages
-                  
.PIPE_LOG_EXCEPTION_OCCURRED_WHEN_HANDLING_SINGLE_CONSUMER_GROUP_META_10E7688C,
-              consumerGroupId,
-              e);
-          final String exceptionMessage =
-              String.format(
-                  "Subscription: Failed to handle single consumer group meta 
changes for consumer group %s, because %s",
-                  consumerGroupId, e);
-          return new TPushConsumerGroupMetaRespExceptionMessage(
-              consumerGroupId, exceptionMessage, System.currentTimeMillis());
-        }
+    for (final ConsumerGroupMeta consumerGroupMetaFromCoordinator :
+        consumerGroupMetasFromCoordinator) {
+      final String consumerGroupId = 
consumerGroupMetaFromCoordinator.getConsumerGroupId();
+      try {
+        executeWithConsumerGroupLock(
+            consumerGroupId,
+            () -> 
handleSingleConsumerGroupMetaChangesInternal(consumerGroupMetaFromCoordinator));
+      } catch (final Exception e) {
+        LOGGER.warn(
+            DataNodePipeMessages
+                
.PIPE_LOG_EXCEPTION_OCCURRED_WHEN_HANDLING_SINGLE_CONSUMER_GROUP_META_10E7688C,
+            consumerGroupId,
+            e);
+        final String exceptionMessage =
+            String.format(
+                "Subscription: Failed to handle single consumer group meta 
changes for consumer group %s, because %s",
+                consumerGroupId, e);
+        return new TPushConsumerGroupMetaRespExceptionMessage(
+            consumerGroupId, exceptionMessage, System.currentTimeMillis());
       }
-      return null;
-    } finally {
-      releaseWriteLock();
     }
+    return null;
   }
 
   public TPushConsumerGroupMetaRespExceptionMessage handleDropConsumerGroup(
       final String consumerGroupId) {
-    acquireWriteLock();
     try {
-      handleDropConsumerGroupInternal(consumerGroupId);
+      executeWithConsumerGroupLock(
+          consumerGroupId, () -> 
handleDropConsumerGroupInternal(consumerGroupId));
       return null;
     } catch (final Exception e) {
       LOGGER.warn(DataNodeMiscMessages.EXCEPTION_DROPPING_CONSUMER_GROUP, 
consumerGroupId, e);
@@ -238,8 +238,6 @@ public class SubscriptionConsumerAgent {
               "Subscription: Failed to drop consumer group %s, because %s", 
consumerGroupId, e);
       return new TPushConsumerGroupMetaRespExceptionMessage(
           consumerGroupId, exceptionMessage, System.currentTimeMillis());
-    } finally {
-      releaseWriteLock();
     }
   }
 
@@ -258,7 +256,68 @@ public class SubscriptionConsumerAgent {
           consumerGroupId);
     }
 
-    consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId);
+    removeConsumerGroupMeta(consumerGroupId);
+  }
+
+  private ConsumerGroupMeta getConsumerGroupMeta(final String consumerGroupId) 
{
+    acquireReadLock();
+    try {
+      return consumerGroupMetaKeeper.getConsumerGroupMeta(consumerGroupId);
+    } finally {
+      releaseReadLock();
+    }
+  }
+
+  private void replaceConsumerGroupMeta(
+      final String consumerGroupId, final ConsumerGroupMeta consumerGroupMeta) 
{
+    acquireWriteLock();
+    try {
+      consumerGroupMetaKeeper.addConsumerGroupMeta(consumerGroupId, 
consumerGroupMeta);
+    } finally {
+      releaseWriteLock();
+    }
+  }
+
+  private void removeConsumerGroupMeta(final String consumerGroupId) {
+    acquireWriteLock();
+    try {
+      consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId);
+    } finally {
+      releaseWriteLock();
+    }
+  }
+
+  void executeWithConsumerGroupLock(final String consumerGroupId, final 
Runnable operation) {
+    final ConsumerGroupOperationLock operationLock =
+        consumerGroupOperationLocks.compute(
+            consumerGroupId,
+            (ignored, existingLock) -> {
+              if (Objects.isNull(existingLock)) {
+                return new ConsumerGroupOperationLock();
+              }
+              existingLock.referenceCount++;
+              return existingLock;
+            });
+    operationLock.lock.lock();
+    try {
+      operation.run();
+    } finally {
+      operationLock.lock.unlock();
+      consumerGroupOperationLocks.computeIfPresent(
+          consumerGroupId,
+          (ignored, existingLock) -> {
+            if (existingLock != operationLock) {
+              return existingLock;
+            }
+            return --existingLock.referenceCount == 0 ? null : existingLock;
+          });
+    }
+  }
+
+  private static final class ConsumerGroupOperationLock {
+
+    private final ReentrantLock lock = new ReentrantLock(true);
+    private int referenceCount = 1;
   }
 
   public boolean isConsumerExisted(final String consumerGroupId, final String 
consumerId) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
index 4df735a7b76..9765cdc4ea0 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
@@ -710,7 +710,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
   }
 
   public void unbindConsensusPrefetchingQueue(final String topicName) {
-    closeAndRemoveConsensusPrefetchingQueues(topicName, true);
+    closeAndRemoveConsensusPrefetchingQueues(topicName, true, true);
   }
 
   @Override
@@ -816,11 +816,11 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
           topicName,
           brokerId);
     }
-    closeAndRemoveConsensusPrefetchingQueues(topicName, false);
+    closeAndRemoveConsensusPrefetchingQueues(topicName, false, false);
   }
 
   private void closeAndRemoveConsensusPrefetchingQueues(
-      final String topicName, final boolean warnIfMissing) {
+      final String topicName, final boolean warnIfMissing, final boolean 
removeProgressAfterClose) {
     final List<ConsensusPrefetchingQueue> queuesToClose;
     synchronized (queueLifecycleLock) {
       final List<ConsensusPrefetchingQueue> queues =
@@ -841,7 +841,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
     }
 
     for (final ConsensusPrefetchingQueue q : queuesToClose) {
-      q.close();
+      q.close(removeProgressAfterClose);
     }
     LOGGER.info(
         DataNodePipeMessages
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index 7f9ba6afd24..79cb048afac 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -19,6 +19,8 @@
 
 package org.apache.iotdb.db.subscription.broker.consensus;
 
+import org.apache.iotdb.commons.concurrent.IoTDBThreadPoolFactory;
+import org.apache.iotdb.commons.concurrent.ThreadName;
 import org.apache.iotdb.commons.consensus.ConsensusGroupId;
 import org.apache.iotdb.commons.subscription.config.SubscriptionConfig;
 import 
org.apache.iotdb.commons.subscription.meta.consumer.SubscriptionProgressSnapshot;
@@ -84,6 +86,7 @@ import java.util.PriorityQueue;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.PriorityBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -103,6 +106,10 @@ public class ConsensusPrefetchingQueue {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(ConsensusPrefetchingQueue.class);
 
+  private static final ExecutorService CLOSE_CLEANUP_EXECUTOR =
+      IoTDBThreadPoolFactory.newCachedThreadPoolWithDaemon(
+          ThreadName.SUBSCRIPTION_CONSENSUS_PREFETCH_EXECUTOR_POOL.getName() + 
"-Close-Cleanup");
+
   private final String consumerGroupId;
   private final String topicName;
   private final ConsensusGroupId consensusGroupId;
@@ -3726,6 +3733,10 @@ public class ConsensusPrefetchingQueue {
   }
 
   public void close() {
+    close(false);
+  }
+
+  public void close(final boolean removeProgressAfterClose) {
     final PendingSeekRequest seekRequestToFail;
     final Pair<ConsensusSubscriptionPrefetchExecutor, 
ConsensusPrefetchSubtask> prefetchBinding;
 
@@ -3760,9 +3771,32 @@ public class ConsensusPrefetchingQueue {
 
     if (Objects.nonNull(prefetchBinding.right)) {
       prefetchBinding.right.cancelPendingExecution();
-      prefetchBinding.right.awaitIdle();
+      if (prefetchBinding.right.isScheduledOrRunning()) {
+        CLOSE_CLEANUP_EXECUTOR.execute(
+            () -> finishCloseInBackground(prefetchBinding, 
removeProgressAfterClose));
+        return;
+      }
+    }
+
+    finishClose(prefetchBinding, removeProgressAfterClose);
+  }
+
+  private void finishCloseInBackground(
+      final Pair<ConsensusSubscriptionPrefetchExecutor, 
ConsensusPrefetchSubtask> prefetchBinding,
+      final boolean removeProgressAfterClose) {
+    try {
+      finishClose(prefetchBinding, removeProgressAfterClose);
+    } catch (final Exception e) {
+      LOGGER.warn("Failed to finish closing consensus prefetching queue {}", 
this, e);
     }
+  }
 
+  private void finishClose(
+      final Pair<ConsensusSubscriptionPrefetchExecutor, 
ConsensusPrefetchSubtask> prefetchBinding,
+      final boolean removeProgressAfterClose) {
+    if (Objects.nonNull(prefetchBinding.right)) {
+      prefetchBinding.right.awaitIdle();
+    }
     try {
       acquireWriteLock();
       try {
@@ -3790,8 +3824,12 @@ public class ConsensusPrefetchingQueue {
       try {
         cleanUp();
       } finally {
-        // Persist progress before closing
-        commitManager.persistAll();
+        if (removeProgressAfterClose) {
+          commitManager.removeAllStatesForTopic(consumerGroupId, topicName);
+        } else {
+          // Persist progress before closing.
+          commitManager.persistAll();
+        }
       }
       closeCleanupPending = false;
     } finally {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java
index 8ad28911e08..0d564ed0c72 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java
@@ -25,12 +25,21 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class SubscriptionConsumerAgentTest {
 
+  private static final long TEST_TIMEOUT_SECONDS = 5L;
+
   @Test
   public void testTopicDiffSetsUpBeforeTeardownAndMetaPublication() {
     final List<String> operations = new ArrayList<>();
@@ -62,4 +71,109 @@ public class SubscriptionConsumerAgentTest {
 
     assertEquals(Collections.singletonList("setup"), operations);
   }
+
+  @Test
+  public void testSameConsumerGroupMetaChangesAreSerialized() throws Exception 
{
+    final SubscriptionConsumerAgent agent = new SubscriptionConsumerAgent();
+    final ExecutorService executor = Executors.newFixedThreadPool(2);
+    final CountDownLatch firstEntered = new CountDownLatch(1);
+    final CountDownLatch releaseFirst = new CountDownLatch(1);
+    final CountDownLatch secondEntered = new CountDownLatch(1);
+    try {
+      final Future<?> first =
+          executor.submit(
+              () ->
+                  agent.executeWithConsumerGroupLock(
+                      "group",
+                      () -> {
+                        firstEntered.countDown();
+                        await(releaseFirst);
+                      }));
+      assertTrue(firstEntered.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+      final Future<?> second =
+          executor.submit(
+              () -> agent.executeWithConsumerGroupLock("group", 
secondEntered::countDown));
+      assertFalse(secondEntered.await(200L, TimeUnit.MILLISECONDS));
+
+      releaseFirst.countDown();
+      first.get(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      second.get(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      assertEquals(0L, secondEntered.getCount());
+    } finally {
+      releaseFirst.countDown();
+      executor.shutdownNow();
+      executor.awaitTermination(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testDifferentConsumerGroupMetaChangesRunIndependently() throws 
Exception {
+    final SubscriptionConsumerAgent agent = new SubscriptionConsumerAgent();
+    final ExecutorService executor = Executors.newFixedThreadPool(2);
+    final CountDownLatch firstEntered = new CountDownLatch(1);
+    final CountDownLatch releaseFirst = new CountDownLatch(1);
+    final CountDownLatch secondEntered = new CountDownLatch(1);
+    try {
+      final Future<?> first =
+          executor.submit(
+              () ->
+                  agent.executeWithConsumerGroupLock(
+                      "group-1",
+                      () -> {
+                        firstEntered.countDown();
+                        await(releaseFirst);
+                      }));
+      assertTrue(firstEntered.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+      final Future<?> second =
+          executor.submit(
+              () -> agent.executeWithConsumerGroupLock("group-2", 
secondEntered::countDown));
+      assertTrue(secondEntered.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+      releaseFirst.countDown();
+      first.get(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      second.get(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    } finally {
+      releaseFirst.countDown();
+      executor.shutdownNow();
+      executor.awaitTermination(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testConsumerMetaReadsRemainAvailableDuringGroupOperation() 
throws Exception {
+    final SubscriptionConsumerAgent agent = new SubscriptionConsumerAgent();
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final CountDownLatch operationEntered = new CountDownLatch(1);
+    final CountDownLatch releaseOperation = new CountDownLatch(1);
+    try {
+      final Future<?> operation =
+          executor.submit(
+              () ->
+                  agent.executeWithConsumerGroupLock(
+                      "group",
+                      () -> {
+                        operationEntered.countDown();
+                        await(releaseOperation);
+                      }));
+      assertTrue(operationEntered.await(TEST_TIMEOUT_SECONDS, 
TimeUnit.SECONDS));
+      assertFalse(agent.isConsumerExisted("group", "consumer"));
+
+      releaseOperation.countDown();
+      operation.get(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    } finally {
+      releaseOperation.countDown();
+      executor.shutdownNow();
+      executor.awaitTermination(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+  }
+
+  private static void await(final CountDownLatch latch) {
+    try {
+      latch.await();
+    } catch (final InterruptedException e) {
+      Thread.currentThread().interrupt();
+    }
+  }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index 6dabc8e2a19..69ece3840ef 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -43,6 +43,8 @@ import 
org.apache.iotdb.db.subscription.agent.SubscriptionAgent;
 import org.apache.iotdb.db.subscription.agent.SubscriptionConsumerAgent;
 import org.apache.iotdb.db.subscription.event.SubscriptionEvent;
 import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
+import 
org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutor;
+import org.apache.iotdb.db.subscription.task.subtask.ConsensusPrefetchSubtask;
 import org.apache.iotdb.rpc.subscription.config.TopicConstant;
 import org.apache.iotdb.rpc.subscription.payload.poll.ErrorPayload;
 import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
@@ -92,14 +94,88 @@ import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 public class ConsensusPrefetchingQueueTest {
 
+  private static final long TEST_TIMEOUT_SECONDS = 5L;
+
   @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
+  @Test
+  public void testCloseDetachesBlockedPrefetchCleanup() throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = temporaryFolder.newFolder("async-prefetch-close");
+    final CountDownLatch cleanupStarted = new CountDownLatch(1);
+    final CountDownLatch releaseCleanup = new CountDownLatch(1);
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
+      when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+      when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+
+      final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+      when(converter.getDatabaseName()).thenReturn("db");
+      when(converter.isTableModel()).thenReturn(true);
+
+      queue =
+          new ConsensusPrefetchingQueue(
+              "consumerGroup",
+              "topic",
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+              new DataRegionId(1),
+              serverImpl,
+              new SubscriptionWalRetentionPolicy(
+                  "topic",
+                  SubscriptionWalRetentionPolicy.UNBOUNDED,
+                  SubscriptionWalRetentionPolicy.UNBOUNDED),
+              converter,
+              newCommitManager(systemDir),
+              new RegionProgress(Collections.emptyMap()),
+              1L,
+              1L,
+              true);
+
+      final ConsensusPrefetchSubtask subtask = 
mock(ConsensusPrefetchSubtask.class);
+      final ConsensusSubscriptionPrefetchExecutor prefetchExecutor =
+          mock(ConsensusSubscriptionPrefetchExecutor.class);
+      when(subtask.isScheduledOrRunning()).thenReturn(true);
+      when(prefetchExecutor.isShutdown()).thenReturn(true);
+      doAnswer(
+              invocation -> {
+                cleanupStarted.countDown();
+                releaseCleanup.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+                return null;
+              })
+          .when(subtask)
+          .awaitIdle();
+      setPrefetchBinding(queue, prefetchExecutor, subtask);
+
+      final long closeStartNanos = System.nanoTime();
+      queue.close();
+      final long closeElapsedMillis =
+          TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - closeStartNanos);
+
+      assertTrue(closeElapsedMillis < 1000L);
+      assertTrue(cleanupStarted.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+      assertFalse(queue.isClosed());
+
+      releaseCleanup.countDown();
+      verify(subtask, 
timeout(TimeUnit.SECONDS.toMillis(TEST_TIMEOUT_SECONDS))).close();
+      assertTrue(queue.isClosed());
+    } finally {
+      releaseCleanup.countDown();
+      if (queue != null && !queue.isClosed()) {
+        queue.close();
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
   @Test
   public void testInitializationAndActivationUseIndependentMonitors() throws 
Exception {
     assertFalse(
@@ -2305,6 +2381,20 @@ public class ConsensusPrefetchingQueueTest {
     return (ReentrantReadWriteLock) field.get(queue);
   }
 
+  private static void setPrefetchBinding(
+      final ConsensusPrefetchingQueue queue,
+      final ConsensusSubscriptionPrefetchExecutor executor,
+      final ConsensusPrefetchSubtask subtask)
+      throws Exception {
+    final Field executorField =
+        ConsensusPrefetchingQueue.class.getDeclaredField("prefetchExecutor");
+    executorField.setAccessible(true);
+    executorField.set(queue, executor);
+    final Field subtaskField = 
ConsensusPrefetchingQueue.class.getDeclaredField("prefetchSubtask");
+    subtaskField.setAccessible(true);
+    subtaskField.set(queue, subtask);
+  }
+
   private static ProgressWALIterator subscriptionWalIterator(final 
ConsensusPrefetchingQueue queue)
       throws Exception {
     final Field field = 
ConsensusPrefetchingQueue.class.getDeclaredField("subscriptionWALIterator");

Reply via email to