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 b94aa09ab66 [Fix] Prevent consensus subscription teardown from 
blocking unsubscribe (#18804)
b94aa09ab66 is described below

commit b94aa09ab66a3b563c7c61c6bfb294152634fbce
Author: Caideyipi <[email protected]>
AuthorDate: Sat Oct 10 14:32:40 2026 +0800

    [Fix] Prevent consensus subscription teardown from blocking unsubscribe 
(#18804)
    
    * [Fix] Prevent consensus subscription teardown from blocking unsubscribe
    
    * Add consensus subscription unsubscribe integration test
---
 ...TDBConsensusSubscriptionUnsubscribeTableIT.java | 253 +++++++++++++++++++++
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   2 +
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   2 +
 .../broker/ConsensusSubscriptionBroker.java        |   9 +-
 .../consensus/ConsensusPrefetchingQueue.java       | 147 ++++++++----
 .../ConsensusSubscriptionPrefetchExecutor.java     |  10 +
 .../task/subtask/ConsensusPrefetchSubtask.java     |   1 +
 .../consensus/ConsensusPrefetchingQueueTest.java   | 183 +++++++++++++++
 8 files changed, 560 insertions(+), 47 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionUnsubscribeTableIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionUnsubscribeTableIT.java
new file mode 100644
index 00000000000..afa740b983e
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionUnsubscribeTableIT.java
@@ -0,0 +1,253 @@
+/*
+ * 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.iotdb.subscription.it.consensus.local.tablemodel;
+
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.session.subscription.ISubscriptionTableSession;
+import org.apache.iotdb.session.subscription.SubscriptionTableSessionBuilder;
+import 
org.apache.iotdb.session.subscription.consumer.table.SubscriptionTablePullConsumer;
+import org.apache.iotdb.session.subscription.model.Subscription;
+import org.apache.iotdb.subscription.it.AbstractSubscriptionIT;
+
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.Callable;
+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 java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({TableClusterIT.class})
+public class IoTDBConsensusSubscriptionUnsubscribeTableIT extends 
AbstractSubscriptionIT {
+
+  private static final long MAX_UNSUBSCRIBE_MS = 30_000L;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setConfigNodeConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS)
+        
.setSchemaRegionConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS)
+        .setDataRegionConsensusProtocolClass(ConsensusFactory.IOT_CONSENSUS)
+        .setSchemaReplicationFactor(1)
+        .setDataReplicationFactor(1)
+        .setAutoCreateSchemaEnabled(true)
+        .setPipeMemoryManagementEnabled(false)
+        .setIsPipeEnableMemoryCheck(false);
+    EnvFactory.getEnv().initClusterEnvironment(3, 1);
+  }
+
+  @Override
+  @After
+  public void tearDown() throws Exception {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+    super.tearDown();
+  }
+
+  @Test
+  public void testLastConsumerUnsubscribeDuringWritesAndImmediateResubscribe() 
throws Exception {
+    final ConsensusSubscriptionTableITSupport.TestIdentifiers ids =
+        
ConsensusSubscriptionTableITSupport.newIdentifiers("unsubscribe_during_writes");
+    final String database = ids.getDatabase();
+    final String table = "table_0";
+    final String firstConsumerId = ids.consumer("first");
+    final String lastConsumerId = ids.consumer("last");
+    final ExecutorService writer = Executors.newSingleThreadExecutor();
+    final ExecutorService unsubscribeExecutor =
+        Executors.newSingleThreadExecutor(
+            runnable -> {
+              final Thread thread = new Thread(runnable, 
"subscription-unsubscribe-it");
+              thread.setDaemon(true);
+              return thread;
+            });
+    final CountDownLatch writesStarted = new CountDownLatch(1);
+    final AtomicBoolean keepWriting = new AtomicBoolean(true);
+    SubscriptionTablePullConsumer consumer1 = null;
+    SubscriptionTablePullConsumer consumer2 = null;
+    Future<?> writeFuture = null;
+
+    try {
+      ConsensusSubscriptionTableITSupport.bootstrapDatabaseAndTable(
+          database, table, 
ConsensusSubscriptionTableITSupport.DEFAULT_TABLE_SCHEMA);
+      ConsensusSubscriptionTableITSupport.createConsensusTopic(ids.getTopic(), 
database, table);
+      consumer1 =
+          ConsensusSubscriptionTableITSupport.createConsumer(
+              firstConsumerId, ids.getConsumerGroupId());
+      consumer2 =
+          ConsensusSubscriptionTableITSupport.createConsumer(
+              lastConsumerId, ids.getConsumerGroupId());
+      consumer1.subscribe(ids.getTopic());
+      consumer2.subscribe(ids.getTopic());
+      awaitConsumerIds(ids.getTopic(), ids.getConsumerGroupId(), 
firstConsumerId, lastConsumerId);
+
+      writeFuture =
+          writer.submit(
+              (Callable<Void>)
+                  () -> {
+                    writeWhileSubscribed(database, table, keepWriting, 
writesStarted);
+                    return null;
+                  });
+      Assert.assertTrue("Writer did not start", writesStarted.await(30, 
TimeUnit.SECONDS));
+      if (writeFuture.isDone()) {
+        writeFuture.get();
+      }
+      Assert.assertFalse("Writer finished before unsubscribe", 
writeFuture.isDone());
+      consumer1.poll(Duration.ofMillis(100));
+
+      unsubscribeWithin(consumer1, ids.getTopic(), unsubscribeExecutor);
+      awaitConsumerIds(ids.getTopic(), ids.getConsumerGroupId(), 
lastConsumerId);
+      Assert.assertFalse("Writer finished before last unsubscribe", 
writeFuture.isDone());
+      unsubscribeWithin(consumer2, ids.getTopic(), unsubscribeExecutor);
+      awaitConsumerIds(ids.getTopic(), ids.getConsumerGroupId());
+
+      // A previous queue may still be closing. The new queue must retain its 
task registration.
+      consumer1.subscribe(ids.getTopic());
+      awaitConsumerIds(ids.getTopic(), ids.getConsumerGroupId(), 
firstConsumerId);
+
+      keepWriting.set(false);
+      writeFuture.get(30, TimeUnit.SECONDS);
+      final Set<String> expectedRows =
+          ConsensusSubscriptionTableITSupport.insertRows(database, table, 
1_000_000_000L, 20, true);
+      final ConsensusSubscriptionTableITSupport.ConsumedRecords consumed =
+          ConsensusSubscriptionTableITSupport.pollAndCommitUntilContains(
+              consumer1, expectedRows, 60);
+      Assert.assertTrue(consumed.toString(), 
consumed.getRowKeys().containsAll(expectedRows));
+    } finally {
+      keepWriting.set(false);
+      if (writeFuture != null && !writeFuture.isDone()) {
+        writeFuture.cancel(true);
+      }
+      writer.shutdownNow();
+      writer.awaitTermination(10, TimeUnit.SECONDS);
+      unsubscribeExecutor.shutdownNow();
+      closeQuietly(consumer2);
+      closeQuietly(consumer1);
+      ConsensusSubscriptionTableITSupport.cleanup(null, ids.getTopic(), 
database);
+    }
+  }
+
+  private static void writeWhileSubscribed(
+      final String database,
+      final String table,
+      final AtomicBoolean keepWriting,
+      final CountDownLatch writesStarted)
+      throws Exception {
+    try (final ITableSession session = 
EnvFactory.getEnv().getTableSessionConnection()) {
+      session.executeNonQueryStatement("use " + database);
+      for (long row = 0; keepWriting.get(); row++) {
+        final long timestamp = 1_000L + row;
+        session.executeNonQueryStatement(
+            String.format(
+                Locale.ROOT,
+                "insert into %s(tag1, s1, time) values ('writing', %d, %d)",
+                table,
+                timestamp,
+                timestamp));
+        if (row == 20) {
+          writesStarted.countDown();
+        }
+        Thread.sleep(10L);
+      }
+      session.executeNonQueryStatement("flush");
+    } finally {
+      writesStarted.countDown();
+    }
+  }
+
+  private static void unsubscribeWithin(
+      final SubscriptionTablePullConsumer consumer,
+      final String topicName,
+      final ExecutorService executor)
+      throws Exception {
+    final Future<?> future = executor.submit(() -> 
consumer.unsubscribe(topicName));
+    try {
+      future.get(MAX_UNSUBSCRIBE_MS, TimeUnit.MILLISECONDS);
+    } finally {
+      future.cancel(true);
+    }
+  }
+
+  private static void closeQuietly(final SubscriptionTablePullConsumer 
consumer) {
+    if (consumer != null) {
+      try {
+        consumer.close();
+      } catch (final Exception ignored) {
+        // Best effort cleanup after a failed assertion.
+      }
+    }
+  }
+
+  private static void awaitConsumerIds(
+      final String topicName, final String consumerGroupId, final String... 
expectedConsumerIds)
+      throws Exception {
+    try (final ISubscriptionTableSession session =
+        new SubscriptionTableSessionBuilder()
+            .host(EnvFactory.getEnv().getIP())
+            .port(Integer.parseInt(EnvFactory.getEnv().getPort()))
+            .build()) {
+      session.open();
+      Awaitility.await()
+          .pollInSameThread()
+          .pollInterval(Duration.ofMillis(500))
+          .atMost(Duration.ofSeconds(30))
+          .untilAsserted(
+              () -> {
+                final Set<Subscription> subscriptions = 
session.getSubscriptions(topicName);
+                if (expectedConsumerIds.length == 0) {
+                  Assert.assertTrue(subscriptions.toString(), 
subscriptions.isEmpty());
+                } else {
+                  Assert.assertEquals(subscriptions.toString(), 1, 
subscriptions.size());
+                  final Subscription subscription = 
subscriptions.iterator().next();
+                  Assert.assertEquals(consumerGroupId, 
subscription.getConsumerGroupId());
+                  final String consumerIds = subscription.getConsumerIds();
+                  Assert.assertTrue(
+                      subscription.toString(),
+                      consumerIds.startsWith("[") && 
consumerIds.endsWith("]"));
+                  final String idList = consumerIds.substring(1, 
consumerIds.length() - 1);
+                  Assert.assertEquals(
+                      subscription.toString(),
+                      new HashSet<>(Arrays.asList(expectedConsumerIds)),
+                      new HashSet<>(Arrays.asList(idList.split(",\\s*"))));
+                }
+              });
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 3cd7ae14585..a8896d1cf41 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2188,6 +2188,8 @@ public final class DataNodePipeMessages {
           + "frontier";
   public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_ERROR_DURING_DEREGISTER_34C332E7 =
       "ConsensusPrefetchingQueue {}: error during deregister";
+  public static final String 
LOG_FAILED_TO_FINISH_CLOSING_CONSENSUS_PREFETCHING_QUEUE_ARG_3C31731C =
+      "Failed to finish closing consensus prefetching queue {}";
   public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_FAILED_TO_FLUSH_LINGERING_BATCH_F97D8AA7 =
       "ConsensusPrefetchingQueue {}: failed to flush lingering batch during 
close, discarding it";
   public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_PREFETCH_ROUND_FAILED_TYPE_MESSAGE_63BC909B =
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 23f58a00c51..f9a4df76fb7 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2031,6 +2031,8 @@ public final class DataNodePipeMessages {
       "ConsensusPrefetchingQueue {}:不支持 seekAfterRegionProgress(没有 WAL 目录)";
   public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_FAILED_TO_READ_WAL_METADATA_FROM_A2ED50D1 =
       "ConsensusPrefetchingQueue {}:计算 seekToEnd frontier 时,从 {} 读取 WAL 
metadata 失败";
+  public static final String 
LOG_FAILED_TO_FINISH_CLOSING_CONSENSUS_PREFETCHING_QUEUE_ARG_3C31731C =
+      "未能完成共识预取队列 {} 的关闭";
   public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_ERROR_DURING_DEREGISTER_34C332E7 =
       "ConsensusPrefetchingQueue {}:注销期间出错";
   public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_FAILED_TO_FLUSH_LINGERING_BATCH_F97D8AA7 =
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 fd04724488c..2792dff39e2 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
@@ -743,7 +743,10 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
   }
 
   public void unbindConsensusPrefetchingQueue(final String topicName) {
-    closeAndRemoveConsensusPrefetchingQueues(topicName, true, true);
+    // Subscription setup teardown and rollback manage commit state after this 
call returns. Let
+    // queue close persist the current state, while avoiding an asynchronous 
close deleting state
+    // restored by a concurrent setup rollback.
+    closeAndRemoveConsensusPrefetchingQueues(topicName, true, false);
   }
 
   @Override
@@ -774,7 +777,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
     }
 
     for (int i = 0; i < queuesToClose.size(); i++) {
-      queuesToClose.get(i).close();
+      queuesToClose.get(i).closeAsync(false);
       LOGGER.info(
           DataNodePipeMessages
               
.PIPE_LOG_SUBSCRIPTION_CLOSED_CONSENSUS_PREFETCHING_QUEUE_FOR_TOPIC_3A9DDEC5,
@@ -874,7 +877,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
     }
 
     for (final ConsensusPrefetchingQueue q : queuesToClose) {
-      q.close(removeProgressAfterClose);
+      q.closeAsync(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 b8cd6b258d3..af3bf6344a0 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
@@ -222,6 +222,10 @@ public class ConsensusPrefetchingQueue {
    */
   private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
 
+  private final Object closeLifecycleLock = new Object();
+
+  private boolean closeInProgress;
+
   private volatile boolean isClosed = false;
 
   private volatile boolean closeRequested = false;
@@ -779,7 +783,7 @@ public class ConsensusPrefetchingQueue {
           && Objects.nonNull(staleExecutor)
           && (staleExecutor != currentExecutor || staleSubtask.isClosed())
           && !staleExecutor.isShutdown()) {
-        staleExecutor.deregister(staleSubtask.getTaskId());
+        staleExecutor.deregister(staleSubtask);
       }
 
       final ConsensusPrefetchSubtask newSubtask = new 
ConsensusPrefetchSubtask(this);
@@ -2315,6 +2319,9 @@ public class ConsensusPrefetchingQueue {
       final long commitLocalSeq,
       final long retainedBytes,
       final long expectedSeekGeneration) {
+    if (closeRequested || isClosed) {
+      return false;
+    }
     if (seekGeneration.get() != expectedSeekGeneration) {
       LOGGER.debug(
           DataNodePipeMessages
@@ -3965,28 +3972,75 @@ public class ConsensusPrefetchingQueue {
   }
 
   public void close(final boolean removeProgressAfterClose) {
-    final PendingSeekRequest seekRequestToFail;
-    final Pair<ConsensusSubscriptionPrefetchExecutor, 
ConsensusPrefetchSubtask> prefetchBinding;
+    if (!lock.writeLock().tryLock()) {
+      if (beginClose()) {
+        CLOSE_CLEANUP_EXECUTOR.execute(
+            () -> finishCloseAfterWriteLock(removeProgressAfterClose, true));
+      }
+      return;
+    }
 
-    acquireWriteLock();
+    final PendingSeekRequest seekRequestToFail;
     try {
-      if (closeRequested || (isClosed && !closeCleanupPending)) {
+      if (!beginClose()) {
         return;
       }
-      closeRequested = true;
-      closeCleanupPending = true;
       seekRequestToFail = pendingSeekRequest;
       pendingSeekRequest = null;
     } finally {
       releaseWriteLock();
     }
 
-    // Stop receiving real-time in-memory writes before flushing close-time 
batches. Requests that
-    // remain in pendingEntries have not advanced subscription progress and 
can be replayed from WAL
-    // if this queue is created again.
-    deregisterPendingEntriesFromConsensusServer();
+    try {
+      finishCloseRequest(seekRequestToFail, removeProgressAfterClose, false);
+    } catch (final Exception e) {
+      handleCloseFailure(e);
+    }
+  }
+
+  /** Starts teardown without waiting for an active prefetch round or 
consensus queue writes. */
+  public void closeAsync(final boolean removeProgressAfterClose) {
+    if (beginClose()) {
+      CLOSE_CLEANUP_EXECUTOR.execute(
+          () -> finishCloseAfterWriteLock(removeProgressAfterClose, true));
+    }
+  }
+
+  private boolean beginClose() {
+    synchronized (closeLifecycleLock) {
+      if (closeInProgress || closeRequested || (isClosed && 
!closeCleanupPending)) {
+        return false;
+      }
+      closeInProgress = true;
+      closeRequested = true;
+      closeCleanupPending = true;
+      return true;
+    }
+  }
+
+  private void finishCloseAfterWriteLock(
+      final boolean removeProgressAfterClose, final boolean backgroundCleanup) 
{
+    try {
+      final PendingSeekRequest seekRequestToFail;
+      acquireWriteLock();
+      try {
+        seekRequestToFail = pendingSeekRequest;
+        pendingSeekRequest = null;
+      } finally {
+        releaseWriteLock();
+      }
+      finishCloseRequest(seekRequestToFail, removeProgressAfterClose, 
backgroundCleanup);
+    } catch (final Exception e) {
+      handleCloseFailure(e);
+    }
+  }
 
-    prefetchBinding = detachPrefetchSubtask();
+  private void finishCloseRequest(
+      final PendingSeekRequest seekRequestToFail,
+      final boolean removeProgressAfterClose,
+      final boolean backgroundCleanup) {
+    final Pair<ConsensusSubscriptionPrefetchExecutor, 
ConsensusPrefetchSubtask> prefetchBinding =
+        detachPrefetchSubtask();
 
     if (Objects.nonNull(seekRequestToFail)) {
       seekRequestToFail.fail(
@@ -3999,14 +4053,21 @@ public class ConsensusPrefetchingQueue {
 
     if (Objects.nonNull(prefetchBinding.right)) {
       prefetchBinding.right.cancelPendingExecution();
-      if (prefetchBinding.right.isScheduledOrRunning()) {
-        CLOSE_CLEANUP_EXECUTOR.execute(
-            () -> finishCloseInBackground(prefetchBinding, 
removeProgressAfterClose));
-        return;
-      }
     }
 
-    finishClose(prefetchBinding, removeProgressAfterClose);
+    if (!backgroundCleanup
+        && Objects.nonNull(prefetchBinding.right)
+        && prefetchBinding.right.isScheduledOrRunning()) {
+      CLOSE_CLEANUP_EXECUTOR.execute(
+          () -> finishCloseInBackground(prefetchBinding, 
removeProgressAfterClose));
+      return;
+    }
+
+    if (backgroundCleanup) {
+      finishCloseInBackground(prefetchBinding, removeProgressAfterClose);
+    } else {
+      finishClose(prefetchBinding, removeProgressAfterClose);
+    }
   }
 
   private void finishCloseInBackground(
@@ -4015,24 +4076,38 @@ public class ConsensusPrefetchingQueue {
     try {
       finishClose(prefetchBinding, removeProgressAfterClose);
     } catch (final Exception e) {
-      LOGGER.warn("Failed to finish closing consensus prefetching queue {}", 
this, e);
+      handleCloseFailure(e);
     }
   }
 
+  private void handleCloseFailure(final Exception e) {
+    closeRequested = false;
+    synchronized (closeLifecycleLock) {
+      closeInProgress = false;
+    }
+    LOGGER.warn(
+        
DataNodePipeMessages.LOG_FAILED_TO_FINISH_CLOSING_CONSENSUS_PREFETCHING_QUEUE_ARG_3C31731C,
+        this,
+        e);
+  }
+
   private void finishClose(
       final Pair<ConsensusSubscriptionPrefetchExecutor, 
ConsensusPrefetchSubtask> prefetchBinding,
       final boolean removeProgressAfterClose) {
+    // Queue deregistration may wait for consensus-side WAL/index work. 
Metadata callers use
+    // closeAsync so this cleanup cannot hold up the consumer-group metadata 
push.
+    // Requests left in pendingEntries have not advanced subscription progress 
and can be replayed
+    // from WAL if this queue is created again.
+    deregisterPendingEntriesFromConsensusServer();
+
     if (Objects.nonNull(prefetchBinding.right)) {
       prefetchBinding.right.awaitIdle();
     }
     try {
       acquireWriteLock();
       try {
-        if (!isClosed
-            && pendingSeekRequest == null
-            && seekGeneration.get() == observedSeekGeneration) {
-          flushLingeringBatchOnCloseUnderWriteLock();
-        }
+        // Any uncommitted linger batch is discarded by cleanUp() and replayed 
from WAL if the
+        // topic is subscribed again. Emitting it during close could recreate 
removed commit state.
         markClosed();
       } finally {
         releaseWriteLock();
@@ -4043,7 +4118,7 @@ public class ConsensusPrefetchingQueue {
 
       if (Objects.nonNull(prefetchBinding.left) && 
Objects.nonNull(prefetchBinding.right)) {
         if (!prefetchBinding.left.isShutdown()) {
-          prefetchBinding.left.deregister(prefetchBinding.right.getTaskId());
+          prefetchBinding.left.deregister(prefetchBinding.right);
         } else {
           prefetchBinding.right.close();
         }
@@ -4062,6 +4137,9 @@ public class ConsensusPrefetchingQueue {
       closeCleanupPending = false;
     } finally {
       closeRequested = false;
+      synchronized (closeLifecycleLock) {
+        closeInProgress = false;
+      }
     }
   }
 
@@ -4076,25 +4154,6 @@ public class ConsensusPrefetchingQueue {
     }
   }
 
-  private void flushLingeringBatchOnCloseUnderWriteLock() {
-    if (lingerBatch.isEmpty()) {
-      return;
-    }
-    LOGGER.info(
-        DataNodePipeMessages
-            
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_FLUSHING_LINGERING_TABLETS_DURING_4C4AF235,
-        this,
-        lingerBatch.tablets.size());
-    if (!flushBatch(lingerBatch, observedSeekGeneration)) {
-      LOGGER.warn(
-          DataNodePipeMessages
-              
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_FAILED_TO_FLUSH_LINGERING_BATCH_F97D8AA7,
-          this);
-      discardBatch(lingerBatch);
-      resetBatchWriterProgress();
-    }
-  }
-
   private SubscriptionEvent generateErrorResponse(final String errorMessage) {
     return new SubscriptionEvent(
         SubscriptionPollResponseType.ERROR.getType(),
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/execution/ConsensusSubscriptionPrefetchExecutor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/execution/ConsensusSubscriptionPrefetchExecutor.java
index 01700febb45..d34773e5510 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/execution/ConsensusSubscriptionPrefetchExecutor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/execution/ConsensusSubscriptionPrefetchExecutor.java
@@ -104,6 +104,16 @@ public class ConsensusSubscriptionPrefetchExecutor {
     subtask.close();
   }
 
+  /** Removes a detached queue's task only if it still owns the registration. 
*/
+  public synchronized void deregister(final ConsensusPrefetchSubtask subtask) {
+    if (!taskIdToSubtask.remove(subtask.getTaskId(), subtask)) {
+      return;
+    }
+    readyQueue.remove(subtask);
+    subtask.cancelPendingExecution();
+    subtask.close();
+  }
+
   public void enqueue(final ConsensusPrefetchSubtask subtask) {
     if (shutdown.get() || subtask.isClosed()) {
       return;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
index e2de603eda1..703289b736b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
@@ -191,6 +191,7 @@ public class ConsensusPrefetchSubtask {
 
   public void cancelPendingExecution() {
     synchronized (monitor) {
+      closed = true;
       delayedWakeToken++;
       wakeupPending = false;
       if (scheduledOrRunning && !running) {
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 2b3dc9f0ef5..371bb149c4c 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
@@ -109,6 +109,28 @@ public class ConsensusPrefetchingQueueTest {
 
   @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
+  @Test
+  public void testLateOldPrefetchDeregistrationKeepsReplacementTask() {
+    final ConsensusSubscriptionPrefetchExecutor executor =
+        new ConsensusSubscriptionPrefetchExecutor();
+    try {
+      final ConsensusPrefetchSubtask oldSubtask = 
mock(ConsensusPrefetchSubtask.class);
+      final ConsensusPrefetchSubtask replacementSubtask = 
mock(ConsensusPrefetchSubtask.class);
+      when(oldSubtask.getTaskId()).thenReturn("same-task-id");
+      when(replacementSubtask.getTaskId()).thenReturn("same-task-id");
+
+      assertTrue(executor.register(oldSubtask));
+      executor.deregister(oldSubtask);
+      assertTrue(executor.register(replacementSubtask));
+
+      executor.deregister(oldSubtask);
+      verify(replacementSubtask, never()).close();
+      verify(replacementSubtask, never()).cancelPendingExecution();
+    } finally {
+      executor.shutdown();
+    }
+  }
+
   @Test
   public void testCloseDetachesBlockedPrefetchCleanup() throws Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
@@ -170,12 +192,152 @@ public class ConsensusPrefetchingQueueTest {
 
       releaseCleanup.countDown();
       verify(subtask, 
timeout(TimeUnit.SECONDS.toMillis(TEST_TIMEOUT_SECONDS))).close();
+      awaitCloseFinished(queue);
       assertTrue(queue.isClosed());
     } finally {
       releaseCleanup.countDown();
       if (queue != null && !queue.isClosed()) {
         queue.close();
       }
+      if (queue != null) {
+        awaitCloseFinished(queue);
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  @Test
+  public void testCloseDoesNotWaitForActivePrefetchReadLock() throws Exception 
{
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = 
temporaryFolder.newFolder("async-prefetch-read-lock");
+    final ExecutorService closeExecutor = Executors.newSingleThreadExecutor();
+    ConsensusPrefetchingQueue queue = null;
+    ReentrantReadWriteLock queueLock = 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 Field lockField = 
ConsensusPrefetchingQueue.class.getDeclaredField("lock");
+      lockField.setAccessible(true);
+      queueLock = (ReentrantReadWriteLock) lockField.get(queue);
+      queueLock.readLock().lock();
+
+      final ConsensusPrefetchingQueue queueToClose = queue;
+      final Future<?> closeFuture = closeExecutor.submit((Runnable) 
queueToClose::close);
+      closeFuture.get(1, TimeUnit.SECONDS);
+      assertFalse(queue.isClosed());
+    } finally {
+      if (queueLock != null && queueLock.getReadHoldCount() > 0) {
+        queueLock.readLock().unlock();
+      }
+      closeExecutor.shutdownNow();
+      closeExecutor.awaitTermination(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      if (queue != null && !queue.isClosed()) {
+        queue.close();
+      }
+      if (queue != null) {
+        awaitCloseFinished(queue);
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+    assertTrue(queue.isClosed());
+  }
+
+  @Test
+  public void testCloseDetachesBlockedConsensusQueueDeregistration() throws 
Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = 
temporaryFolder.newFolder("async-prefetch-deregister");
+    final CountDownLatch deregistrationStarted = new CountDownLatch(1);
+    final CountDownLatch releaseDeregistration = 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());
+      doAnswer(
+              invocation -> {
+                deregistrationStarted.countDown();
+                releaseDeregistration.await(TEST_TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+                return null;
+              })
+          .when(serverImpl)
+          .deregisterSubscriptionQueue(any());
+
+      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(false);
+      when(prefetchExecutor.isShutdown()).thenReturn(true);
+      setPrefetchBinding(queue, prefetchExecutor, subtask);
+
+      final long closeStartNanos = System.nanoTime();
+      queue.closeAsync(false);
+      final long closeElapsedMillis =
+          TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - closeStartNanos);
+
+      assertTrue(closeElapsedMillis < 1000L);
+      assertTrue(deregistrationStarted.await(TEST_TIMEOUT_SECONDS, 
TimeUnit.SECONDS));
+      assertFalse(queue.isClosed());
+
+      releaseDeregistration.countDown();
+      verify(subtask, 
timeout(TimeUnit.SECONDS.toMillis(TEST_TIMEOUT_SECONDS))).close();
+      awaitCloseFinished(queue);
+      assertTrue(queue.isClosed());
+    } finally {
+      releaseDeregistration.countDown();
+      if (queue != null && !queue.isClosed()) {
+        queue.close();
+      }
+      if (queue != null) {
+        awaitCloseFinished(queue);
+      }
       
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
     }
   }
@@ -2916,6 +3078,27 @@ public class ConsensusPrefetchingQueueTest {
     subtaskField.set(queue, subtask);
   }
 
+  private static void awaitCloseFinished(final ConsensusPrefetchingQueue 
queue) throws Exception {
+    final Field closeLifecycleLockField =
+        ConsensusPrefetchingQueue.class.getDeclaredField("closeLifecycleLock");
+    closeLifecycleLockField.setAccessible(true);
+    final Object closeLifecycleLock = closeLifecycleLockField.get(queue);
+
+    final Field closeInProgressField =
+        ConsensusPrefetchingQueue.class.getDeclaredField("closeInProgress");
+    closeInProgressField.setAccessible(true);
+    final long closeDeadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(TEST_TIMEOUT_SECONDS);
+    while (System.nanoTime() < closeDeadline) {
+      synchronized (closeLifecycleLock) {
+        if (!closeInProgressField.getBoolean(queue)) {
+          return;
+        }
+      }
+      Thread.sleep(10L);
+    }
+    throw new AssertionError("Timed out waiting for queue close cleanup");
+  }
+
   private static ProgressWALIterator subscriptionWalIterator(final 
ConsensusPrefetchingQueue queue)
       throws Exception {
     final Field field = 
ConsensusPrefetchingQueue.class.getDeclaredField("subscriptionWALIterator");

Reply via email to