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 6709dfa9494 Fix concurrent subscription consumer handshakes (#18654)
6709dfa9494 is described below
commit 6709dfa9494cb7517ad8b940aa301fe93a03b43b
Author: Caideyipi <[email protected]>
AuthorDate: Fri Sep 18 17:37:37 2026 +0800
Fix concurrent subscription consumer handshakes (#18654)
* Fix concurrent subscription consumer handshakes
* Make consumer fencing consistent across DataNodes
---
.../java/org/apache/iotdb/rpc/TSStatusCode.java | 1 +
.../rpc/subscription/config/ConsumerConfig.java | 4 +
.../rpc/subscription/config/ConsumerConstant.java | 1 +
.../SubscriptionConsumerFencedException.java | 51 +++
.../base/AbstractSubscriptionConsumer.java | 200 +++++++++---
.../base/AbstractSubscriptionProvider.java | 21 ++
.../base/AbstractSubscriptionProviders.java | 55 +++-
.../base/AbstractSubscriptionPullConsumer.java | 14 +-
.../base/AbstractSubscriptionPushConsumer.java | 4 +-
.../base/SubscriptionConsumerLifecycleTest.java | 352 +++++++++++++++++---
.../base/SubscriptionProviderStatusTest.java | 72 ++++
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 6 +
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 6 +
.../agent/SubscriptionReceiverAgent.java | 86 ++++-
.../receiver/SubscriptionReceiver.java | 8 +
.../receiver/SubscriptionReceiverV1.java | 42 ++-
.../agent/SubscriptionReceiverAgentTest.java | 361 ++++++++++++++++++++-
.../receiver/SubscriptionReceiverV1Test.java | 41 +++
18 files changed, 1197 insertions(+), 128 deletions(-)
diff --git
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TSStatusCode.java
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TSStatusCode.java
index 61431249eb3..fce965fe27b 100644
---
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TSStatusCode.java
+++
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TSStatusCode.java
@@ -332,6 +332,7 @@ public enum TSStatusCode {
SUBSCRIPTION_OWNER_EPOCH_REQUIRED(1916),
SUBSCRIPTION_OWNER_LEASE_EXPIRED(1917),
SUBSCRIPTION_OWNER_EPOCH_CONFLICT(1918),
+ SUBSCRIPTION_CONSUMER_FENCED(1919),
// Topic
CREATE_TOPIC_ERROR(2000),
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConfig.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConfig.java
index 13f2a9ee3fb..47926b5fc91 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConfig.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConfig.java
@@ -68,6 +68,10 @@ public class ConsumerConfig extends PipeParameters {
return getString(ConsumerConstant.CONSUMER_GROUP_ID_KEY);
}
+ public String getConsumerInstanceId() {
+ return getString(ConsumerConstant.CONSUMER_INSTANCE_ID_KEY);
+ }
+
public String getOwnerId() {
return getString(ConsumerConstant.OWNER_ID_KEY);
}
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConstant.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConstant.java
index 3df95facf36..89878efb49a 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConstant.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/ConsumerConstant.java
@@ -40,6 +40,7 @@ public class ConsumerConstant {
public static final String CONSUMER_ID_KEY = "consumer-id";
public static final String CONSUMER_GROUP_ID_KEY = "group-id";
+ public static final String CONSUMER_INSTANCE_ID_KEY = "consumer-instance-id";
public static final String OWNER_ID_KEY = "owner-id";
public static final String OWNER_EPOCH_KEY = "owner-epoch";
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionConsumerFencedException.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionConsumerFencedException.java
new file mode 100644
index 00000000000..ef8e83b2c7c
--- /dev/null
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionConsumerFencedException.java
@@ -0,0 +1,51 @@
+/*
+ * 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.rpc.subscription.exception;
+
+import java.util.Objects;
+
+/**
+ * Indicates that another connection has taken over the same subscription
consumer identity. The
+ * fenced consumer instance can no longer issue requests and must not reclaim
ownership
+ * automatically.
+ */
+public class SubscriptionConsumerFencedException extends
SubscriptionRuntimeCriticalException {
+
+ public SubscriptionConsumerFencedException(final String message) {
+ super(message);
+ }
+
+ public SubscriptionConsumerFencedException(final String message, final
Throwable cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return obj instanceof SubscriptionConsumerFencedException
+ && Objects.equals(getMessage(), ((SubscriptionConsumerFencedException)
obj).getMessage())
+ && Objects.equals(
+ getTimeStamp(), ((SubscriptionConsumerFencedException)
obj).getTimeStamp());
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+}
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 8a2c9ff6334..a4f5d36a324 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
@@ -24,6 +24,7 @@ import org.apache.iotdb.isession.SessionConfig;
import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
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.exception.SubscriptionOwnerFencedException;
import
org.apache.iotdb.rpc.subscription.exception.SubscriptionPipeTimeoutException;
@@ -90,6 +91,8 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
@@ -107,6 +110,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
private static final long SLEEP_MS = 100L;
private static final long SLEEP_DELTA_MS = 50L;
private static final long TIMER_DELTA_MS = 250L;
+ private static final AtomicLong LAST_CONSUMER_INSTANCE_EPOCH = new
AtomicLong();
private final String username;
private final String password;
@@ -116,6 +120,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
protected String consumerGroupId;
protected String ownerId;
protected Long ownerEpoch;
+ private final String consumerInstanceId = generateConsumerInstanceId();
private final long heartbeatIntervalMs;
private final long endpointsSyncIntervalMs;
@@ -123,6 +128,8 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
private final AbstractSubscriptionProviders providers;
private final AtomicBoolean isClosed = new AtomicBoolean(true);
+ private final AtomicReference<SubscriptionConsumerFencedException>
fencedException =
+ new AtomicReference<>();
// This variable indicates whether the consumer has ever been closed.
private final AtomicBoolean isReleased = new AtomicBoolean(false);
@@ -193,6 +200,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
return ownerEpoch;
}
+ String getConsumerInstanceId() {
+ return consumerInstanceId;
+ }
+
/////////////////////////////// ctor ///////////////////////////////
protected AbstractSubscriptionConsumer(final
AbstractSubscriptionConsumerBuilder builder) {
@@ -296,6 +307,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
/////////////////////////////// open & close ///////////////////////////////
private void checkIfHasBeenClosed() throws SubscriptionException {
+ checkIfFenced();
if (isReleased.get()) {
final String errorMessage =
String.format("%s has ever been closed, unsupported operation after
closing.", this);
@@ -305,6 +317,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
}
private void checkIfOpened() throws SubscriptionException {
+ checkIfFenced();
if (isClosed.get()) {
final String errorMessage =
String.format("%s is not yet open, please open the subscription
consumer first.", this);
@@ -324,6 +337,9 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
providers.acquireWriteLock();
try {
providers.openProviders(this); // throw SubscriptionException
+ } catch (final SubscriptionException e) {
+ providers.closeProviders(!isFenced());
+ throw e;
} finally {
providers.releaseWriteLock();
}
@@ -354,13 +370,28 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
// Do not wait for the providers write lock here. A poll or heartbeat may
hold it while blocked
// in network I/O. prepareClose() bounds or interrupts those RPCs before
providers are detached.
providers.prepareClose();
- providers.closeProviders();
+ providers.closeProviders(!isFenced());
}
boolean isClosed() {
return isClosed.get();
}
+ boolean isFenced() {
+ return fencedException.get() != null;
+ }
+
+ void fence(final SubscriptionConsumerFencedException e) {
+ fencedException.compareAndSet(null, e);
+ }
+
+ void checkIfFenced() {
+ final SubscriptionConsumerFencedException e = fencedException.get();
+ if (e != null) {
+ throw e;
+ }
+ }
+
/////////////////////////////// subscribe & unsubscribe
///////////////////////////////
protected void subscribe(final String topicName) throws
SubscriptionException {
@@ -557,13 +588,18 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
this.thriftMaxFrameSize,
this.heartbeatIntervalMs,
this.connectionTimeoutInMs);
+ provider.setConsumerInstanceId(consumerInstanceId);
try {
provider.handshake();
} catch (final Exception e) {
try {
- provider.close();
+ provider.closeSession();
} catch (final Exception ignored) {
}
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
throw new SubscriptionConnectionException(
String.format(
SubscriptionMessages
@@ -598,6 +634,13 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
return message;
}
+ private static String generateConsumerInstanceId() {
+ final long epoch =
+ LAST_CONSUMER_INSTANCE_EPOCH.updateAndGet(
+ previous -> Math.max(System.currentTimeMillis(), previous + 1));
+ return String.format("%016x-%s", epoch,
RandomStringGenerator.generate(16));
+ }
+
/////////////////////////////// file ops ///////////////////////////////
private Path getFileDir(final String topicName) throws IOException {
@@ -734,6 +777,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
protected List<SubscriptionMessage> multiplePoll(
/* @NotNull */ final Set<String> topicNames, final long timeoutMs) {
+ checkIfFenced();
if (topicNames.isEmpty()) {
return Collections.emptyList();
}
@@ -754,42 +798,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
tasks.add(new PollTask(partition, timeoutMs));
}
- // submit multiple tasks to poll messages
- final List<SubscriptionMessage> messages = new ArrayList<>();
- SubscriptionRuntimeCriticalException
lastSubscriptionRuntimeCriticalException = null;
try {
// strict timeout
- for (final Future<List<SubscriptionMessage>> future :
- SubscriptionExecutorServiceManager.submitMultiplePollTasks(tasks,
timeoutMs)) {
- try {
- if (future.isCancelled()) {
- continue;
- }
- messages.addAll(future.get());
- } catch (final CancellationException ignored) {
-
- } catch (final ExecutionException e) {
- final Throwable cause = e.getCause();
- if (cause instanceof SubscriptionRuntimeCriticalException) {
- final SubscriptionRuntimeCriticalException ex =
- (SubscriptionRuntimeCriticalException) cause;
- LOGGER.warn(
- SubscriptionMessages
-
.LOG_SUBSCRIPTIONRUNTIMECRITICALEXCEPTION_OCCURRED_SUBSCRIPTIONCONSUMER_ARG_POLLING_TOPICS_ARG_C96324AD,
- this,
- topicNames,
- ex);
- lastSubscriptionRuntimeCriticalException = ex;
- } else {
- LOGGER.warn(
- SubscriptionMessages
-
.LOG_EXECUTIONEXCEPTION_OCCURRED_SUBSCRIPTIONCONSUMER_ARG_POLLING_TOPICS_ARG_40F5E1CC,
- this,
- topicNames,
- e);
- }
- }
- }
+ return collectMultiplePollResults(
+ SubscriptionExecutorServiceManager.submitMultiplePollTasks(tasks,
timeoutMs), topicNames);
} catch (final InterruptedException e) {
LOGGER.warn(
SubscriptionMessages
@@ -802,6 +814,56 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
// TODO: ignore possible interrupted state?
+ return Collections.emptyList();
+ }
+
+ List<SubscriptionMessage> collectMultiplePollResults(
+ final List<Future<List<SubscriptionMessage>>> futures, final Set<String>
topicNames)
+ throws InterruptedException {
+ final List<SubscriptionMessage> messages = new ArrayList<>();
+ SubscriptionRuntimeCriticalException
lastSubscriptionRuntimeCriticalException = null;
+ for (final Future<List<SubscriptionMessage>> future : futures) {
+ try {
+ if (future.isCancelled()) {
+ continue;
+ }
+ messages.addAll(future.get());
+ } catch (final CancellationException ignored) {
+
+ } catch (final ExecutionException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof SubscriptionConsumerFencedException) {
+ final SubscriptionConsumerFencedException fencedException =
+ (SubscriptionConsumerFencedException) cause;
+ fence(fencedException);
+ throw fencedException;
+ }
+ if (cause instanceof SubscriptionRuntimeCriticalException) {
+ final SubscriptionRuntimeCriticalException ex =
+ (SubscriptionRuntimeCriticalException) cause;
+ LOGGER.warn(
+ SubscriptionMessages
+
.LOG_SUBSCRIPTIONRUNTIMECRITICALEXCEPTION_OCCURRED_SUBSCRIPTIONCONSUMER_ARG_POLLING_TOPICS_ARG_C96324AD,
+ this,
+ topicNames,
+ ex);
+ lastSubscriptionRuntimeCriticalException = ex;
+ } else {
+ LOGGER.warn(
+ SubscriptionMessages
+
.LOG_EXECUTIONEXCEPTION_OCCURRED_SUBSCRIPTIONCONSUMER_ARG_POLLING_TOPICS_ARG_40F5E1CC,
+ this,
+ topicNames,
+ e);
+ }
+ }
+ }
+
+ // A timed-out task can be cancelled after fencing the consumer but before
its exception is
+ // observable through Future#get. Never deliver messages collected by
sibling tasks in that
+ // case.
+ checkIfFenced();
+
// even if a SubscriptionRuntimeCriticalException is encountered, try to
deliver the message to
// the client
if (messages.isEmpty() &&
Objects.nonNull(lastSubscriptionRuntimeCriticalException)) {
@@ -830,6 +892,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
private List<SubscriptionMessage> singlePoll(
/* @NotNull */ final Set<String> topicNames, final long timeoutMs)
throws SubscriptionException {
+ checkIfFenced();
if (topicNames.isEmpty()) {
return Collections.emptyList();
}
@@ -876,6 +939,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
}
}
} catch (final SubscriptionRuntimeCriticalException e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw e;
+ }
LOGGER.warn(
SubscriptionMessages
.LOG_SUBSCRIPTIONRUNTIMECRITICALEXCEPTION_OCCURRED_SUBSCRIPTIONCONSUMER_ARG_POLLING_TOPICS_ARG_C96324AD,
@@ -974,6 +1041,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
try (final RandomAccessFile fileWriter = new RandomAccessFile(file, "rw"))
{
return pollFileInternal(commitContext, fileName, file, fileWriter,
timer);
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
if (!(e instanceof SubscriptionPollTimeoutException)) {
inFlightFilesCommitContextSet.remove(commitContext);
}
@@ -1184,6 +1255,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
try {
return pollTabletsInternal(response, timer);
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
// construct temporary message to nack
nack(
Collections.singletonList(
@@ -1323,6 +1398,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
private List<SubscriptionPollResponse> pollInternal(
final Set<String> topicNames, final long timeoutMs) throws
SubscriptionException {
+ checkIfFenced();
providers.acquireReadLock();
try {
final AbstractSubscriptionProvider provider =
providers.getNextAvailableProvider();
@@ -1428,6 +1504,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
private void commit(final Iterable<SubscriptionCommitContext>
commitContexts, final boolean nack)
throws SubscriptionException {
+ checkIfFenced();
final Map<Integer, List<SubscriptionCommitContext>>
dataNodeIdToSubscriptionCommitContexts =
new HashMap<>();
for (final SubscriptionCommitContext commitContext : commitContexts) {
@@ -1590,7 +1667,12 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
nack,
dataNodeId));
}
- return provider.commit(subscriptionCommitContexts, nack);
+ try {
+ return provider.commit(subscriptionCommitContexts, nack);
+ } catch (final SubscriptionConsumerFencedException e) {
+ fence(e);
+ throw e;
+ }
} finally {
providers.releaseReadLock();
}
@@ -1607,7 +1689,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
future[0] =
SubscriptionExecutorServiceManager.submitHeartbeatWorker(
() -> {
- if (isClosed()) {
+ if (isClosed() || isFenced()) {
if (Objects.nonNull(future[0])) {
future[0].cancel(false);
LOGGER.info(SubscriptionMessages.CONSUMER_CANCEL_HEARTBEAT_WORKER, this);
@@ -1627,7 +1709,7 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
future[0] =
SubscriptionExecutorServiceManager.submitEndpointsSyncer(
() -> {
- if (isClosed()) {
+ if (isClosed() || isFenced()) {
if (Objects.nonNull(future[0])) {
future[0].cancel(false);
LOGGER.info(SubscriptionMessages.CONSUMER_CANCEL_ENDPOINTS_SYNCER, this);
@@ -1661,11 +1743,11 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
@Override
public void run() {
- if (isClosed()) {
- return;
- }
-
try {
+ checkIfFenced();
+ if (isClosed()) {
+ return;
+ }
ack(messages);
callback.onComplete();
} catch (final Exception e) {
@@ -1678,11 +1760,11 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
final CompletableFuture<Void> future = new CompletableFuture<>();
SubscriptionExecutorServiceManager.submitAsyncCommitWorker(
() -> {
- if (isClosed()) {
- return;
- }
-
try {
+ checkIfFenced();
+ if (isClosed()) {
+ return;
+ }
ack(messages);
future.complete(null);
} catch (final Throwable e) {
@@ -1709,6 +1791,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
subscribedTopics = provider.subscribe(topicNames);
return;
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
if (e instanceof SubscriptionOwnerFencedException) {
throw (SubscriptionOwnerFencedException) e;
}
@@ -1750,6 +1836,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
subscribedTopics = provider.unsubscribe(topicNames);
return;
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
if (e instanceof SubscriptionPipeTimeoutException) {
// degrade exception to log for pipe timeout
LOGGER.warn(e.getMessage());
@@ -1795,6 +1885,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
try {
provider.seek(topicName, seekType, timestamp);
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
failedProviders.add(provider);
if (Objects.isNull(firstFailure)) {
firstFailure = e;
@@ -1836,6 +1930,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
try {
provider.seekToTopicProgress(topicName, topicProgress);
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
failedProviders.add(provider);
if (Objects.isNull(firstFailure)) {
firstFailure = e;
@@ -1878,6 +1976,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
try {
provider.seekAfterTopicProgress(topicName, topicProgress);
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
failedProviders.add(provider);
if (Objects.isNull(firstFailure)) {
firstFailure = e;
@@ -2148,6 +2250,10 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
try {
return provider.heartbeat().getEndPoints();
} catch (final Exception e) {
+ if (e instanceof SubscriptionConsumerFencedException) {
+ fence((SubscriptionConsumerFencedException) e);
+ throw (SubscriptionConsumerFencedException) e;
+ }
LOGGER.warn(
SubscriptionMessages
.LOG_ARG_FAILED_FETCH_ALL_ENDPOINTS_SUBSCRIPTION_PROVIDER_ARG_TRY_NEXT_25651CAD,
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 92350097787..02a6de8a9bb 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
@@ -26,6 +26,7 @@ import
org.apache.iotdb.rpc.subscription.config.ConsumerConfig;
import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
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.exception.SubscriptionOwnerFencedException;
import
org.apache.iotdb.rpc.subscription.exception.SubscriptionPipeTimeoutException;
@@ -91,6 +92,7 @@ public abstract class AbstractSubscriptionProvider {
private String consumerId;
private String consumerGroupId;
+ private String consumerInstanceId;
private final String ownerId;
private final Long ownerEpoch;
@@ -189,6 +191,10 @@ public abstract class AbstractSubscriptionProvider {
return consumerGroupId;
}
+ void setConsumerInstanceId(final String consumerInstanceId) {
+ this.consumerInstanceId = consumerInstanceId;
+ }
+
TEndPoint getEndPoint() {
return endPoint;
}
@@ -206,6 +212,9 @@ public abstract class AbstractSubscriptionProvider {
final Map<String, String> consumerAttributes = new HashMap<>();
consumerAttributes.put(ConsumerConstant.CONSUMER_GROUP_ID_KEY,
consumerGroupId);
consumerAttributes.put(ConsumerConstant.CONSUMER_ID_KEY, consumerId);
+ if (consumerInstanceId != null) {
+ consumerAttributes.put(ConsumerConstant.CONSUMER_INSTANCE_ID_KEY,
consumerInstanceId);
+ }
if (ownerId != null) {
consumerAttributes.put(ConsumerConstant.OWNER_ID_KEY, ownerId);
}
@@ -327,6 +336,15 @@ public abstract class AbstractSubscriptionProvider {
}
}
+ synchronized void closeSession() throws IoTDBConnectionException {
+ try {
+ session.close();
+ } finally {
+ setUnavailable();
+ isClosed.set(true);
+ }
+ }
+
void closeInternal() throws SubscriptionException {
final TPipeSubscribeResp resp;
try {
@@ -711,6 +729,9 @@ public abstract class AbstractSubscriptionProvider {
LOGGER.warn(errorMessage);
throw new SubscriptionOwnerFencedException(errorMessage);
}
+ case 1919: // SUBSCRIPTION_CONSUMER_FENCED
+ LOGGER.warn(status.message);
+ throw new SubscriptionConsumerFencedException(status.message);
case 1900: // SUBSCRIPTION_VERSION_ERROR
case 1901: // SUBSCRIPTION_TYPE_ERROR
case 1909: // SUBSCRIPTION_MISSING_CONSUMER
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 0f8e032bec2..3eaf6e6906e 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
@@ -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.IoTDBConnectionException;
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.i18n.SubscriptionMessages;
import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
@@ -92,6 +93,9 @@ final class AbstractSubscriptionProviders {
try {
defaultProvider = consumer.constructProviderAndHandshake(endPoint);
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ throw e;
} catch (final Exception e) {
connectionFailures.put(endPoint,
consumer.sanitizeConnectionFailureMessage(e));
connectionFailureCauses.add(e);
@@ -109,6 +113,9 @@ final class AbstractSubscriptionProviders {
final Map<Integer, TEndPoint> allEndPoints;
try {
allEndPoints = defaultProvider.heartbeat().getEndPoints();
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ throw e;
} catch (final Exception e) {
LOGGER.warn(
SubscriptionMessages.LOG_ARG_FAILED_FETCH_ALL_ENDPOINTS_ARG_BECAUSE_ARG_2C9E11D4,
@@ -127,6 +134,9 @@ final class AbstractSubscriptionProviders {
final AbstractSubscriptionProvider provider;
try {
provider = consumer.constructProviderAndHandshake(entry.getValue());
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ throw e;
} catch (final Exception e) {
LOGGER.warn(
SubscriptionMessages.LOG_ARG_FAILED_CREATE_CONNECTION_ARG_BECAUSE_ARG_E536E22A,
@@ -164,11 +174,20 @@ final class AbstractSubscriptionProviders {
/** Detaches and closes the current providers. Terminal consumer close may
call this lock-free. */
void closeProviders() {
+ closeProviders(true);
+ }
+
+ /** Detaches and closes the current providers. Terminal consumer close may
call this lock-free. */
+ void closeProviders(final boolean closeConsumer) {
final List<AbstractSubscriptionProvider> providers = getAllProviders();
subscriptionProviders.clear();
for (final AbstractSubscriptionProvider provider : providers) {
try {
- provider.close();
+ if (closeConsumer) {
+ provider.close();
+ } else {
+ provider.closeSession();
+ }
} catch (final Exception e) {
LOGGER.warn(SubscriptionMessages.PROVIDER_CLOSE_FAILED, provider, e,
e);
}
@@ -293,13 +312,13 @@ final class AbstractSubscriptionProviders {
/////////////////////////////// heartbeat ///////////////////////////////
void heartbeat(final AbstractSubscriptionConsumer consumer) {
- if (consumer.isClosed()) {
+ if (consumer.isClosed() || consumer.isFenced()) {
return;
}
acquireWriteLock();
try {
- if (consumer.isClosed()) {
+ if (consumer.isClosed() || consumer.isFenced()) {
return;
}
heartbeatInternal(consumer);
@@ -310,6 +329,9 @@ final class AbstractSubscriptionProviders {
private void heartbeatInternal(final AbstractSubscriptionConsumer consumer) {
for (final AbstractSubscriptionProvider provider : getAllProviders()) {
+ if (consumer.isFenced()) {
+ return;
+ }
try {
final List<SubscriptionCommitContext> processorBufferedCommitContexts =
consumer.getProcessorBufferedCommitContexts(provider.getDataNodeId());
@@ -326,6 +348,10 @@ final class AbstractSubscriptionProviders {
consumer.unsubscribe(topicName);
}
provider.setAvailable();
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ provider.setUnavailable();
+ return;
} catch (final Exception e) {
LOGGER.warn(
SubscriptionMessages
@@ -342,13 +368,13 @@ final class AbstractSubscriptionProviders {
/////////////////////////////// sync endpoints
///////////////////////////////
void sync(final AbstractSubscriptionConsumer consumer) {
- if (consumer.isClosed()) {
+ if (consumer.isClosed() || consumer.isFenced()) {
return;
}
acquireWriteLock();
try {
- if (consumer.isClosed()) {
+ if (consumer.isClosed() || consumer.isFenced()) {
return;
}
syncInternal(consumer);
@@ -358,9 +384,15 @@ final class AbstractSubscriptionProviders {
}
private void syncInternal(final AbstractSubscriptionConsumer consumer) {
+ if (consumer.isFenced()) {
+ return;
+ }
if (hasNoAvailableProviders()) {
try {
openProviders(consumer);
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ return;
} catch (final Exception e) {
LOGGER.warn(SubscriptionMessages.OPEN_PROVIDERS_FAILED, consumer, e,
e);
return;
@@ -370,6 +402,9 @@ final class AbstractSubscriptionProviders {
final Map<Integer, TEndPoint> allEndPoints;
try {
allEndPoints = consumer.fetchAllEndPointsWithRedirection();
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ return;
} catch (final Exception e) {
LOGGER.warn(SubscriptionMessages.FETCH_ENDPOINTS_FAILED, consumer, e, e);
return;
@@ -377,6 +412,9 @@ final class AbstractSubscriptionProviders {
// add new providers or handshake existing providers
for (final Map.Entry<Integer, TEndPoint> entry : allEndPoints.entrySet()) {
+ if (consumer.isFenced()) {
+ return;
+ }
final AbstractSubscriptionProvider provider =
getProvider(entry.getKey());
if (Objects.isNull(provider)) {
// new provider
@@ -384,6 +422,9 @@ final class AbstractSubscriptionProviders {
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,
@@ -399,6 +440,10 @@ final class AbstractSubscriptionProviders {
try {
consumer.subscribedTopics = provider.heartbeat().getTopics();
provider.setAvailable();
+ } catch (final SubscriptionConsumerFencedException e) {
+ consumer.fence(e);
+ provider.setUnavailable();
+ return;
} catch (final Exception e) {
LOGGER.warn(
SubscriptionMessages
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPullConsumer.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPullConsumer.java
index 61d558cd829..b565d0d9293 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPullConsumer.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPullConsumer.java
@@ -166,6 +166,13 @@ public abstract class AbstractSubscriptionPullConsumer
extends AbstractSubscript
return;
}
+ if (isFenced()) {
+ isClosed.set(true);
+ prepareClose();
+ super.close();
+ return;
+ }
+
List<SubscriptionMessage> drainedProcessorMessages =
Collections.emptyList();
if (!processors.isEmpty()) {
drainedProcessorMessages = drainProcessorPipeline();
@@ -640,7 +647,7 @@ public abstract class AbstractSubscriptionPullConsumer
extends AbstractSubscript
future[0] =
SubscriptionExecutorServiceManager.submitAutoCommitWorker(
() -> {
- if (isClosed()) {
+ if (isClosed() || isFenced()) {
if (Objects.nonNull(future[0])) {
future[0].cancel(false);
LOGGER.info(SubscriptionMessages.PULL_CONSUMER_CANCEL_AUTO_COMMIT, this);
@@ -656,7 +663,7 @@ public abstract class AbstractSubscriptionPullConsumer
extends AbstractSubscript
private class AutoCommitWorker implements Runnable {
@Override
public void run() {
- if (isClosed()) {
+ if (isClosed() || isFenced()) {
return;
}
@@ -690,6 +697,9 @@ public abstract class AbstractSubscriptionPullConsumer
extends AbstractSubscript
}
private void commitAllUncommittedMessages() {
+ if (isFenced()) {
+ return;
+ }
for (final Map.Entry<Long, Set<SubscriptionCommitContext>> entry :
uncommittedCommitContexts.entrySet()) {
try {
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPushConsumer.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPushConsumer.java
index a06a2e9cc4e..972b4262e4d 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPushConsumer.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionPushConsumer.java
@@ -175,7 +175,7 @@ public abstract class AbstractSubscriptionPushConsumer
extends AbstractSubscript
future[0] =
SubscriptionExecutorServiceManager.submitAutoPollWorker(
() -> {
- if (isClosed()) {
+ if (isClosed() || isFenced()) {
if (Objects.nonNull(future[0])) {
future[0].cancel(false);
LOGGER.info(SubscriptionMessages.PUSH_CONSUMER_CANCEL_AUTO_POLL, this);
@@ -191,7 +191,7 @@ public abstract class AbstractSubscriptionPushConsumer
extends AbstractSubscript
class AutoPollWorker implements Runnable {
@Override
public void run() {
- if (isClosed()) {
+ if (isClosed() || isFenced()) {
return;
}
diff --git
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerLifecycleTest.java
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerLifecycleTest.java
index df42e2b244c..00db8d31145 100644
---
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerLifecycleTest.java
+++
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionConsumerLifecycleTest.java
@@ -20,23 +20,39 @@
package org.apache.iotdb.session.subscription.consumer.base;
import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+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;
+import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType;
+import org.apache.iotdb.rpc.subscription.payload.poll.TabletsPayload;
+import org.apache.iotdb.rpc.subscription.payload.poll.TopicProgress;
import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeHeartbeatResp;
import org.apache.iotdb.session.AbstractSessionBuilder;
import org.apache.iotdb.session.subscription.SubscriptionTreeSessionBuilder;
+import org.apache.iotdb.session.subscription.consumer.AsyncCommitCallback;
+import org.apache.iotdb.session.subscription.payload.SubscriptionMessage;
import org.junit.Assert;
import org.junit.Test;
+import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
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.AtomicReference;
import java.util.function.BooleanSupplier;
public class SubscriptionConsumerLifecycleTest {
@@ -97,6 +113,179 @@ public class SubscriptionConsumerLifecycleTest {
Assert.assertTrue(consumer.closedStatesDuringClose.get(0));
}
+ @Test
+ public void testFencedHeartbeatStopsBackgroundReconnect() throws Exception {
+ final TestPullConsumer consumer = new TestPullConsumer();
+ final AbstractSubscriptionProviders providers = getProviders(consumer);
+ try {
+ consumer.open();
+ consumer.fenceOnHeartbeat = true;
+ providers.heartbeat(consumer);
+
+ Assert.assertTrue(consumer.isFenced());
+ providers.sync(consumer);
+ providers.heartbeat(consumer);
+ Assert.assertEquals(1, consumer.createdProviders.size());
+ try {
+ consumer.multiplePoll(Collections.singleton("topic"), 100L);
+ Assert.fail("The fenced consumer must not poll or reconnect");
+ } catch (final SubscriptionConsumerFencedException expected) {
+ Assert.assertEquals("consumer connection fenced",
expected.getMessage());
+ }
+ consumer.close();
+ Assert.assertEquals(0, consumer.closeRequestCount);
+ Assert.assertEquals(1, consumer.sessionCloseCount);
+ Assert.assertEquals(1, consumer.closedStatesDuringClose.size());
+ } finally {
+ consumer.close();
+ }
+ }
+
+ @Test
+ public void testFencedDuringOpenClosesPartiallyOpenedProviders() throws
Exception {
+ final TestPullConsumer consumer = new TestPullConsumer();
+ consumer.fenceOnHeartbeat = true;
+ try {
+ consumer.open();
+ Assert.fail("The consumer must fail to open when its handshake is
fenced");
+ } catch (final SubscriptionConsumerFencedException expected) {
+ Assert.assertTrue(consumer.isFenced());
+ Assert.assertEquals(1, consumer.createdProviders.size());
+ Assert.assertEquals(0, consumer.closeRequestCount);
+ Assert.assertEquals(1, consumer.sessionCloseCount);
+ Assert.assertEquals(1, consumer.closedStatesDuringClose.size());
+ }
+
+ try {
+ consumer.open();
+ Assert.fail("The fenced consumer must not retry the handshake");
+ } catch (final SubscriptionConsumerFencedException expected) {
+ Assert.assertEquals(1, consumer.createdProviders.size());
+ }
+ }
+
+ @Test
+ public void testFencedHandshakeClosesOpenedSession() throws Exception {
+ final TestPullConsumer consumer = new TestPullConsumer();
+ consumer.fenceOnHandshake = true;
+
+ try {
+ consumer.open();
+ Assert.fail("The consumer must fail to open when its handshake is
fenced");
+ } catch (final SubscriptionConsumerFencedException expected) {
+ Assert.assertTrue(consumer.isFenced());
+ Assert.assertEquals(1, consumer.createdProviders.size());
+ Assert.assertEquals(0, consumer.closeRequestCount);
+ Assert.assertEquals(1, consumer.sessionCloseCount);
+ Assert.assertEquals(1, consumer.closedStatesDuringClose.size());
+ }
+ }
+
+ @Test
+ public void testFencedTabletContinuationDoesNotSendNack() throws Exception {
+ final TestPullConsumer consumer = new TestPullConsumer();
+ try {
+ consumer.open();
+ consumer.returnPartialTablets = true;
+ consumer.fenceOnPollTablets = true;
+
+ try {
+ consumer.multiplePoll(Collections.singleton("topic"), 1_000L);
+ Assert.fail("A fenced tablet continuation must fail the poll");
+ } catch (final SubscriptionConsumerFencedException expected) {
+ Assert.assertEquals("consumer connection fenced",
expected.getMessage());
+ }
+
+ Assert.assertTrue(consumer.isFenced());
+ Assert.assertEquals(0, consumer.commitRequestCount);
+ } finally {
+ consumer.close();
+ }
+ }
+
+ @Test
+ public void testFencedParallelPollDoesNotDeliverSiblingMessages() throws
Exception {
+ final TestPullConsumer consumer = new TestPullConsumer();
+ final SubscriptionConsumerFencedException fencedException =
+ new SubscriptionConsumerFencedException("consumer connection fenced");
+ final SubscriptionMessage message =
+ new SubscriptionMessage(
+ new SubscriptionCommitContext(0, 0, "topic", CONSUMER_GROUP_ID,
0L), 1L);
+ final CompletableFuture<List<SubscriptionMessage>> fencedFuture = new
CompletableFuture<>();
+ fencedFuture.completeExceptionally(fencedException);
+
+ try {
+ consumer.collectMultiplePollResults(
+ Arrays.asList(
+
CompletableFuture.completedFuture(Collections.singletonList(message)),
fencedFuture),
+ Collections.singleton("topic"));
+ Assert.fail("A fenced poll task must discard messages returned by
sibling tasks");
+ } catch (final SubscriptionConsumerFencedException expected) {
+ Assert.assertSame(fencedException, expected);
+ }
+
+ Assert.assertTrue(consumer.isFenced());
+ }
+
+ @Test
+ public void testFencedAsyncCommitFailsBeforeReadingMessages() throws
Exception {
+ final TestPullConsumer consumer = new TestPullConsumer();
+ try {
+ final SubscriptionConsumerFencedException fencedException =
+ new SubscriptionConsumerFencedException("consumer connection
fenced");
+ consumer.fence(fencedException);
+ final AtomicBoolean messagesIterated = new AtomicBoolean(false);
+ final Iterable<SubscriptionMessage> messages =
+ () -> {
+ messagesIterated.set(true);
+ return Collections.emptyIterator();
+ };
+
+ final CountDownLatch callbackCompleted = new CountDownLatch(1);
+ final AtomicBoolean callbackSucceeded = new AtomicBoolean(false);
+ final AtomicReference<Throwable> callbackFailure = new
AtomicReference<>();
+ consumer.commitAsync(
+ messages,
+ new AsyncCommitCallback() {
+ @Override
+ public void onComplete() {
+ callbackSucceeded.set(true);
+ callbackCompleted.countDown();
+ }
+
+ @Override
+ public void onFailure(final Throwable e) {
+ callbackFailure.set(e);
+ callbackCompleted.countDown();
+ }
+ });
+
+ Assert.assertTrue(callbackCompleted.await(5, TimeUnit.SECONDS));
+ Assert.assertFalse(callbackSucceeded.get());
+ Assert.assertSame(fencedException, callbackFailure.get());
+
+ final CompletableFuture<Void> future = consumer.commitAsync(messages);
+ try {
+ future.get(5, TimeUnit.SECONDS);
+ Assert.fail("A fenced async commit must complete exceptionally");
+ } catch (final ExecutionException expected) {
+ Assert.assertSame(fencedException, expected.getCause());
+ }
+
+ Assert.assertFalse(messagesIterated.get());
+ Assert.assertEquals(0, consumer.commitRequestCount);
+ } finally {
+ consumer.close();
+ }
+ }
+
+ private AbstractSubscriptionProviders getProviders(final
AbstractSubscriptionConsumer consumer)
+ throws Exception {
+ final Field field =
AbstractSubscriptionConsumer.class.getDeclaredField("providers");
+ field.setAccessible(true);
+ return (AbstractSubscriptionProviders) field.get(consumer);
+ }
+
@Test
public void testConcurrentPullConsumerCloseReturnsWithoutWaiting() throws
Exception {
final CountDownLatch providerCloseStarted = new CountDownLatch(1);
@@ -169,7 +358,16 @@ public class SubscriptionConsumerLifecycleTest {
connectionTimeoutInMs,
this::isClosed,
closedStatesDuringHandshake,
- closedStatesDuringClose);
+ closedStatesDuringClose,
+ () -> false,
+ () -> false,
+ () -> false,
+ () -> false,
+ () -> {},
+ () -> {},
+ () -> {},
+ null,
+ null);
}
}
@@ -177,6 +375,14 @@ public class SubscriptionConsumerLifecycleTest {
private final List<Boolean> closedStatesDuringHandshake = new
ArrayList<>();
private final List<Boolean> closedStatesDuringClose = new ArrayList<>();
+ private final List<TestSubscriptionProvider> createdProviders = new
ArrayList<>();
+ private boolean fenceOnHandshake;
+ private boolean fenceOnHeartbeat;
+ private boolean fenceOnPollTablets;
+ private boolean returnPartialTablets;
+ private int closeRequestCount;
+ private int sessionCloseCount;
+ private int commitRequestCount;
private final CountDownLatch providerCloseStarted;
private final CountDownLatch allowProviderClose;
@@ -212,23 +418,33 @@ public class SubscriptionConsumerLifecycleTest {
final int thriftMaxFrameSize,
final long heartbeatIntervalMs,
final int connectionTimeoutInMs) {
- return new TestSubscriptionProvider(
- endPoint,
- username,
- password,
- encryptedPassword,
- consumerId,
- consumerGroupId,
- ownerId,
- ownerEpoch,
- thriftMaxFrameSize,
- heartbeatIntervalMs,
- connectionTimeoutInMs,
- this::isClosed,
- closedStatesDuringHandshake,
- closedStatesDuringClose,
- providerCloseStarted,
- allowProviderClose);
+ final TestSubscriptionProvider provider =
+ new TestSubscriptionProvider(
+ endPoint,
+ username,
+ password,
+ encryptedPassword,
+ consumerId,
+ consumerGroupId,
+ ownerId,
+ ownerEpoch,
+ thriftMaxFrameSize,
+ heartbeatIntervalMs,
+ connectionTimeoutInMs,
+ this::isClosed,
+ closedStatesDuringHandshake,
+ closedStatesDuringClose,
+ () -> fenceOnHandshake,
+ () -> fenceOnHeartbeat,
+ () -> fenceOnPollTablets,
+ () -> returnPartialTablets,
+ () -> closeRequestCount++,
+ () -> commitRequestCount++,
+ () -> sessionCloseCount++,
+ providerCloseStarted,
+ allowProviderClose);
+ createdProviders.add(provider);
+ return provider;
}
}
@@ -237,43 +453,16 @@ public class SubscriptionConsumerLifecycleTest {
private final BooleanSupplier consumerClosedSupplier;
private final List<Boolean> closedStatesDuringHandshake;
private final List<Boolean> closedStatesDuringClose;
+ private final BooleanSupplier fenceOnHandshake;
+ private final BooleanSupplier fenceOnHeartbeat;
+ private final BooleanSupplier fenceOnPollTablets;
+ private final BooleanSupplier returnPartialTablets;
+ private final Runnable closeRequest;
+ private final Runnable commitRequest;
+ private final Runnable sessionClose;
private final CountDownLatch providerCloseStarted;
private final CountDownLatch allowProviderClose;
- private TestSubscriptionProvider(
- final TEndPoint endPoint,
- final String username,
- final String password,
- final String encryptedPassword,
- final String consumerId,
- final String consumerGroupId,
- final String ownerId,
- final Long ownerEpoch,
- final int thriftMaxFrameSize,
- final long heartbeatIntervalMs,
- final int connectionTimeoutInMs,
- final BooleanSupplier consumerClosedSupplier,
- final List<Boolean> closedStatesDuringHandshake,
- final List<Boolean> closedStatesDuringClose) {
- this(
- endPoint,
- username,
- password,
- encryptedPassword,
- consumerId,
- consumerGroupId,
- ownerId,
- ownerEpoch,
- thriftMaxFrameSize,
- heartbeatIntervalMs,
- connectionTimeoutInMs,
- consumerClosedSupplier,
- closedStatesDuringHandshake,
- closedStatesDuringClose,
- null,
- null);
- }
-
private TestSubscriptionProvider(
final TEndPoint endPoint,
final String username,
@@ -289,6 +478,13 @@ public class SubscriptionConsumerLifecycleTest {
final BooleanSupplier consumerClosedSupplier,
final List<Boolean> closedStatesDuringHandshake,
final List<Boolean> closedStatesDuringClose,
+ final BooleanSupplier fenceOnHandshake,
+ final BooleanSupplier fenceOnHeartbeat,
+ final BooleanSupplier fenceOnPollTablets,
+ final BooleanSupplier returnPartialTablets,
+ final Runnable closeRequest,
+ final Runnable commitRequest,
+ final Runnable sessionClose,
final CountDownLatch providerCloseStarted,
final CountDownLatch allowProviderClose) {
super(
@@ -306,6 +502,13 @@ public class SubscriptionConsumerLifecycleTest {
this.consumerClosedSupplier = consumerClosedSupplier;
this.closedStatesDuringHandshake = closedStatesDuringHandshake;
this.closedStatesDuringClose = closedStatesDuringClose;
+ this.fenceOnHandshake = fenceOnHandshake;
+ this.fenceOnHeartbeat = fenceOnHeartbeat;
+ this.fenceOnPollTablets = fenceOnPollTablets;
+ this.returnPartialTablets = returnPartialTablets;
+ this.closeRequest = closeRequest;
+ this.commitRequest = commitRequest;
+ this.sessionClose = sessionClose;
this.providerCloseStarted = providerCloseStarted;
this.allowProviderClose = allowProviderClose;
}
@@ -333,12 +536,16 @@ public class SubscriptionConsumerLifecycleTest {
@Override
synchronized void handshake() {
closedStatesDuringHandshake.add(consumerClosedSupplier.getAsBoolean());
+ if (fenceOnHandshake.getAsBoolean()) {
+ throw new SubscriptionConsumerFencedException("consumer connection
fenced");
+ }
setAvailable();
}
@Override
synchronized void close() {
closedStatesDuringClose.add(consumerClosedSupplier.getAsBoolean());
+ closeRequest.run();
if (Objects.nonNull(providerCloseStarted)) {
providerCloseStarted.countDown();
}
@@ -352,10 +559,51 @@ public class SubscriptionConsumerLifecycleTest {
setUnavailable();
}
+ @Override
+ synchronized void closeSession() {
+ closedStatesDuringClose.add(consumerClosedSupplier.getAsBoolean());
+ sessionClose.run();
+ setUnavailable();
+ }
+
@Override
PipeSubscribeHeartbeatResp heartbeat(
final List<SubscriptionCommitContext> processorBufferedCommitContexts)
{
+ if (fenceOnHeartbeat.getAsBoolean()) {
+ throw new SubscriptionConsumerFencedException("consumer connection
fenced");
+ }
return new PipeSubscribeHeartbeatResp();
}
+
+ @Override
+ List<SubscriptionPollResponse> poll(
+ final Set<String> topicNames,
+ final long timeoutMs,
+ final Map<String, TopicProgress> progressByTopic) {
+ if (!returnPartialTablets.getAsBoolean()) {
+ return Collections.emptyList();
+ }
+ return Collections.singletonList(
+ new SubscriptionPollResponse(
+ SubscriptionPollResponseType.TABLETS.getType(),
+ new TabletsPayload(Collections.emptyMap(), 1),
+ new SubscriptionCommitContext(0, 0, "topic", CONSUMER_GROUP_ID,
0L)));
+ }
+
+ @Override
+ List<SubscriptionPollResponse> pollTablets(
+ final SubscriptionCommitContext commitContext, final int offset, final
long timeoutMs) {
+ if (fenceOnPollTablets.getAsBoolean()) {
+ throw new SubscriptionConsumerFencedException("consumer connection
fenced");
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ CommitResult commit(
+ final List<SubscriptionCommitContext> subscriptionCommitContexts,
final boolean nack) {
+ commitRequest.run();
+ return CommitResult.empty();
+ }
}
}
diff --git
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderStatusTest.java
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderStatusTest.java
new file mode 100644
index 00000000000..5da15cadcb4
--- /dev/null
+++
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderStatusTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.session.subscription.consumer.base;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.rpc.TSStatusCode;
+import
org.apache.iotdb.rpc.subscription.exception.SubscriptionConsumerFencedException;
+import org.apache.iotdb.rpc.subscription.exception.SubscriptionException;
+import
org.apache.iotdb.rpc.subscription.exception.SubscriptionRuntimeCriticalException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class SubscriptionProviderStatusTest {
+
+ @Test
+ public void testConsumerFencedStatusMapsToSpecificException() throws
Exception {
+ final SubscriptionException exception =
+ invokeVerifyPipeSubscribeSuccess(
+ new
TSStatus(TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode())
+ .setMessage("consumer fenced"));
+
+ Assert.assertTrue(exception instanceof
SubscriptionConsumerFencedException);
+ Assert.assertEquals("consumer fenced", exception.getMessage());
+ }
+
+ @Test
+ public void testMissingConsumerStatusRemainsCriticalException() throws
Exception {
+ final SubscriptionException exception =
+ invokeVerifyPipeSubscribeSuccess(
+ new
TSStatus(TSStatusCode.SUBSCRIPTION_MISSING_CONSUMER.getStatusCode())
+ .setMessage("missing consumer"));
+
+ Assert.assertEquals(SubscriptionRuntimeCriticalException.class,
exception.getClass());
+ Assert.assertEquals("missing consumer", exception.getMessage());
+ }
+
+ private SubscriptionException invokeVerifyPipeSubscribeSuccess(final
TSStatus status)
+ throws Exception {
+ final Method method =
+ AbstractSubscriptionProvider.class.getDeclaredMethod(
+ "verifyPipeSubscribeSuccess", TSStatus.class);
+ method.setAccessible(true);
+ try {
+ method.invoke(null, status);
+ Assert.fail("Expected a subscription exception");
+ return null;
+ } catch (final InvocationTargetException e) {
+ return (SubscriptionException) e.getCause();
+ }
+ }
+}
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 b818a52e838..d0236c1293d 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
@@ -1798,6 +1798,12 @@ public final class DataNodePipeMessages {
"Subscription: The consumer {} has already existed when handshaking,
skip creating consumer.";
public static final String
PIPE_LOG_SUBSCRIPTION_CONSUMER_HANDSHAKE_SUCCESSFULLY_DATA_NODE_ID_58DA6A5F =
"Subscription: consumer {} handshake successfully, data node id: {}";
+ public static final String
+
LOG_SUBSCRIPTION_CONSUMER_ARG_IN_CONSUMER_GROUP_ARG_WAS_TAKEN_OVER_BY_A_NEWER_CONNECTION_FENCED_THE_PREVIOUS_CONNECTION_4E72DBD9
=
+ "Subscription: consumer {} in consumer group {} was taken over by a
newer connection; fenced the previous connection.";
+ public static final String
+
MESSAGE_SUBSCRIPTION_CONSUMER_CONNECTION_WAS_FENCED_BECAUSE_A_NEWER_CONNECTION_WITH_THE_SAME_CONSUMER_ID_AND_CONSUMER_GROUP_ID_COMPLETED_THE_HANDSHAKE_THIS_CONSUMER_INSTANCE_CANNOT_BE_REUSED_CREATE_A_NEW_CONSUMER_INSTANCE_TO_RECONNECT_B0C2CCBE
=
+ "Subscription: consumer connection was fenced because a newer
connection with the same consumer ID and consumer group ID completed the
handshake. This consumer instance cannot be reused; create a new consumer
instance to reconnect.";
public static final String
PIPE_LOG_SUBSCRIPTION_CONSUMER_UNSUBSCRIBE_SUCCESSFULLY_AA5E0AA9 =
"Subscription: consumer {} unsubscribe {} successfully";
public static final String
PIPE_LOG_SUBSCRIPTION_CONSUMER_COMMIT_NACK_ACCEPTED_SUCCESSFULLY_58D1C111 =
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 fe768a1f018..d11e16f41f6 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
@@ -1667,6 +1667,12 @@ public final class DataNodePipeMessages {
"Subscription:握手时 consumer {} 已存在,跳过 consumer 创建。";
public static final String
PIPE_LOG_SUBSCRIPTION_CONSUMER_HANDSHAKE_SUCCESSFULLY_DATA_NODE_ID_58DA6A5F =
"Subscription:consumer {} 握手成功,data node id:{}";
+ public static final String
+
LOG_SUBSCRIPTION_CONSUMER_ARG_IN_CONSUMER_GROUP_ARG_WAS_TAKEN_OVER_BY_A_NEWER_CONNECTION_FENCED_THE_PREVIOUS_CONNECTION_4E72DBD9
=
+ "Subscription:consumer {}(consumer group {})已被新连接接管,旧连接已被隔离。";
+ public static final String
+
MESSAGE_SUBSCRIPTION_CONSUMER_CONNECTION_WAS_FENCED_BECAUSE_A_NEWER_CONNECTION_WITH_THE_SAME_CONSUMER_ID_AND_CONSUMER_GROUP_ID_COMPLETED_THE_HANDSHAKE_THIS_CONSUMER_INSTANCE_CANNOT_BE_REUSED_CREATE_A_NEW_CONSUMER_INSTANCE_TO_RECONNECT_B0C2CCBE
=
+ "Subscription:consumer 连接已被隔离,因为具有相同 consumer ID 和 consumer group ID
的新连接已完成握手。当前 consumer 实例不可复用,请创建新的 consumer 实例进行重连。";
public static final String
PIPE_LOG_SUBSCRIPTION_CONSUMER_UNSUBSCRIBE_SUCCESSFULLY_AA5E0AA9 =
"Subscription:consumer {} 取消订阅 {} 成功";
public static final String
PIPE_LOG_SUBSCRIPTION_CONSUMER_COMMIT_NACK_ACCEPTED_SUCCESSFULLY_58D1C111 =
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
index 3fdadb6e84e..dc42d4fbcae 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
@@ -47,6 +47,7 @@ import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
@@ -62,6 +63,15 @@ public class SubscriptionReceiverAgent {
PipeSubscribeResponseVersion.VERSION_1.getVersion(),
PipeSubscribeResponseType.ACK.getType());
+ private static final TPipeSubscribeResp SUBSCRIPTION_CONSUMER_FENCED_RESP =
+ new TPipeSubscribeResp(
+ RpcUtils.getStatus(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED,
+ DataNodePipeMessages
+
.MESSAGE_SUBSCRIPTION_CONSUMER_CONNECTION_WAS_FENCED_BECAUSE_A_NEWER_CONNECTION_WITH_THE_SAME_CONSUMER_ID_AND_CONSUMER_GROUP_ID_COMPLETED_THE_HANDSHAKE_THIS_CONSUMER_INSTANCE_CANNOT_BE_REUSED_CREATE_A_NEW_CONSUMER_INSTANCE_TO_RECONNECT_B0C2CCBE),
+ PipeSubscribeResponseVersion.VERSION_1.getVersion(),
+ PipeSubscribeResponseType.ACK.getType());
+
private final Map<Byte, Supplier<SubscriptionReceiver>> receiverConstructors
= new HashMap<>();
private final ThreadLocal<SubscriptionReceiver> receiverThreadLocal = new
ThreadLocal<>();
@@ -127,7 +137,8 @@ public class SubscriptionReceiverAgent {
if (receiverConstructors.containsKey(reqVersion)) {
final SubscriptionReceiver receiver = getReceiver(reqVersion);
receiver.setAuthenticatedUsername(username);
- final ConsumerIdentity consumerIdentity = getConsumerIdentity(req,
receiver);
+ final ConsumerConnection consumerConnection = getConsumerConnection(req,
receiver);
+ final ConsumerIdentity consumerIdentity = consumerConnection.identity();
final RequestResult requestResult = new RequestResult();
if (Objects.isNull(consumerIdentity)) {
@@ -136,12 +147,21 @@ public class SubscriptionReceiverAgent {
consumerReceivers.compute(
consumerIdentity,
(identity, currentReceiver) -> {
+ if (isHandshake(req)
+ && currentReceiver != null
+ && currentReceiver != receiver
+ && shouldKeepCurrentReceiver(
+ currentReceiver,
consumerConnection.consumerInstanceId())) {
+ receiver.invalidateConsumer();
+ requestResult.response = SUBSCRIPTION_CONSUMER_FENCED_RESP;
+ return currentReceiver;
+ }
requestResult.response = handleRequest(receiver, req,
currentReceiver);
if (isHandshake(req)) {
if (isSuccessful(requestResult.response)) {
if (currentReceiver != null && currentReceiver != receiver) {
- currentReceiver.invalidateConsumer();
+ invalidateReplacedReceiver(currentReceiver, identity);
}
return receiver;
}
@@ -158,7 +178,9 @@ public class SubscriptionReceiverAgent {
if (isHandshake(req) && isSuccessful(requestResult.response)) {
final ConsumerIdentity activeIdentity = getConsumerIdentity(receiver);
if (!Objects.equals(consumerIdentity, activeIdentity)) {
- registerReceiver(receiver, activeIdentity);
+ if (!registerReceiver(receiver, activeIdentity)) {
+ requestResult.response = SUBSCRIPTION_CONSUMER_FENCED_RESP;
+ }
} else {
removeReceiverMappingsExcept(receiver, activeIdentity);
}
@@ -281,21 +303,36 @@ public class SubscriptionReceiverAgent {
return receiver.handle(req);
}
- private void registerReceiver(
+ private boolean registerReceiver(
final SubscriptionReceiver receiver, final ConsumerIdentity identity) {
if (Objects.isNull(identity)) {
removeReceiverMappings(receiver);
- return;
+ return true;
}
+ final AtomicBoolean registered = new AtomicBoolean(false);
consumerReceivers.compute(
identity,
(key, currentReceiver) -> {
- if (currentReceiver != null && currentReceiver != receiver) {
- currentReceiver.invalidateConsumer();
+ if (currentReceiver == null || currentReceiver == receiver) {
+ registered.set(true);
+ return receiver;
+ }
+ if (receiver.getConsumerInstanceId() != null
+ && !shouldKeepCurrentReceiver(currentReceiver,
receiver.getConsumerInstanceId())) {
+ invalidateReplacedReceiver(currentReceiver, key);
+ registered.set(true);
+ return receiver;
}
- return receiver;
+ // The receiver completed its handshake after another receiver had
already claimed the
+ // identity. Keep the current owner and fence this late receiver
instead of allowing an
+ // old connection to take the consumer back.
+ invalidateReplacedReceiver(receiver, key);
+ return currentReceiver;
});
- removeReceiverMappingsExcept(receiver, identity);
+ if (registered.get()) {
+ removeReceiverMappingsExcept(receiver, identity);
+ }
+ return registered.get();
}
private void removeReceiverMappingsExcept(
@@ -314,7 +351,17 @@ public class SubscriptionReceiverAgent {
(identity, currentReceiver) -> consumerReceivers.remove(identity,
receiver));
}
- private static ConsumerIdentity getConsumerIdentity(
+ private void invalidateReplacedReceiver(
+ final SubscriptionReceiver receiver, final ConsumerIdentity identity) {
+ LOGGER.info(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_CONSUMER_ARG_IN_CONSUMER_GROUP_ARG_WAS_TAKEN_OVER_BY_A_NEWER_CONNECTION_FENCED_THE_PREVIOUS_CONNECTION_4E72DBD9,
+ identity.consumerId(),
+ identity.consumerGroupId());
+ receiver.invalidateConsumer();
+ }
+
+ private static ConsumerConnection getConsumerConnection(
final TPipeSubscribeReq req, final SubscriptionReceiver receiver) {
if (isHandshake(req) && req.isSetBody()) {
try {
@@ -325,7 +372,7 @@ public class SubscriptionReceiverAgent {
ConsumerIdentity.of(
consumerConfig.getConsumerGroupId(),
consumerConfig.getConsumerId());
if (Objects.nonNull(identity)) {
- return identity;
+ return new ConsumerConnection(identity,
consumerConfig.getConsumerInstanceId());
}
}
} catch (final RuntimeException ignored) {
@@ -333,7 +380,20 @@ public class SubscriptionReceiverAgent {
// original buffer, so parsing is intentionally done on a duplicate
above.
}
}
- return getConsumerIdentity(receiver);
+ return new ConsumerConnection(getConsumerIdentity(receiver),
receiver.getConsumerInstanceId());
+ }
+
+ private static boolean shouldKeepCurrentReceiver(
+ final SubscriptionReceiver currentReceiver, final String
incomingConsumerInstanceId) {
+ final String currentConsumerInstanceId =
currentReceiver.getConsumerInstanceId();
+ if (Objects.equals(currentConsumerInstanceId, incomingConsumerInstanceId))
{
+ return false;
+ }
+ if (currentConsumerInstanceId == null) {
+ return false;
+ }
+ return incomingConsumerInstanceId == null
+ || currentConsumerInstanceId.compareTo(incomingConsumerInstanceId) > 0;
}
private static ConsumerIdentity getConsumerIdentity(final
SubscriptionReceiver receiver) {
@@ -365,4 +425,6 @@ public class SubscriptionReceiverAgent {
: new ConsumerIdentity(consumerGroupId, consumerId);
}
}
+
+ private record ConsumerConnection(ConsumerIdentity identity, String
consumerInstanceId) {}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
index e5de617c89c..1d2fbe4fc18 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
@@ -47,6 +47,14 @@ public interface SubscriptionReceiver {
*/
String getConsumerGroupId();
+ /**
+ * Returns the identifier shared by all DataNode connections of the current
consumer instance, or
+ * {@code null} for a legacy client.
+ */
+ default String getConsumerInstanceId() {
+ return null;
+ }
+
/**
* Invalidates this receiver so that requests from an obsolete connection
cannot affect a new
* owner.
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
index b0eec505a5c..4c8c1c08d30 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
@@ -47,6 +47,7 @@ import
org.apache.iotdb.db.subscription.metric.SubscriptionPrefetchingQueueMetri
import org.apache.iotdb.rpc.RpcUtils;
import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.iotdb.rpc.subscription.config.ConsumerConfig;
+import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
import org.apache.iotdb.rpc.subscription.config.TopicConfig;
import org.apache.iotdb.rpc.subscription.exception.SubscriptionException;
import
org.apache.iotdb.rpc.subscription.exception.SubscriptionPayloadExceedException;
@@ -121,11 +122,21 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
PipeSubscribeResponseVersion.VERSION_1.getVersion(),
PipeSubscribeResponseType.ACK.getType());
+ private static final TPipeSubscribeResp SUBSCRIPTION_CONSUMER_FENCED_RESP =
+ new TPipeSubscribeResp(
+ RpcUtils.getStatus(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED,
+ DataNodePipeMessages
+
.MESSAGE_SUBSCRIPTION_CONSUMER_CONNECTION_WAS_FENCED_BECAUSE_A_NEWER_CONNECTION_WITH_THE_SAME_CONSUMER_ID_AND_CONSUMER_GROUP_ID_COMPLETED_THE_HANDSHAKE_THIS_CONSUMER_INSTANCE_CANNOT_BE_REUSED_CREATE_A_NEW_CONSUMER_INSTANCE_TO_RECONNECT_B0C2CCBE),
+ PipeSubscribeResponseVersion.VERSION_1.getVersion(),
+ PipeSubscribeResponseType.ACK.getType());
+
private final ThreadLocal<ConsumerConfig> consumerConfigThreadLocal = new
ThreadLocal<>();
private final ThreadLocal<PollTimer> pollTimerThreadLocal = new
ThreadLocal<>();
private volatile String authenticatedUsername;
private volatile ConsumerConfig sharedConsumerConfig;
private volatile boolean consumerInvalidated;
+ private volatile boolean consumerFenced;
private volatile long lastActivityTimeMs = System.currentTimeMillis();
private final AtomicLong inFlightRequestCount = new AtomicLong(0);
private long consumerStateVersion;
@@ -135,8 +146,11 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
@Override
public final TPipeSubscribeResp handle(final TPipeSubscribeReq req) {
final short reqType = req.getType();
- beforeHandle(reqType);
+ final boolean isFencedRequest = beforeHandle(reqType);
try {
+ if (isFencedRequest) {
+ return SUBSCRIPTION_CONSUMER_FENCED_RESP;
+ }
if (PipeSubscribeRequestType.isValidatedRequestType(reqType)) {
switch (PipeSubscribeRequestType.valueOf(reqType)) {
case HANDSHAKE:
@@ -199,6 +213,12 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
return Objects.isNull(consumerConfig) ? null :
consumerConfig.getConsumerGroupId();
}
+ @Override
+ public String getConsumerInstanceId() {
+ final ConsumerConfig consumerConfig = sharedConsumerConfig;
+ return Objects.isNull(consumerConfig) ? null :
consumerConfig.getConsumerInstanceId();
+ }
+
@Override
public boolean hasActiveConsumer() {
return Objects.nonNull(sharedConsumerConfig);
@@ -206,7 +226,10 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
@Override
public void invalidateConsumer() {
- clearSharedConsumerState();
+ synchronized (this) {
+ consumerFenced = true;
+ clearSharedConsumerState();
+ }
}
@Override
@@ -1192,11 +1215,14 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
private void createConsumer(final ConsumerConfig consumerConfig) throws
SubscriptionException {
try (final ConfigNodeClient configNodeClient =
CONFIG_NODE_CLIENT_MANAGER.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) {
+ final Map<String, String> persistedConsumerAttributes =
+ new HashMap<>(consumerConfig.getAttribute());
+
persistedConsumerAttributes.remove(ConsumerConstant.CONSUMER_INSTANCE_ID_KEY);
final TCreateConsumerReq req =
new TCreateConsumerReq()
.setConsumerId(consumerConfig.getConsumerId())
.setConsumerGroupId(consumerConfig.getConsumerGroupId())
- .setConsumerAttributes(consumerConfig.getAttribute());
+ .setConsumerAttributes(persistedConsumerAttributes);
final TSStatus tsStatus = configNodeClient.createConsumer(req);
if (TSStatusCode.SUCCESS_STATUS.getStatusCode() != tsStatus.getCode()) {
LOGGER.warn(
@@ -1340,17 +1366,23 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
}
}
- private void beforeHandle(final short reqType) {
+ private boolean beforeHandle(final short reqType) {
synchronized (this) {
+ final boolean isHandshake = PipeSubscribeRequestType.HANDSHAKE.getType()
== reqType;
+ // A receiver fenced by a newer connection is terminal. In particular,
do not allow the old
+ // connection to handshake again and reclaim the consumer identity. A
normal disconnected
+ // receiver remains recoverable through the existing consumerInvalidated
handshake path.
+ final boolean isFencedRequest = consumerFenced;
if (consumerInvalidated) {
consumerConfigThreadLocal.remove();
pollTimerThreadLocal.remove();
- if (PipeSubscribeRequestType.HANDSHAKE.getType() == reqType) {
+ if (isHandshake && !consumerFenced) {
consumerInvalidated = false;
}
}
inFlightRequestCount.incrementAndGet();
lastActivityTimeMs = System.currentTimeMillis();
+ return isFencedRequest;
}
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgentTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgentTest.java
index bdf75a3a339..2d48518e5f2 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgentTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgentTest.java
@@ -29,6 +29,8 @@ import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeCloseReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeHandshakeReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestType;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestVersion;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSubscribeReq;
+import
org.apache.iotdb.rpc.subscription.payload.request.SubscriptionHeartbeatReq;
import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeResponseType;
import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeResponseVersion;
import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
@@ -41,6 +43,7 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
@@ -135,6 +138,291 @@ public class SubscriptionReceiverAgentTest {
Assert.assertEquals(1, newReceiver.timeoutCount.get());
}
+ @Test
+ public void
testDuplicateConnectionFencesOldReceiverWithoutInvalidatingNewReceiver()
+ throws Exception {
+ final CopyOnWriteArrayList<FakeSubscriptionReceiver> receivers = new
CopyOnWriteArrayList<>();
+ final SubscriptionReceiverAgent agent = createAgent(receivers, false /*
closeOnTimeout */);
+ final TPipeSubscribeReq handshake = createHandshakeRequest("group",
"consumer");
+
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ agent.handle(handshake, "root").getStatus().getCode());
+ final AtomicReference<TPipeSubscribeResp> duplicateResponse = new
AtomicReference<>();
+ final Thread newConnection =
+ new Thread(() -> duplicateResponse.set(agent.handle(handshake,
"root")));
+ newConnection.start();
+ newConnection.join(TimeUnit.SECONDS.toMillis(10));
+ Assert.assertFalse(newConnection.isAlive());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
duplicateResponse.get().getStatus().getCode());
+ final FakeSubscriptionReceiver oldReceiver = receivers.get(0);
+ final FakeSubscriptionReceiver newReceiver = receivers.get(1);
+ Assert.assertTrue(oldReceiver.invalidated);
+ Assert.assertNotNull(newReceiver.consumerConfig);
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode(),
+ agent.handle(SubscriptionHeartbeatReq.toThriftReq(),
"root").getStatus().getCode());
+
+ agent.checkReceiverTimeouts();
+
+ Assert.assertEquals(0, oldReceiver.timeoutCount.get());
+ Assert.assertEquals(1, newReceiver.timeoutCount.get());
+ }
+
+ @Test
+ public void testConsumerInstanceWinnerIsIndependentOfHandshakeOrder() throws
Exception {
+ final TPipeSubscribeReq olderHandshake =
+ createHandshakeRequest("group", "consumer", "0000000000000001-older");
+ final TPipeSubscribeReq newerHandshake =
+ createHandshakeRequest("group", "consumer", "0000000000000002-newer");
+
+ final CopyOnWriteArrayList<FakeSubscriptionReceiver> olderFirstReceivers =
+ new CopyOnWriteArrayList<>();
+ final SubscriptionReceiverAgent olderFirstAgent =
+ createAgent(olderFirstReceivers, false /* closeOnTimeout */);
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ handleOnNewConnection(olderFirstAgent,
olderHandshake).getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ handleOnNewConnection(olderFirstAgent,
newerHandshake).getStatus().getCode());
+ Assert.assertTrue(olderFirstReceivers.get(0).invalidated);
+ Assert.assertFalse(olderFirstReceivers.get(1).invalidated);
+
+ final CopyOnWriteArrayList<FakeSubscriptionReceiver> newerFirstReceivers =
+ new CopyOnWriteArrayList<>();
+ final SubscriptionReceiverAgent newerFirstAgent =
+ createAgent(newerFirstReceivers, false /* closeOnTimeout */);
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ handleOnNewConnection(newerFirstAgent,
newerHandshake).getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode(),
+ handleOnNewConnection(newerFirstAgent,
olderHandshake).getStatus().getCode());
+ Assert.assertFalse(newerFirstReceivers.get(0).invalidated);
+ Assert.assertTrue(newerFirstReceivers.get(1).invalidated);
+ }
+
+ @Test
+ public void testConcurrentHandshakeWithSameIdentityFencesOldReceiver()
throws Exception {
+ final CopyOnWriteArrayList<FakeSubscriptionReceiver> receivers = new
CopyOnWriteArrayList<>();
+ final CountDownLatch oldHandshakeEntered = new CountDownLatch(1);
+ final CountDownLatch releaseOldHandshake = new CountDownLatch(1);
+ final CountDownLatch newReceiverCreated = new CountDownLatch(1);
+ final CountDownLatch newHandshakeFinished = new CountDownLatch(1);
+ final CountDownLatch oldHeartbeatFinished = new CountDownLatch(1);
+ final AtomicInteger receiverIndex = new AtomicInteger();
+ final AtomicReference<Throwable> threadFailure = new AtomicReference<>();
+ final SubscriptionReceiverAgent agent =
+ new SubscriptionReceiverAgent(
+ () -> {
+ final boolean isOldReceiver = receiverIndex.getAndIncrement() ==
0;
+ final FakeSubscriptionReceiver receiver =
+ new FakeSubscriptionReceiver(
+ false,
+ false,
+ isOldReceiver ? oldHandshakeEntered : null,
+ isOldReceiver ? releaseOldHandshake : null);
+ receivers.add(receiver);
+ if (!isOldReceiver) {
+ newReceiverCreated.countDown();
+ }
+ return receiver;
+ },
+ false,
+ () -> true);
+ final TPipeSubscribeReq oldHandshake = createHandshakeRequest("group",
"consumer");
+ final TPipeSubscribeReq newHandshake = createHandshakeRequest("group",
"consumer");
+ final AtomicReference<TPipeSubscribeResp> oldHandshakeResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> newHandshakeResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> oldHeartbeatResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> newHeartbeatResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> newSubscribeResponse = new
AtomicReference<>();
+
+ final Thread oldConnection =
+ new Thread(
+ () -> {
+ try {
+ oldHandshakeResponse.set(agent.handle(oldHandshake, "root"));
+ if (!newHandshakeFinished.await(10, TimeUnit.SECONDS)) {
+ throw new AssertionError("The new handshake did not finish");
+ }
+ oldHeartbeatResponse.set(
+ agent.handle(SubscriptionHeartbeatReq.toThriftReq(),
"root"));
+ } catch (final Throwable t) {
+ threadFailure.compareAndSet(null, t);
+ } finally {
+ oldHeartbeatFinished.countDown();
+ }
+ });
+ final Thread newConnection =
+ new Thread(
+ () -> {
+ try {
+ newHandshakeResponse.set(agent.handle(newHandshake, "root"));
+ newHandshakeFinished.countDown();
+ if (!oldHeartbeatFinished.await(10, TimeUnit.SECONDS)) {
+ throw new AssertionError("The old heartbeat did not finish");
+ }
+ newHeartbeatResponse.set(
+ agent.handle(SubscriptionHeartbeatReq.toThriftReq(),
"root"));
+ newSubscribeResponse.set(
+ agent.handle(
+
PipeSubscribeSubscribeReq.toTPipeSubscribeReq(Set.of("topic")), "root"));
+ } catch (final Throwable t) {
+ threadFailure.compareAndSet(null, t);
+ } finally {
+ newHandshakeFinished.countDown();
+ }
+ });
+
+ oldConnection.start();
+ Assert.assertTrue(oldHandshakeEntered.await(10, TimeUnit.SECONDS));
+ newConnection.start();
+ try {
+ Assert.assertTrue(newReceiverCreated.await(10, TimeUnit.SECONDS));
+ } finally {
+ releaseOldHandshake.countDown();
+ oldConnection.join(TimeUnit.SECONDS.toMillis(10));
+ newConnection.join(TimeUnit.SECONDS.toMillis(10));
+ }
+
+ Assert.assertFalse(oldConnection.isAlive());
+ Assert.assertFalse(newConnection.isAlive());
+ if (threadFailure.get() != null) {
+ throw new AssertionError(threadFailure.get());
+ }
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ oldHandshakeResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ newHandshakeResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode(),
+ oldHeartbeatResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ newHeartbeatResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ newSubscribeResponse.get().getStatus().getCode());
+ Assert.assertEquals(2, receivers.size());
+ Assert.assertTrue(receivers.get(0).invalidated);
+ Assert.assertFalse(receivers.get(1).invalidated);
+ }
+
+ @Test
+ public void testReconnectSucceedsAfterActiveConnectionExits() throws
IOException {
+ final CopyOnWriteArrayList<FakeSubscriptionReceiver> receivers = new
CopyOnWriteArrayList<>();
+ final SubscriptionReceiverAgent agent = createAgent(receivers, false /*
closeOnTimeout */);
+ final TPipeSubscribeReq handshake = createHandshakeRequest("group",
"consumer");
+
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ agent.handle(handshake, "root").getStatus().getCode());
+ agent.handleClientExit();
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ agent.handle(handshake, "root").getStatus().getCode());
+
+ Assert.assertTrue(receivers.get(0).invalidated);
+ Assert.assertFalse(receivers.get(1).invalidated);
+ }
+
+ @Test
+ public void testLateHandshakeCannotTakeOverNewReceiver() throws Exception {
+ final CopyOnWriteArrayList<FakeSubscriptionReceiver> receivers = new
CopyOnWriteArrayList<>();
+ final CountDownLatch oldHandshakeEntered = new CountDownLatch(1);
+ final CountDownLatch releaseOldHandshake = new CountDownLatch(1);
+ final CountDownLatch newOwnerReady = new CountDownLatch(1);
+ final CountDownLatch oldHandshakeFinished = new CountDownLatch(1);
+ final AtomicInteger receiverIndex = new AtomicInteger();
+ final AtomicReference<Throwable> threadFailure = new AtomicReference<>();
+ final SubscriptionReceiverAgent agent =
+ new SubscriptionReceiverAgent(
+ () -> {
+ final boolean isOldReceiver = receiverIndex.getAndIncrement() ==
0;
+ final FakeSubscriptionReceiver receiver =
+ new FakeSubscriptionReceiver(
+ false,
+ true,
+ isOldReceiver ? oldHandshakeEntered : null,
+ isOldReceiver ? releaseOldHandshake : null);
+ receivers.add(receiver);
+ return receiver;
+ },
+ false,
+ () -> true);
+ final TPipeSubscribeReq handshake =
createHandshakeRequestWithoutIdentity();
+ final AtomicReference<TPipeSubscribeResp> oldHandshakeResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> newHandshakeResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> newHeartbeatResponse = new
AtomicReference<>();
+ final AtomicReference<TPipeSubscribeResp> newSubscribeResponse = new
AtomicReference<>();
+
+ final Thread oldConnection =
+ new Thread(
+ () -> {
+ try {
+ oldHandshakeResponse.set(agent.handle(handshake, "root"));
+ } catch (final Throwable t) {
+ threadFailure.set(t);
+ } finally {
+ oldHandshakeFinished.countDown();
+ }
+ });
+ oldConnection.start();
+ Assert.assertTrue(oldHandshakeEntered.await(10, TimeUnit.SECONDS));
+
+ final Thread newConnection =
+ new Thread(
+ () -> {
+ try {
+ newHandshakeResponse.set(agent.handle(handshake, "root"));
+ newOwnerReady.countDown();
+ oldHandshakeFinished.await(10, TimeUnit.SECONDS);
+ newHeartbeatResponse.set(
+ agent.handle(SubscriptionHeartbeatReq.toThriftReq(),
"root"));
+ newSubscribeResponse.set(
+ agent.handle(
+
PipeSubscribeSubscribeReq.toTPipeSubscribeReq(Set.of("topic")), "root"));
+ } catch (final Throwable t) {
+ threadFailure.set(t);
+ newOwnerReady.countDown();
+ }
+ });
+ newConnection.start();
+ try {
+ Assert.assertTrue(newOwnerReady.await(10, TimeUnit.SECONDS));
+ } finally {
+ releaseOldHandshake.countDown();
+ oldConnection.join(TimeUnit.SECONDS.toMillis(10));
+ newConnection.join(TimeUnit.SECONDS.toMillis(10));
+ }
+
+ Assert.assertFalse(oldConnection.isAlive());
+ Assert.assertFalse(newConnection.isAlive());
+ if (threadFailure.get() != null) {
+ throw new AssertionError(threadFailure.get());
+ }
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode(),
+ oldHandshakeResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ newHandshakeResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ newHeartbeatResponse.get().getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ newSubscribeResponse.get().getStatus().getCode());
+ Assert.assertEquals(2, receivers.size());
+ Assert.assertTrue(receivers.get(0).invalidated);
+ Assert.assertFalse(receivers.get(1).invalidated);
+ }
+
@Test
public void testLateExitFromOldConnectionKeepsNewReceiverRegistered() throws
Exception {
final CopyOnWriteArrayList<FakeSubscriptionReceiver> receivers = new
CopyOnWriteArrayList<>();
@@ -228,28 +516,90 @@ public class SubscriptionReceiverAgentTest {
private TPipeSubscribeReq createHandshakeRequest(
final String consumerGroupId, final String consumerId) throws
IOException {
+ return createHandshakeRequest(consumerGroupId, consumerId, null);
+ }
+
+ private TPipeSubscribeReq createHandshakeRequest(
+ final String consumerGroupId, final String consumerId, final String
consumerInstanceId)
+ throws IOException {
final Map<String, String> attributes = new HashMap<>();
attributes.put(ConsumerConstant.CONSUMER_GROUP_ID_KEY, consumerGroupId);
attributes.put(ConsumerConstant.CONSUMER_ID_KEY, consumerId);
+ if (consumerInstanceId != null) {
+ attributes.put(ConsumerConstant.CONSUMER_INSTANCE_ID_KEY,
consumerInstanceId);
+ }
return PipeSubscribeHandshakeReq.toTPipeSubscribeReq(new
ConsumerConfig(attributes));
}
+ private TPipeSubscribeResp handleOnNewConnection(
+ final SubscriptionReceiverAgent agent, final TPipeSubscribeReq request)
throws Exception {
+ final AtomicReference<TPipeSubscribeResp> response = new
AtomicReference<>();
+ final AtomicReference<Throwable> failure = new AtomicReference<>();
+ final Thread connection =
+ new Thread(
+ () -> {
+ try {
+ response.set(agent.handle(request, "root"));
+ } catch (final Throwable t) {
+ failure.set(t);
+ }
+ });
+ connection.start();
+ connection.join(TimeUnit.SECONDS.toMillis(10));
+ Assert.assertFalse(connection.isAlive());
+ if (failure.get() != null) {
+ throw new AssertionError(failure.get());
+ }
+ return response.get();
+ }
+
+ private TPipeSubscribeReq createHandshakeRequestWithoutIdentity() throws
IOException {
+ return PipeSubscribeHandshakeReq.toTPipeSubscribeReq(new
ConsumerConfig(new HashMap<>()));
+ }
+
private static class FakeSubscriptionReceiver implements
SubscriptionReceiver {
private final boolean closeOnTimeout;
+ private final boolean assignDefaultIdentity;
+ private final CountDownLatch handshakeEntered;
+ private final CountDownLatch releaseHandshake;
private final AtomicInteger timeoutCount = new AtomicInteger();
private final AtomicInteger exitCount = new AtomicInteger();
- private ConsumerConfig consumerConfig;
- private boolean invalidated;
+ private volatile ConsumerConfig consumerConfig;
+ private volatile boolean invalidated;
private FakeSubscriptionReceiver(final boolean closeOnTimeout) {
+ this(closeOnTimeout, false, null, null);
+ }
+
+ private FakeSubscriptionReceiver(
+ final boolean closeOnTimeout,
+ final boolean assignDefaultIdentity,
+ final CountDownLatch handshakeEntered,
+ final CountDownLatch releaseHandshake) {
this.closeOnTimeout = closeOnTimeout;
+ this.assignDefaultIdentity = assignDefaultIdentity;
+ this.handshakeEntered = handshakeEntered;
+ this.releaseHandshake = releaseHandshake;
}
@Override
public TPipeSubscribeResp handle(final TPipeSubscribeReq req) {
if (req.getType() == PipeSubscribeRequestType.HANDSHAKE.getType()) {
consumerConfig = ConsumerConfig.deserialize(req.bufferForBody());
+ if (assignDefaultIdentity) {
+ consumerConfig.setConsumerGroupId("group");
+ consumerConfig.setConsumerId("consumer");
+ }
+ if (handshakeEntered != null) {
+ handshakeEntered.countDown();
+ try {
+ Assert.assertTrue(releaseHandshake.await(10, TimeUnit.SECONDS));
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(e);
+ }
+ }
invalidated = false;
return response(TSStatusCode.SUCCESS_STATUS);
}
@@ -259,7 +609,7 @@ public class SubscriptionReceiverAgentTest {
return response(TSStatusCode.SUCCESS_STATUS);
}
return response(
- invalidated ? TSStatusCode.SUBSCRIPTION_MISSING_CONSUMER :
TSStatusCode.SUCCESS_STATUS);
+ invalidated ? TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED :
TSStatusCode.SUCCESS_STATUS);
}
@Override
@@ -296,6 +646,11 @@ public class SubscriptionReceiverAgentTest {
return consumerConfig == null ? null :
consumerConfig.getConsumerGroupId();
}
+ @Override
+ public String getConsumerInstanceId() {
+ return consumerConfig == null ? null :
consumerConfig.getConsumerInstanceId();
+ }
+
@Override
public void invalidateConsumer() {
consumerConfig = null;
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
index 21d2e7b67d8..807d7231514 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
@@ -26,6 +26,8 @@ import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.iotdb.rpc.subscription.config.ConsumerConfig;
import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
import org.apache.iotdb.rpc.subscription.config.TopicConstant;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeHandshakeReq;
+import
org.apache.iotdb.rpc.subscription.payload.request.SubscriptionHeartbeatReq;
import org.junit.Assert;
import org.junit.Test;
@@ -159,6 +161,45 @@ public class SubscriptionReceiverV1Test {
Assert.assertNull(consumerConfigThreadLocal.get());
}
+ @Test
+ public void testInvalidatedConsumerReturnsFencedStatus() throws Exception {
+ final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();
+ final ConsumerConfig consumerConfig = createConsumerConfig(1_000L);
+ setField(receiver, "sharedConsumerConfig", consumerConfig);
+
+ receiver.invalidateConsumer();
+
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode(),
+
receiver.handle(SubscriptionHeartbeatReq.toThriftReq()).getStatus().getCode());
+ }
+
+ @Test
+ public void testFencedConsumerCannotHandshakeAgain() throws Exception {
+ final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();
+ final ConsumerConfig consumerConfig = createConsumerConfig(1_000L);
+
+ setField(receiver, "sharedConsumerConfig", consumerConfig);
+ receiver.invalidateConsumer();
+
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_CONSUMER_FENCED.getStatusCode(),
+ receiver
+
.handle(PipeSubscribeHandshakeReq.toTPipeSubscribeReq(consumerConfig))
+ .getStatus()
+ .getCode());
+ Assert.assertTrue((boolean) getField(receiver, "consumerFenced"));
+ }
+
+ @Test
+ public void testNeverHandshakenConsumerStillReturnsMissingConsumerStatus() {
+ final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();
+
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_MISSING_CONSUMER.getStatusCode(),
+
receiver.handle(SubscriptionHeartbeatReq.toThriftReq()).getStatus().getCode());
+ }
+
@Test
public void testCalculateConsumerInactivityTimeoutUsesDefaultTimeout()
throws Exception {
final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();