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 495f04f7c17 Fix subscription provider recovery after broker loss 
(#19018)
495f04f7c17 is described below

commit 495f04f7c179089aa261d19143c8fdd5b0601e94
Author: Caideyipi <[email protected]>
AuthorDate: Mon Oct 26 19:30:49 2026 +0800

    Fix subscription provider recovery after broker loss (#19018)
    
    * Fix subscription provider recovery after broker loss
    
    * Preserve subscription state during provider recovery
    
    Close stale sessions without dropping subscriptions, keep provider failures 
terminal, ignore retired heartbeat results, and stop recovery after consumer 
close. Recognize file and tablet broker-missing poll diagnostics in both server 
locales and cover the recovery and commit paths with regression tests.
---
 .../base/AbstractSubscriptionConsumer.java         |   8 +
 .../base/AbstractSubscriptionProvider.java         |  55 +++-
 .../base/AbstractSubscriptionProviders.java        | 111 ++++---
 .../SubscriptionConsumerMultiProviderPollTest.java | 336 +++++++++++++++++++--
 .../SubscriptionProviderConnectionTimeoutTest.java | 130 ++++++++
 5 files changed, 571 insertions(+), 69 deletions(-)

diff --git 
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumer.java
 
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumer.java
index ea04b52109f..840fe5ac6ea 100644
--- 
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumer.java
+++ 
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumer.java
@@ -1453,6 +1453,7 @@ abstract class AbstractSubscriptionConsumer implements 
AutoCloseable {
         if (isClosed()) {
           return Collections.emptyList();
         }
+        providers.syncIfUnavailable(this);
         throw new SubscriptionConnectionException(
             String.format(
                 SubscriptionMessages
@@ -1464,6 +1465,7 @@ abstract class AbstractSubscriptionConsumer implements 
AutoCloseable {
       try {
         return provider.pollFile(commitContext, writingOffset, timeoutMs);
       } catch (final SubscriptionConnectionException ignored) {
+        providers.syncIfUnavailable(this);
         return Collections.emptyList();
       }
     } finally {
@@ -1482,6 +1484,7 @@ abstract class AbstractSubscriptionConsumer implements 
AutoCloseable {
         if (isClosed()) {
           return Collections.emptyList();
         }
+        providers.syncIfUnavailable(this);
         throw new SubscriptionConnectionException(
             String.format(
                 SubscriptionMessages
@@ -1493,6 +1496,7 @@ abstract class AbstractSubscriptionConsumer implements 
AutoCloseable {
       try {
         return provider.pollTablets(commitContext, offset, timeoutMs);
       } catch (final SubscriptionConnectionException ignored) {
+        providers.syncIfUnavailable(this);
         return Collections.emptyList();
       }
     } finally {
@@ -1684,6 +1688,7 @@ abstract class AbstractSubscriptionConsumer implements 
AutoCloseable {
         if (isClosed()) {
           return AbstractSubscriptionProvider.CommitResult.empty();
         }
+        providers.syncIfUnavailable(this);
         throw new SubscriptionConnectionException(
             String.format(
                 SubscriptionMessages
@@ -1697,6 +1702,9 @@ abstract class AbstractSubscriptionConsumer implements 
AutoCloseable {
       } catch (final SubscriptionConsumerFencedException e) {
         fence(e);
         throw e;
+      } catch (final SubscriptionConnectionException e) {
+        providers.syncIfUnavailable(this);
+        throw e;
       }
     } finally {
       providers.releaseReadLock();
diff --git 
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
 
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
index 0beabf8d614..56f73249103 100644
--- 
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
+++ 
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
@@ -78,6 +78,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.ReentrantLock;
 
 public abstract class AbstractSubscriptionProvider {
@@ -107,7 +108,15 @@ public abstract class AbstractSubscriptionProvider {
 
   private final AtomicBoolean isClosed = new AtomicBoolean(true);
   private final AtomicBoolean isClosing = new AtomicBoolean(false);
-  private final AtomicBoolean isAvailable = new AtomicBoolean(false);
+
+  private enum Availability {
+    NEW,
+    AVAILABLE,
+    UNAVAILABLE
+  }
+
+  private final AtomicReference<Availability> availability =
+      new AtomicReference<>(Availability.NEW);
   private final ReentrantLock rpcLock = new ReentrantLock();
 
   private final int localThriftMaxFrameSize;
@@ -243,15 +252,17 @@ public abstract class AbstractSubscriptionProvider {
   }
 
   boolean isAvailable() {
-    return isAvailable.get();
+    return availability.get() == Availability.AVAILABLE;
   }
 
   void setAvailable() {
-    isAvailable.set(true);
+    // A heartbeat that completes after a failed RPC must not revive this 
connection. Recovery
+    // constructs and handshakes a new provider instead.
+    availability.compareAndSet(Availability.NEW, Availability.AVAILABLE);
   }
 
   void setUnavailable() {
-    isAvailable.set(false);
+    availability.set(Availability.UNAVAILABLE);
   }
 
   int getDataNodeId() {
@@ -413,10 +424,17 @@ public abstract class AbstractSubscriptionProvider {
   }
 
   synchronized void closeSession() throws IoTDBConnectionException {
+    setUnavailable();
     try {
+      try {
+        // Recovery must preserve subscriptions and interrupt any heartbeat 
still using this
+        // session.
+        getSessionConnection().forceClose();
+      } catch (final IoTDBConnectionException ignored) {
+        // A failed handshake may not have opened a session.
+      }
       session.close();
     } finally {
-      setUnavailable();
       isClosed.set(true);
     }
   }
@@ -736,11 +754,34 @@ public abstract class AbstractSubscriptionProvider {
       setUnavailable();
       throw new SubscriptionConnectionException(e.getMessage(), e);
     }
-    verifyPipeSubscribeSuccess(resp.status);
+    try {
+      verifyPipeSubscribeSuccess(resp.status);
+    } catch (final SubscriptionRuntimeNonCriticalException e) {
+      // A missing broker is reported as a poll error instead of a transport 
failure. Treat it as a
+      // provider failure so the consumer can recreate this provider before 
the next poll.
+      if (isMissingBrokerPollError(resp.status)) {
+        setUnavailable();
+        throw new SubscriptionConnectionException(e.getMessage(), e);
+      }
+      throw e;
+    }
     final PipeSubscribePollResp pollResp = 
PipeSubscribePollResp.fromTPipeSubscribeResp(resp);
     return pollResp.getResponses();
   }
 
+  private boolean isMissingBrokerPollError(final TSStatus status) {
+    if (status.code != TSStatusCode.SUBSCRIPTION_POLL_ERROR.getStatusCode()
+        || status.message == null) {
+      return false;
+    }
+    // These are server protocol diagnostics. Match both server locales 
independently of the client
+    // locale, including the distinct messages returned by file and tablet 
continuation polls.
+    return status.message.contains("no broker bound to consumer group [" + 
consumerGroupId + "]")
+        || status.message.contains(
+            "broker bound to consumer group [" + consumerGroupId + "] does not 
exist")
+        || status.message.contains("绑定到消费者组 [" + consumerGroupId + "] 的 Broker 
不存在");
+  }
+
   CommitResult commit(
       final List<SubscriptionCommitContext> subscriptionCommitContexts, final 
boolean nack)
       throws SubscriptionException {
@@ -948,7 +989,7 @@ public abstract class AbstractSubscriptionProvider {
         + ", consumerGroupId="
         + consumerGroupId
         + ", isAvailable="
-        + isAvailable
+        + isAvailable()
         + ", isClosed="
         + isClosed
         + "}";
diff --git 
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProviders.java
 
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProviders.java
index 565c719149d..afb5285b5f5 100644
--- 
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProviders.java
+++ 
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProviders.java
@@ -90,11 +90,14 @@ final class AbstractSubscriptionProviders {
   /** Caller should ensure that the method is called in the lock {@link 
#acquireWriteLock()}. */
   void openProviders(final AbstractSubscriptionConsumer consumer) throws 
SubscriptionException {
     // close stale providers
-    closeProviders();
+    closeProviders(false);
 
     final Map<TEndPoint, String> connectionFailures = new LinkedHashMap<>();
     final List<Throwable> connectionFailureCauses = new ArrayList<>();
     for (final TEndPoint endPoint : initialEndpoints) {
+      if (isClosing.get() || consumer.isFenced()) {
+        return;
+      }
       final AbstractSubscriptionProvider defaultProvider;
       final int defaultDataNodeId;
 
@@ -116,6 +119,9 @@ final class AbstractSubscriptionProviders {
       }
       defaultDataNodeId = defaultProvider.getDataNodeId();
       addProvider(defaultDataNodeId, defaultProvider);
+      if (isClosing.get() || consumer.isFenced()) {
+        return;
+      }
 
       final Map<Integer, TEndPoint> allEndPoints;
       try {
@@ -134,6 +140,9 @@ final class AbstractSubscriptionProviders {
       }
 
       for (final Map.Entry<Integer, TEndPoint> entry : 
allEndPoints.entrySet()) {
+        if (isClosing.get() || consumer.isFenced()) {
+          return;
+        }
         if (defaultDataNodeId == entry.getKey()) {
           continue;
         }
@@ -243,7 +252,8 @@ final class AbstractSubscriptionProviders {
     }
     final AbstractSubscriptionProvider provider = 
subscriptionProviders.get(dataNodeId);
     try {
-      provider.close();
+      // Replacing a connection must not unsubscribe topics or drop the shared 
consumer.
+      provider.closeSession();
     } finally {
       LOGGER.info(SubscriptionMessages.CLOSE_STALE_PROVIDER, provider);
       subscriptionProviders.remove(dataNodeId);
@@ -338,13 +348,19 @@ final class AbstractSubscriptionProviders {
   private void heartbeatProvider(
       final AbstractSubscriptionConsumer consumer, final 
AbstractSubscriptionProvider provider) {
     try {
-      if (consumer.isClosed() || consumer.isFenced() || 
!containsProvider(provider)) {
+      if (consumer.isClosed()
+          || consumer.isFenced()
+          || !containsProvider(provider)
+          || !provider.isAvailable()) {
         return;
       }
       final List<SubscriptionCommitContext> processorBufferedCommitContexts =
           
consumer.getProcessorBufferedCommitContexts(provider.getDataNodeId());
       final PipeSubscribeHeartbeatResp resp = 
provider.heartbeat(processorBufferedCommitContexts);
-      if (consumer.isClosed() || consumer.isFenced() || 
!containsProvider(provider)) {
+      if (consumer.isClosed()
+          || consumer.isFenced()
+          || !containsProvider(provider)
+          || !provider.isAvailable()) {
         return;
       }
       provider.setAvailable();
@@ -360,7 +376,9 @@ final class AbstractSubscriptionProviders {
         }
       }
     } catch (final SubscriptionConsumerFencedException e) {
-      consumer.fence(e);
+      if (containsProvider(provider)) {
+        consumer.fence(e);
+      }
       provider.setUnavailable();
     } catch (final Exception e) {
       LOGGER.warn(
@@ -398,7 +416,7 @@ final class AbstractSubscriptionProviders {
     }
   }
 
-  /** Reconnects providers immediately after a failed RPC when every provider 
is unavailable. */
+  /** Reconnects unavailable providers immediately after a failed RPC. */
   void syncIfUnavailable(final AbstractSubscriptionConsumer consumer) {
     if (consumer.isClosed()
         || consumer.isFenced()
@@ -415,7 +433,7 @@ final class AbstractSubscriptionProviders {
                 }
                 acquireWriteLock();
                 try {
-                  if (consumer.isClosed() || consumer.isFenced() || 
!hasNoAvailableProviders()) {
+                  if (consumer.isClosed() || consumer.isFenced()) {
                     return;
                   }
                   final long now = System.currentTimeMillis();
@@ -436,7 +454,7 @@ final class AbstractSubscriptionProviders {
   }
 
   private void syncInternal(final AbstractSubscriptionConsumer consumer) {
-    if (consumer.isFenced()) {
+    if (consumer.isClosed() || consumer.isFenced()) {
       return;
     }
     if (hasNoAvailableProviders()) {
@@ -462,64 +480,71 @@ final class AbstractSubscriptionProviders {
       return;
     }
 
-    // add new providers or handshake existing providers
+    // Add new providers, replace unavailable providers, or heartbeat existing 
providers.
     for (final Map.Entry<Integer, TEndPoint> entry : allEndPoints.entrySet()) {
-      if (consumer.isFenced()) {
+      if (consumer.isClosed() || consumer.isFenced()) {
         return;
       }
-      final AbstractSubscriptionProvider provider = 
getProvider(entry.getKey());
-      if (Objects.isNull(provider)) {
-        // new provider
-        final TEndPoint endPoint = entry.getValue();
-        final AbstractSubscriptionProvider newProvider;
+      AbstractSubscriptionProvider provider = getProvider(entry.getKey());
+      if (Objects.nonNull(provider) && provider.isAvailable()) {
+        // Existing provider
         try {
-          newProvider = consumer.constructProviderAndHandshake(endPoint);
+          consumer.subscribedTopics = provider.heartbeat().getTopics();
+          provider.setAvailable();
         } catch (final SubscriptionConsumerFencedException e) {
           consumer.fence(e);
+          provider.setUnavailable();
           return;
         } catch (final Exception e) {
           LOGGER.warn(
-              
SubscriptionMessages.LOG_ARG_FAILED_CREATE_CONNECTION_ARG_BECAUSE_ARG_E536E22A,
+              SubscriptionMessages
+                  
.LOG_ARG_FAILED_SENDING_HEARTBEAT_SUBSCRIPTION_PROVIDER_ARG_BECAUSE_ARG_SET_0B38FB1F,
               consumer,
-              endPoint,
+              provider,
               e,
               e);
-          continue;
+          provider.setUnavailable();
         }
-        addProvider(entry.getKey(), newProvider);
-      } else {
-        // existing provider
+      }
+
+      if (Objects.nonNull(provider) && !provider.isAvailable()) {
+        // Remove unavailable providers before creating replacements in this 
same sync round.
         try {
-          consumer.subscribedTopics = provider.heartbeat().getTopics();
-          provider.setAvailable();
-        } catch (final SubscriptionConsumerFencedException e) {
-          consumer.fence(e);
-          provider.setUnavailable();
-          return;
+          closeAndRemoveProvider(entry.getKey());
         } catch (final Exception e) {
           LOGGER.warn(
               SubscriptionMessages
-                  
.LOG_ARG_FAILED_SENDING_HEARTBEAT_SUBSCRIPTION_PROVIDER_ARG_BECAUSE_ARG_SET_0B38FB1F,
+                  
.LOG_EXCEPTION_OCCURRED_ARG_CLOSING_REMOVING_SUBSCRIPTION_PROVIDER_ARG_BECAUSE_ARG_2EC38739,
               consumer,
               provider,
               e,
               e);
-          provider.setUnavailable();
         }
-        // close and remove unavailable provider (reset the connection as much 
as possible)
-        if (!provider.isAvailable()) {
-          try {
-            closeAndRemoveProvider(entry.getKey());
-          } catch (final Exception e) {
-            LOGGER.warn(
-                SubscriptionMessages
-                    
.LOG_EXCEPTION_OCCURRED_ARG_CLOSING_REMOVING_SUBSCRIPTION_PROVIDER_ARG_BECAUSE_ARG_2EC38739,
-                consumer,
-                provider,
-                e,
-                e);
-          }
+        provider = null;
+      }
+
+      if (Objects.isNull(provider)) {
+        // New or replacement provider
+        if (consumer.isClosed() || consumer.isFenced()) {
+          return;
         }
+        final TEndPoint endPoint = entry.getValue();
+        final AbstractSubscriptionProvider newProvider;
+        try {
+          newProvider = consumer.constructProviderAndHandshake(endPoint);
+        } catch (final SubscriptionConsumerFencedException e) {
+          consumer.fence(e);
+          return;
+        } catch (final Exception e) {
+          LOGGER.warn(
+              
SubscriptionMessages.LOG_ARG_FAILED_CREATE_CONNECTION_ARG_BECAUSE_ARG_E536E22A,
+              consumer,
+              endPoint,
+              e,
+              e);
+          continue;
+        }
+        addProvider(entry.getKey(), newProvider);
       }
     }
 
diff --git 
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerMultiProviderPollTest.java
 
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerMultiProviderPollTest.java
index 1d2ee7ae2c0..3ad5935b337 100644
--- 
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerMultiProviderPollTest.java
+++ 
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerMultiProviderPollTest.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.session.subscription.consumer.base;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.rpc.subscription.config.TopicConfig;
 import 
org.apache.iotdb.rpc.subscription.exception.SubscriptionConnectionException;
+import 
org.apache.iotdb.rpc.subscription.exception.SubscriptionConsumerFencedException;
 import org.apache.iotdb.rpc.subscription.exception.SubscriptionException;
 import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
 import org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponse;
@@ -40,6 +41,9 @@ import org.apache.tsfile.write.schema.MeasurementSchema;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -47,6 +51,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+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;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -134,11 +144,252 @@ public class SubscriptionConsumerMultiProviderPollTest {
     }
   }
 
+  @Test
+  public void testPollReconnectsWhenOneOfMultipleProvidersBecomesUnavailable()
+      throws SubscriptionException {
+    final TestPullConsumer consumer = new TestPullConsumer(1, 0, false, true, 
3);
+    try {
+      consumer.open();
+      consumer.subscribeTopic();
+
+      final List<SubscriptionMessage> messages = consumer.pollForTest(2_000L);
+
+      Assert.assertEquals(1, messages.size());
+      Assert.assertEquals(4, consumer.getCreatedProviderCount());
+      Assert.assertEquals(2, consumer.getPollCount(1));
+    } finally {
+      consumer.close();
+    }
+  }
+
+  @Test
+  public void 
testPartialRecoveryPreservesHealthyProvidersAndClosesOnlyFailedSession()
+      throws Exception {
+    final TestPullConsumer consumer = new TestPullConsumer(1, 0);
+    try {
+      consumer.open();
+      final AbstractSubscriptionProviders providers = getProviders(consumer);
+      final TestSubscriptionProvider failed = consumer.createdProviders.get(0);
+      final AbstractSubscriptionProvider healthy = providers.getProvider(2);
+      failed.setUnavailable();
+
+      providers.sync(consumer);
+
+      Assert.assertEquals(4, consumer.getCreatedProviderCount());
+      Assert.assertNotSame(failed, providers.getProvider(1));
+      Assert.assertSame(healthy, providers.getProvider(2));
+      Assert.assertEquals(1, failed.sessionCloseCount.get());
+      Assert.assertEquals(0, failed.closeCount.get());
+      Assert.assertTrue(providers.getProvider(1).isAvailable());
+    } finally {
+      consumer.close();
+    }
+  }
+
+  @Test
+  public void testAllUnavailableRecoveryClosesSessionsWithoutClosingConsumer() 
throws Exception {
+    final TestPullConsumer consumer = new TestPullConsumer(1, 0);
+    try {
+      consumer.open();
+      final List<TestSubscriptionProvider> failedProviders =
+          new ArrayList<>(consumer.createdProviders);
+      failedProviders.forEach(AbstractSubscriptionProvider::setUnavailable);
+
+      getProviders(consumer).sync(consumer);
+
+      Assert.assertEquals(6, consumer.getCreatedProviderCount());
+      for (final TestSubscriptionProvider failed : failedProviders) {
+        Assert.assertEquals(1, failed.sessionCloseCount.get());
+        Assert.assertEquals(0, failed.closeCount.get());
+      }
+    } finally {
+      consumer.close();
+    }
+  }
+
+  @Test
+  public void testLateHeartbeatCannotReviveFailedProvider() throws Exception {
+    assertLateHeartbeatIgnored(false);
+  }
+
+  @Test
+  public void testRetiredHeartbeatCannotFenceRecoveredConsumer() throws 
Exception {
+    assertLateHeartbeatIgnored(true);
+  }
+
+  private void assertLateHeartbeatIgnored(final boolean fenced) throws 
Exception {
+    final TestPullConsumer consumer = new TestPullConsumer(1, 0);
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final CountDownLatch heartbeatStarted = new CountDownLatch(1);
+    final CountDownLatch releaseHeartbeat = new CountDownLatch(1);
+    try {
+      consumer.open();
+      consumer.subscribeTopic();
+      final AbstractSubscriptionProviders providers = getProviders(consumer);
+      final TestSubscriptionProvider failed = consumer.createdProviders.get(0);
+      failed.emptyHeartbeatTopics = true;
+      failed.heartbeatAction =
+          () -> {
+            heartbeatStarted.countDown();
+            await(releaseHeartbeat);
+            if (fenced) {
+              throw new SubscriptionConsumerFencedException("retired 
connection fenced");
+            }
+          };
+      final Method heartbeat =
+          AbstractSubscriptionProviders.class.getDeclaredMethod(
+              "heartbeatProvider",
+              AbstractSubscriptionConsumer.class,
+              AbstractSubscriptionProvider.class);
+      heartbeat.setAccessible(true);
+      final Future<?> result = executor.submit(() -> 
heartbeat.invoke(providers, consumer, failed));
+      Assert.assertTrue(heartbeatStarted.await(5, TimeUnit.SECONDS));
+
+      failed.setUnavailable();
+      if (fenced) {
+        providers.sync(consumer);
+      }
+      releaseHeartbeat.countDown();
+      result.get(5, TimeUnit.SECONDS);
+
+      Assert.assertFalse(failed.isAvailable());
+      Assert.assertFalse(consumer.isFenced());
+      Assert.assertTrue(consumer.subscribedTopics.containsKey(TOPIC));
+      providers.sync(consumer);
+      Assert.assertNotSame(failed, providers.getProvider(1));
+    } finally {
+      releaseHeartbeat.countDown();
+      consumer.close();
+      executor.shutdownNow();
+      executor.awaitTermination(5, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testCloseDuringReplacementHandshakeDoesNotOpenMoreProviders() 
throws Exception {
+    assertCloseDuringReplacementHandshake(false);
+    assertCloseDuringReplacementHandshake(true);
+  }
+
+  private void assertCloseDuringReplacementHandshake(final boolean 
allUnavailable)
+      throws Exception {
+    final TestPullConsumer consumer = new TestPullConsumer(1, 0);
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final CountDownLatch handshakeStarted = new CountDownLatch(1);
+    final CountDownLatch releaseHandshake = new CountDownLatch(1);
+    try {
+      consumer.open();
+      final AbstractSubscriptionProviders providers = getProviders(consumer);
+      if (allUnavailable) {
+        
consumer.createdProviders.forEach(AbstractSubscriptionProvider::setUnavailable);
+      } else {
+        consumer.createdProviders.get(0).setUnavailable();
+      }
+      consumer.handshakeAction =
+          () -> {
+            handshakeStarted.countDown();
+            await(releaseHandshake);
+          };
+      final Future<?> sync = executor.submit(() -> providers.sync(consumer));
+      Assert.assertTrue(handshakeStarted.await(5, TimeUnit.SECONDS));
+
+      consumer.close();
+      releaseHandshake.countDown();
+      sync.get(5, TimeUnit.SECONDS);
+
+      Assert.assertTrue(consumer.isClosed());
+      Assert.assertTrue(providers.hasNoProviders());
+      Assert.assertEquals(4, consumer.getCreatedProviderCount());
+      Assert.assertEquals(1, 
consumer.createdProviders.get(3).closeCount.get());
+    } finally {
+      releaseHandshake.countDown();
+      consumer.close();
+      executor.shutdownNow();
+      executor.awaitTermination(5, TimeUnit.SECONDS);
+    }
+  }
+
+  @Test
+  public void testContinuationAndCommitFailuresSchedulePartialRecovery() 
throws Exception {
+    for (final String operation :
+        new String[] {"pollFileInternal", "pollTabletsInternal", 
"commitInternal"}) {
+      final TestPullConsumer consumer = new TestPullConsumer(1, 0);
+      try {
+        consumer.open();
+        final TestSubscriptionProvider failed = 
consumer.createdProviders.get(0);
+        failed.failContinuation = true;
+        final CountDownLatch replacementStarted = new CountDownLatch(1);
+        consumer.handshakeAction = replacementStarted::countDown;
+        final SubscriptionCommitContext context =
+            new SubscriptionCommitContext(1, 0, TOPIC, CONSUMER_GROUP_ID, 0L);
+        final Method method;
+        final Object[] arguments;
+        if ("commitInternal".equals(operation)) {
+          method =
+              AbstractSubscriptionConsumer.class.getDeclaredMethod(
+                  operation, int.class, List.class, boolean.class);
+          arguments = new Object[] {1, Collections.singletonList(context), 
false};
+        } else {
+          method =
+              AbstractSubscriptionConsumer.class.getDeclaredMethod(
+                  operation,
+                  SubscriptionCommitContext.class,
+                  "pollFileInternal".equals(operation) ? long.class : 
int.class,
+                  long.class);
+          arguments =
+              new Object[] {
+                context, "pollFileInternal".equals(operation) ? (Object) 0L : 
0, 1_000L
+              };
+        }
+        method.setAccessible(true);
+        try {
+          method.invoke(consumer, arguments);
+          Assert.assertNotEquals("commitInternal", operation);
+        } catch (final InvocationTargetException e) {
+          Assert.assertEquals("commitInternal", operation);
+          Assert.assertTrue(e.getCause() instanceof 
SubscriptionConnectionException);
+        }
+
+        Assert.assertTrue(replacementStarted.await(5, TimeUnit.SECONDS));
+        final AbstractSubscriptionProviders providers = getProviders(consumer);
+        providers.acquireReadLock();
+        try {
+          Assert.assertNotSame(failed, providers.getProvider(1));
+          Assert.assertEquals(3, providers.getAvailableProviderCount());
+          Assert.assertEquals("commitInternal".equals(operation) ? 1 : 0, 
failed.commitCount.get());
+          Assert.assertEquals(0, 
consumer.createdProviders.get(3).commitCount.get());
+        } finally {
+          providers.releaseReadLock();
+        }
+      } finally {
+        consumer.close();
+      }
+    }
+  }
+
+  private static AbstractSubscriptionProviders getProviders(
+      final AbstractSubscriptionConsumer consumer) throws Exception {
+    final Field field = 
AbstractSubscriptionConsumer.class.getDeclaredField("providers");
+    field.setAccessible(true);
+    return (AbstractSubscriptionProviders) field.get(consumer);
+  }
+
+  private static void await(final CountDownLatch latch) {
+    try {
+      Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
+    } catch (final InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new AssertionError(e);
+    }
+  }
+
   private static class TestPullConsumer extends 
AbstractSubscriptionPullConsumer {
 
     private final Map<Integer, Integer> pollCounts = new HashMap<>();
     private final List<SubscriptionCommitContext> acknowledgedCommitContexts = 
new ArrayList<>();
     private final AtomicInteger createdProviderCount = new AtomicInteger();
+    private final List<TestSubscriptionProvider> createdProviders = new 
CopyOnWriteArrayList<>();
+    private volatile Runnable handshakeAction = () -> {};
     private final AtomicBoolean failFirstPoll;
     private final int dataProviderId;
     private final int emptyPollsBeforeData;
@@ -191,24 +442,28 @@ public class SubscriptionConsumerMultiProviderPollTest {
         final long heartbeatIntervalMs,
         final int connectionTimeoutInMs) {
       createdProviderCount.incrementAndGet();
-      return new TestSubscriptionProvider(
-          endPoint,
-          username,
-          password,
-          encryptedPassword,
-          consumerId,
-          consumerGroupId,
-          ownerId,
-          ownerEpoch,
-          thriftMaxFrameSize,
-          heartbeatIntervalMs,
-          connectionTimeoutInMs,
-          pollCounts,
-          dataProviderId,
-          emptyPollsBeforeData,
-          emptyTabletBatch,
-          failFirstPoll,
-          providerCount);
+      final TestSubscriptionProvider provider =
+          new TestSubscriptionProvider(
+              endPoint,
+              username,
+              password,
+              encryptedPassword,
+              consumerId,
+              consumerGroupId,
+              ownerId,
+              ownerEpoch,
+              thriftMaxFrameSize,
+              heartbeatIntervalMs,
+              connectionTimeoutInMs,
+              pollCounts,
+              dataProviderId,
+              emptyPollsBeforeData,
+              emptyTabletBatch,
+              failFirstPoll,
+              providerCount);
+      provider.handshakeAction = () -> handshakeAction.run();
+      createdProviders.add(provider);
+      return provider;
     }
 
     private void subscribeTopic() {
@@ -251,6 +506,14 @@ public class SubscriptionConsumerMultiProviderPollTest {
 
   private static class TestSubscriptionProvider extends 
AbstractSubscriptionProvider {
 
+    private final AtomicInteger closeCount = new AtomicInteger();
+    private final AtomicInteger sessionCloseCount = new AtomicInteger();
+    private final AtomicInteger commitCount = new AtomicInteger();
+    private volatile Runnable heartbeatAction = () -> {};
+    private boolean emptyHeartbeatTopics;
+    private Runnable handshakeAction = () -> {};
+    private boolean failContinuation;
+
     private final int dataNodeId;
     private final Map<Integer, Integer> pollCounts;
     private final int dataProviderId;
@@ -320,11 +583,19 @@ public class SubscriptionConsumerMultiProviderPollTest {
 
     @Override
     synchronized void handshake() {
+      handshakeAction.run();
       setAvailable();
     }
 
     @Override
     synchronized void close() {
+      closeCount.incrementAndGet();
+      setUnavailable();
+    }
+
+    @Override
+    synchronized void closeSession() {
+      sessionCloseCount.incrementAndGet();
       setUnavailable();
     }
 
@@ -336,8 +607,11 @@ public class SubscriptionConsumerMultiProviderPollTest {
     @Override
     PipeSubscribeHeartbeatResp heartbeat(
         final List<SubscriptionCommitContext> processorBufferedCommitContexts) 
{
+      heartbeatAction.run();
       final PipeSubscribeHeartbeatResp response = new 
PipeSubscribeHeartbeatResp();
-      response.getTopics().put(TOPIC, new TopicConfig());
+      if (!emptyHeartbeatTopics) {
+        response.getTopics().put(TOPIC, new TopicConfig());
+      }
       for (int id = 1; id <= providerCount; id++) {
         response.getEndPoints().put(id, new TEndPoint(HOST, FIRST_PORT + id - 
1));
       }
@@ -389,6 +663,7 @@ public class SubscriptionConsumerMultiProviderPollTest {
     List<SubscriptionPollResponse> pollTablets(
         final SubscriptionCommitContext commitContext, final int offset, final 
long timeoutMs)
         throws SubscriptionException {
+      failContinuationIfRequested();
       if (emptyTabletBatch && dataNodeId == dataProviderId && offset == 1) {
         return Collections.singletonList(
             new SubscriptionPollResponse(
@@ -398,5 +673,28 @@ public class SubscriptionConsumerMultiProviderPollTest {
       }
       return Collections.emptyList();
     }
+
+    @Override
+    List<SubscriptionPollResponse> pollFile(
+        final SubscriptionCommitContext commitContext,
+        final long writingOffset,
+        final long timeoutMs) {
+      failContinuationIfRequested();
+      return Collections.emptyList();
+    }
+
+    @Override
+    CommitResult commit(final List<SubscriptionCommitContext> contexts, final 
boolean nack) {
+      commitCount.incrementAndGet();
+      failContinuationIfRequested();
+      return new CommitResult(contexts, Collections.emptyMap());
+    }
+
+    private void failContinuationIfRequested() {
+      if (failContinuation) {
+        setUnavailable();
+        throw new SubscriptionConnectionException("transport failed");
+      }
+    }
   }
 }
diff --git 
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderConnectionTimeoutTest.java
 
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderConnectionTimeoutTest.java
index ab7a741b07d..46dbd1f8e9f 100644
--- 
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderConnectionTimeoutTest.java
+++ 
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderConnectionTimeoutTest.java
@@ -22,10 +22,13 @@ import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.rpc.TSStatusCode;
 import 
org.apache.iotdb.rpc.subscription.exception.SubscriptionConnectionException;
+import 
org.apache.iotdb.rpc.subscription.exception.SubscriptionRuntimeNonCriticalException;
 import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
+import 
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestType;
 import 
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeHandshakeResp;
 import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
 import org.apache.iotdb.service.rpc.thrift.ServerProperties;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
 import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeResp;
 import org.apache.iotdb.service.rpc.thrift.TSOpenSessionResp;
 import org.apache.iotdb.service.rpc.thrift.TSProtocolVersion;
@@ -128,6 +131,133 @@ public class SubscriptionProviderConnectionTimeoutTest {
     }
   }
 
+  @Test
+  public void testMissingBrokerPollErrorMarksProviderUnavailable() throws 
Exception {
+    assertPollError(
+        "Subscription: no broker bound to consumer group [group]", true /* 
broker missing */);
+  }
+
+  @Test
+  public void testMissingFileBrokerPollErrorMarksProviderUnavailable() throws 
Exception {
+    assertPollError(
+        "Subscription: pipe broker bound to consumer group [group] does not 
exist", true);
+  }
+
+  @Test
+  public void testMissingTabletBrokerPollErrorMarksProviderUnavailable() 
throws Exception {
+    assertPollError("Subscription: broker bound to consumer group [group] does 
not exist", true);
+  }
+
+  @Test
+  public void 
testMissingTabletBrokerPollErrorFromChineseServerMarksProviderUnavailable()
+      throws Exception {
+    assertPollError("订阅:绑定到消费者组 [group] 的 Broker 不存在", true);
+  }
+
+  @Test
+  public void testOtherPollErrorRemainsNonCritical() throws Exception {
+    assertPollError("other poll error", false /* broker missing */);
+  }
+
+  @Test
+  public void testNullPollErrorRemainsNonCritical() throws Exception {
+    assertPollError(null, false /* broker missing */);
+  }
+
+  private void assertPollError(final String message, final boolean 
brokerMissing) throws Exception {
+    final AtomicInteger subscriptionCloseCount = new AtomicInteger();
+    final AtomicInteger sessionCloseCount = new AtomicInteger();
+    final ServerSocket socket = new ServerSocket(0, 50, 
InetAddress.getByName("127.0.0.1"));
+    final TServerSocket serverSocket = new TServerSocket(socket);
+    final IClientRPCService.Iface service =
+        (IClientRPCService.Iface)
+            Proxy.newProxyInstance(
+                IClientRPCService.Iface.class.getClassLoader(),
+                new Class<?>[] {IClientRPCService.Iface.class},
+                (proxy, method, args) -> {
+                  switch (method.getName()) {
+                    case "openSession":
+                      return new TSOpenSessionResp(
+                              successStatus(), 
TSProtocolVersion.IOTDB_SERVICE_PROTOCOL_V3)
+                          .setSessionId(1L);
+                    case "requestStatementId":
+                      return 1L;
+                    case "getProperties":
+                      return new ServerProperties(
+                          "test", Arrays.asList("max_time", "min_time"), "ms", 
1);
+                    case "pipeSubscribe":
+                      final TPipeSubscribeReq request = (TPipeSubscribeReq) 
args[0];
+                      if (request.getType() == 
PipeSubscribeRequestType.HANDSHAKE.getType()) {
+                        return PipeSubscribeHandshakeResp.toTPipeSubscribeResp(
+                            successStatus(), 1, "consumer", "group");
+                      }
+                      if (request.getType() != 
PipeSubscribeRequestType.POLL.getType()) {
+                        if (request.getType() == 
PipeSubscribeRequestType.CLOSE.getType()) {
+                          subscriptionCloseCount.incrementAndGet();
+                        }
+                        return new TPipeSubscribeResp(successStatus(), (byte) 
1, (short) 6);
+                      }
+                      return new TPipeSubscribeResp(
+                          new 
TSStatus(TSStatusCode.SUBSCRIPTION_POLL_ERROR.getStatusCode())
+                              .setMessage(message),
+                          (byte) 1,
+                          (short) 6);
+                    case "closeSession":
+                      sessionCloseCount.incrementAndGet();
+                      return successStatus();
+                    default:
+                      return defaultValue(method.getReturnType());
+                  }
+                });
+    final TSimpleServer server =
+        new TSimpleServer(
+            new TSimpleServer.Args(serverSocket)
+                .processor(new IClientRPCService.Processor<>(service))
+                .transportFactory(new 
org.apache.iotdb.rpc.TElasticFramedTransport.Factory()));
+    final Thread serverThread =
+        new Thread(server::serve, "subscription-provider-poll-error-server");
+    serverThread.setDaemon(true);
+    serverThread.start();
+
+    final TestSubscriptionProvider provider = new 
TestSubscriptionProvider(socket.getLocalPort());
+    try {
+      provider.handshake();
+      final TTransport transport = 
getTransport(provider.getSessionConnection());
+      final SubscriptionCommitContext context =
+          new SubscriptionCommitContext(1, 0, "topic", "group", 1L);
+
+      if (brokerMissing) {
+        Assert.assertThrows(
+            SubscriptionConnectionException.class,
+            () -> provider.poll(Collections.singleton("topic"), 1_000L));
+        Assert.assertFalse(provider.isAvailable());
+        Assert.assertThrows(
+            SubscriptionConnectionException.class, () -> 
provider.pollFile(context, 0L, 1_000L));
+        Assert.assertThrows(
+            SubscriptionConnectionException.class, () -> 
provider.pollTablets(context, 0, 1_000L));
+        provider.closeSession();
+        Assert.assertFalse(transport.isOpen());
+        Assert.assertEquals(0, subscriptionCloseCount.get());
+        Assert.assertEquals(0, sessionCloseCount.get());
+      } else {
+        Assert.assertThrows(
+            SubscriptionRuntimeNonCriticalException.class,
+            () -> provider.poll(Collections.singleton("topic"), 1_000L));
+        Assert.assertTrue(provider.isAvailable());
+        Assert.assertThrows(
+            SubscriptionRuntimeNonCriticalException.class,
+            () -> provider.pollFile(context, 0L, 1_000L));
+        Assert.assertThrows(
+            SubscriptionRuntimeNonCriticalException.class,
+            () -> provider.pollTablets(context, 0, 1_000L));
+      }
+    } finally {
+      provider.close();
+      server.stop();
+      serverThread.join(2_000);
+    }
+  }
+
   private static TTransport getTransport(final SubscriptionSessionConnection 
connection)
       throws Exception {
     final Field field = SessionConnection.class.getDeclaredField("transport");

Reply via email to