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 76796db4782 [Subscription] Bound consumer close when server is
unresponsive (#18659)
76796db4782 is described below
commit 76796db4782c4f555367686f4a77dcf6a6ea976f
Author: Caideyipi <[email protected]>
AuthorDate: Thu Sep 17 11:43:00 2026 +0800
[Subscription] Bound consumer close when server is unresponsive (#18659)
---
.../apache/iotdb/session/SessionConnection.java | 26 ++++
.../iotdb/session/SessionConnectionTest.java | 19 +++
.../SubscriptionSessionConnection.java | 8 ++
.../base/AbstractSubscriptionConsumer.java | 23 ++--
.../base/AbstractSubscriptionProvider.java | 96 ++++++++++---
.../base/AbstractSubscriptionProviders.java | 38 +++++-
.../base/AbstractSubscriptionPullConsumer.java | 86 +++++++-----
.../base/AbstractSubscriptionPushConsumer.java | 20 ++-
.../SubscriptionSessionConnectionTimeoutTest.java | 148 +++++++++++++++++++++
.../base/SubscriptionConsumerLifecycleTest.java | 96 ++++++++++++-
10 files changed, 487 insertions(+), 73 deletions(-)
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
index 2e144448b58..6bcc41859a8 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
@@ -30,6 +30,7 @@ import org.apache.iotdb.rpc.RedirectException;
import org.apache.iotdb.rpc.RpcUtils;
import org.apache.iotdb.rpc.StatementExecutionException;
import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.TimeoutChangeableTransport;
import org.apache.iotdb.rpc.UrlUtils;
import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
import
org.apache.iotdb.service.rpc.thrift.TCreateTimeseriesUsingSchemaTemplateReq;
@@ -326,6 +327,31 @@ public class SessionConnection {
return client;
}
+ protected boolean setTransportTimeout(final int timeoutInMs) {
+ if (!(transport instanceof TimeoutChangeableTransport)) {
+ return false;
+ }
+
+ try {
+ ((TimeoutChangeableTransport) transport).setTimeout(timeoutInMs);
+ return true;
+ } catch (final RuntimeException ignored) {
+ return false;
+ }
+ }
+
+ protected void forceCloseTransport() {
+ if (transport == null) {
+ return;
+ }
+
+ try {
+ transport.close();
+ } catch (final RuntimeException ignored) {
+ // Best effort. The caller must still finish updating its lifecycle
state.
+ }
+ }
+
protected void setTimeZone(String zoneId)
throws StatementExecutionException, IoTDBConnectionException {
final TSStatus status =
diff --git
a/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionConnectionTest.java
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionConnectionTest.java
index fea78466a78..9faa0c6efb6 100644
---
a/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionConnectionTest.java
+++
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionConnectionTest.java
@@ -28,6 +28,7 @@ import org.apache.iotdb.rpc.IoTDBConnectionException;
import org.apache.iotdb.rpc.RedirectException;
import org.apache.iotdb.rpc.StatementExecutionException;
import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.TimeoutChangeableTFastFramedTransport;
import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
import
org.apache.iotdb.service.rpc.thrift.TCreateTimeseriesUsingSchemaTemplateReq;
import org.apache.iotdb.service.rpc.thrift.TSAppendSchemaTemplateReq;
@@ -205,6 +206,24 @@ public class SessionConnectionTest {
sessionConnection.setStorageGroup("root.test1");
}
+ @Test
+ public void testTransportLifecycleControls() {
+ final TimeoutChangeableTFastFramedTransport timeoutTransport =
+ Mockito.mock(TimeoutChangeableTFastFramedTransport.class);
+ Whitebox.setInternalState(sessionConnection, "transport",
timeoutTransport);
+
+ Assert.assertTrue(sessionConnection.setTransportTimeout(123));
+ Mockito.verify(timeoutTransport).setTimeout(123);
+
+ sessionConnection.forceCloseTransport();
+ Mockito.verify(timeoutTransport).close();
+ }
+
+ @Test
+ public void testSetTransportTimeoutOnUnsupportedTransport() {
+ Assert.assertFalse(sessionConnection.setTransportTimeout(123));
+ }
+
@Test
public void testDeleteStorageGroups()
throws IoTDBConnectionException, StatementExecutionException {
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
index 3f15f4dcf6a..df3de1331e9 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
@@ -58,4 +58,12 @@ public class SubscriptionSessionConnection extends
SessionConnection {
public TPipeSubscribeResp pipeSubscribe(final TPipeSubscribeReq req) throws
TException {
return client.pipeSubscribe(req);
}
+
+ public boolean setTimeout(final int timeoutInMs) {
+ return setTransportTimeout(timeoutInMs);
+ }
+
+ public void forceClose() {
+ forceCloseTransport();
+ }
}
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 73a51aa9923..8a2c9ff6334 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
@@ -338,24 +338,23 @@ abstract class AbstractSubscriptionConsumer implements
AutoCloseable {
submitEndpointsSyncer();
}
+ protected void prepareClose() {
+ providers.prepareClose();
+ }
+
@Override
- public synchronized void close() {
- if (isClosed.get()) {
+ public void close() {
+ if (!isClosed.compareAndSet(false, true)) {
return;
}
- // close subscription providers
- providers.acquireWriteLock();
- try {
- providers.closeProviders();
- } finally {
- providers.releaseWriteLock();
- }
-
- isClosed.set(true);
-
// mark is released to avoid reopening after closing
isReleased.set(true);
+
+ // 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();
}
boolean isClosed() {
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 cfe741e2d7e..92350097787 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
@@ -54,6 +54,7 @@ import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeHeartbeat
import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribePollResp;
import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeSubscribeResp;
import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeUnsubscribeResp;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeResp;
import org.apache.iotdb.session.AbstractSessionBuilder;
import org.apache.iotdb.session.subscription.SubscriptionSessionConnection;
@@ -70,11 +71,14 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
public abstract class AbstractSubscriptionProvider {
private static final Logger LOGGER =
LoggerFactory.getLogger(AbstractSubscriptionProvider.class);
+ private static final int CLOSE_TIMEOUT_IN_MS = 5_000;
+
private static final String STATUS_FORMATTER = "Status code is [%s], status
message is [%s].";
private static final String INTERNAL_ERROR_FORMATTER =
"Internal error occurred. " + STATUS_FORMATTER;
@@ -91,7 +95,9 @@ public abstract class AbstractSubscriptionProvider {
private final Long ownerEpoch;
private final AtomicBoolean isClosed = new AtomicBoolean(true);
+ private final AtomicBoolean isClosing = new AtomicBoolean(false);
private final AtomicBoolean isAvailable = new AtomicBoolean(false);
+ private final ReentrantLock rpcLock = new ReentrantLock();
private final TEndPoint endPoint;
private int dataNodeId;
@@ -149,6 +155,16 @@ public abstract class AbstractSubscriptionProvider {
return session.getSessionConnection();
}
+ private TPipeSubscribeResp pipeSubscribe(final TPipeSubscribeReq req)
+ throws TException, IoTDBConnectionException {
+ rpcLock.lock();
+ try {
+ return getSessionConnection().pipeSubscribe(req);
+ } finally {
+ rpcLock.unlock();
+ }
+ }
+
boolean isAvailable() {
return isAvailable.get();
}
@@ -233,7 +249,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
@@ -249,24 +265,72 @@ public abstract class AbstractSubscriptionProvider {
return PipeSubscribeHandshakeResp.fromTPipeSubscribeResp(resp);
}
- synchronized void close() throws SubscriptionException,
IoTDBConnectionException {
- if (isClosed.get()) {
+ void prepareClose() {
+ final SubscriptionSessionConnection connection;
+ try {
+ connection = getSessionConnection();
+ } catch (final IoTDBConnectionException | RuntimeException ignored) {
+ return;
+ }
+ if (connection == null) {
+ return;
+ }
+
+ if (!connection.setTimeout(CLOSE_TIMEOUT_IN_MS)) {
+ setUnavailable();
+ connection.forceClose();
+ return;
+ }
+
+ if (rpcLock.tryLock()) {
+ rpcLock.unlock();
+ } else {
+ // Changing SO_TIMEOUT does not affect a read that is already blocked.
Closing the transport
+ // is the only reliable way to make an in-flight RPC release the
provider immediately.
+ setUnavailable();
+ connection.forceClose();
+ }
+ }
+
+ void close() throws SubscriptionException, IoTDBConnectionException {
+ if (isClosed.get() || !isClosing.compareAndSet(false, true)) {
return;
}
+ SubscriptionSessionConnection connection = null;
try {
- closeInternal(); // throw SubscriptionException
+ connection = getSessionConnection();
+ if (!connection.setTimeout(CLOSE_TIMEOUT_IN_MS) || !rpcLock.tryLock()) {
+ connection.forceClose();
+ return;
+ }
+ try {
+ closeInternal(); // throw SubscriptionException
+ } finally {
+ rpcLock.unlock();
+ }
} finally {
- session.close(); // throw IoTDBConnectionException
- setUnavailable();
- isClosed.set(true);
+ try {
+ if (connection != null) {
+ connection.forceClose();
+ }
+ } finally {
+ try {
+ // The transport is already closed, so Session.close() only updates
local resources and
+ // cannot block on a second closeSession RPC.
+ session.close(); // throw IoTDBConnectionException
+ } finally {
+ setUnavailable();
+ isClosed.set(true);
+ }
+ }
}
}
void closeInternal() throws SubscriptionException {
final TPipeSubscribeResp resp;
try {
- resp =
getSessionConnection().pipeSubscribe(PipeSubscribeCloseReq.toTPipeSubscribeReq());
+ resp = pipeSubscribe(PipeSubscribeCloseReq.toTPipeSubscribeReq());
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
@@ -304,7 +368,7 @@ public abstract class AbstractSubscriptionProvider {
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
@@ -334,7 +398,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
@@ -367,7 +431,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
@@ -401,7 +465,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
LOGGER.warn(
SubscriptionMessages
@@ -431,7 +495,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
LOGGER.warn(
SubscriptionMessages
@@ -461,7 +525,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
LOGGER.warn(
SubscriptionMessages
@@ -532,7 +596,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
@@ -566,7 +630,7 @@ public abstract class AbstractSubscriptionProvider {
}
final TPipeSubscribeResp resp;
try {
- resp = getSessionConnection().pipeSubscribe(req);
+ resp = pipeSubscribe(req);
} catch (final TException | IoTDBConnectionException e) {
// Assume provider unavailable
LOGGER.warn(
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 d89fe998cdf..0f8e032bec2 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
@@ -38,6 +38,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
@@ -47,6 +48,7 @@ final class AbstractSubscriptionProviders {
private final SortedMap<Integer, AbstractSubscriptionProvider>
subscriptionProviders =
new ConcurrentSkipListMap<>();
+ private final AtomicBoolean isClosing = new AtomicBoolean(false);
private int nextDataNodeId = -1;
private final ReentrantReadWriteLock subscriptionProvidersLock = new
ReentrantReadWriteLock(true);
@@ -160,23 +162,51 @@ final class AbstractSubscriptionProviders {
nextDataNodeId = subscriptionProviders.firstKey();
}
- /** Caller should ensure that the method is called in the lock {@link
#acquireWriteLock()}. */
+ /** Detaches and closes the current providers. Terminal consumer close may
call this lock-free. */
void closeProviders() {
- for (final AbstractSubscriptionProvider provider : getAllProviders()) {
+ final List<AbstractSubscriptionProvider> providers = getAllProviders();
+ subscriptionProviders.clear();
+ for (final AbstractSubscriptionProvider provider : providers) {
try {
provider.close();
} catch (final Exception e) {
LOGGER.warn(SubscriptionMessages.PROVIDER_CLOSE_FAILED, provider, e,
e);
}
}
- subscriptionProviders.clear();
+ }
+
+ void prepareClose() {
+ isClosing.set(true);
+ for (final AbstractSubscriptionProvider provider : getAllProviders()) {
+ provider.prepareClose();
+ }
}
/** Caller should ensure that the method is called in the lock {@link
#acquireWriteLock()}. */
void addProvider(final int dataNodeId, final AbstractSubscriptionProvider
provider) {
+ if (isClosing.get()) {
+ closeProviderAddedDuringClosing(provider);
+ return;
+ }
+
+ subscriptionProviders.put(dataNodeId, provider);
+ if (isClosing.get()) {
+ subscriptionProviders.remove(dataNodeId, provider);
+ closeProviderAddedDuringClosing(provider);
+ return;
+ }
+
// the subscription provider is opened
LOGGER.info(SubscriptionMessages.ADD_NEW_PROVIDER, provider);
- subscriptionProviders.put(dataNodeId, provider);
+ }
+
+ private void closeProviderAddedDuringClosing(final
AbstractSubscriptionProvider provider) {
+ provider.prepareClose();
+ try {
+ provider.close();
+ } catch (final Exception e) {
+ LOGGER.warn(SubscriptionMessages.PROVIDER_CLOSE_FAILED, provider, e, e);
+ }
}
/** Caller should ensure that the method is called in the lock {@link
#acquireWriteLock()}. */
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 811b4228c1e..61d558cd829 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
@@ -87,6 +87,7 @@ public abstract class AbstractSubscriptionPullConsumer
extends AbstractSubscript
private final EmptyPollLogThrottler emptyPollLogThrottler = new
EmptyPollLogThrottler();
private final AtomicBoolean isClosed = new AtomicBoolean(true);
+ private final AtomicBoolean isClosing = new AtomicBoolean(false);
@Override
boolean isClosed() {
@@ -154,57 +155,70 @@ public abstract class AbstractSubscriptionPullConsumer
extends AbstractSubscript
}
@Override
- public synchronized void close() {
- if (isClosed.get()) {
+ public void close() {
+ if (isClosed.get() || !isClosing.compareAndSet(false, true)) {
return;
}
- if (!processors.isEmpty()) {
- if (autoCommit) {
- final List<SubscriptionMessage> drainedMessages =
drainProcessorPipeline();
- if (!drainedMessages.isEmpty()) {
+ try {
+ synchronized (this) {
+ if (isClosed.get()) {
+ return;
+ }
+
+ List<SubscriptionMessage> drainedProcessorMessages =
Collections.emptyList();
+ if (!processors.isEmpty()) {
+ drainedProcessorMessages = drainProcessorPipeline();
+ if (!autoCommit && !drainedProcessorMessages.isEmpty()) {
+ pendingDrainedMessages.addAll(drainedProcessorMessages);
+ }
+ }
+
+ // In manual-commit mode, preserve the existing retry contract:
validate before making the
+ // terminal close state visible so the caller can drain and commit,
then retry close().
+ if (!autoCommit) {
+ ensureNoManualBufferedMessagesOnClose();
+ }
+
+ // Publish the terminal state before touching the network. Workers
stop starting new RPCs,
+ // while prepareClose() bounds subsequent requests and interrupts any
read already blocked.
+ isClosed.set(true);
+ prepareClose();
+
+ if (autoCommit && !drainedProcessorMessages.isEmpty()) {
try {
- commitSync(drainedMessages);
+ commitSync(drainedProcessorMessages);
} catch (final SubscriptionException e) {
LOGGER.warn(
SubscriptionMessages.LOG_FAILED_COMMIT_DRAINED_PROCESSOR_MESSAGES_CLOSE_4264DB35,
e);
}
}
- } else {
- final List<SubscriptionMessage> drainedMessages =
drainProcessorPipeline();
- if (!drainedMessages.isEmpty()) {
- pendingDrainedMessages.addAll(drainedMessages);
- }
- ensureNoManualBufferedMessagesOnClose();
- }
- }
- if (autoCommit && !pendingDrainedMessages.isEmpty()) {
- final List<SubscriptionMessage> drainedMessages =
drainPendingDrainedMessages();
- if (!drainedMessages.isEmpty()) {
- try {
- commitSync(drainedMessages);
- } catch (final SubscriptionException e) {
- LOGGER.warn(
- SubscriptionMessages
-
.LOG_FAILED_COMMIT_PENDING_DRAINED_PROCESSOR_MESSAGES_CLOSE_644B5DDD,
- e);
+ if (autoCommit && !pendingDrainedMessages.isEmpty()) {
+ final List<SubscriptionMessage> drainedMessages =
drainPendingDrainedMessages();
+ if (!drainedMessages.isEmpty()) {
+ try {
+ commitSync(drainedMessages);
+ } catch (final SubscriptionException e) {
+ LOGGER.warn(
+ SubscriptionMessages
+
.LOG_FAILED_COMMIT_PENDING_DRAINED_PROCESSOR_MESSAGES_CLOSE_644B5DDD,
+ e);
+ }
+ }
}
- }
- }
- if (!autoCommit) {
- ensureNoManualBufferedMessagesOnClose();
- }
+ if (autoCommit) {
+ // commit all uncommitted messages
+ commitAllUncommittedMessages();
+ }
- if (autoCommit) {
- // commit all uncommitted messages
- commitAllUncommittedMessages();
+ super.close();
+ }
+ } finally {
+ isClosing.set(false);
}
-
- isClosed.set(true);
- super.close();
}
/////////////////////////////// poll & commit ///////////////////////////////
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 f3c7d38c4dc..a06a2e9cc4e 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
@@ -66,6 +66,7 @@ public abstract class AbstractSubscriptionPushConsumer
extends AbstractSubscript
private final EmptyPollLogThrottler emptyPollLogThrottler = new
EmptyPollLogThrottler();
private final AtomicBoolean isClosed = new AtomicBoolean(true);
+ private final AtomicBoolean isClosing = new AtomicBoolean(false);
protected AbstractSubscriptionPushConsumer(
final AbstractSubscriptionPushConsumerBuilder builder) {
@@ -142,13 +143,24 @@ public abstract class AbstractSubscriptionPushConsumer
extends AbstractSubscript
}
@Override
- public synchronized void close() {
- if (isClosed.get()) {
+ public void close() {
+ if (isClosed.get() || !isClosing.compareAndSet(false, true)) {
return;
}
- isClosed.set(true);
- super.close();
+ try {
+ synchronized (this) {
+ if (isClosed.get()) {
+ return;
+ }
+
+ isClosed.set(true);
+ prepareClose();
+ super.close();
+ }
+ } finally {
+ isClosing.set(false);
+ }
}
@Override
diff --git
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnectionTimeoutTest.java
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnectionTimeoutTest.java
new file mode 100644
index 00000000000..8e0446cd71c
--- /dev/null
+++
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnectionTimeoutTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.rpc.TElasticFramedTransport;
+import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeCloseReq;
+import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeResp;
+import org.apache.iotdb.service.rpc.thrift.TSOpenSessionResp;
+import org.apache.iotdb.service.rpc.thrift.TSProtocolVersion;
+
+import org.apache.thrift.TException;
+import org.apache.thrift.server.TSimpleServer;
+import org.apache.thrift.transport.TServerSocket;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.lang.reflect.Proxy;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class SubscriptionSessionConnectionTimeoutTest {
+
+ private static final int READ_TIMEOUT_MS = 200;
+
+ @Test(timeout = 10_000)
+ public void testPipeSubscribeReadTimeout() throws Exception {
+ final CountDownLatch pipeSubscribeStarted = new CountDownLatch(1);
+ final CountDownLatch releasePipeSubscribe = new CountDownLatch(1);
+ 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 "pipeSubscribe":
+ pipeSubscribeStarted.countDown();
+ releasePipeSubscribe.await(5, TimeUnit.SECONDS);
+ return new TPipeSubscribeResp(successStatus(), (byte) 1,
(short) 6);
+ case "closeSession":
+ return successStatus();
+ default:
+ return defaultValue(method.getReturnType());
+ }
+ });
+ final TSimpleServer server =
+ new TSimpleServer(
+ new TSimpleServer.Args(serverSocket)
+ .processor(new IClientRPCService.Processor<>(service))
+ .transportFactory(new TElasticFramedTransport.Factory()));
+ final Thread serverThread = new Thread(server::serve,
"subscription-timeout-test-server");
+ serverThread.setDaemon(true);
+ serverThread.start();
+
+ final SubscriptionSessionWrapper session =
+ new SubscriptionSessionWrapper(
+ new SubscriptionTreeSessionBuilder()
+ .host("127.0.0.1")
+ .port(socket.getLocalPort())
+ .connectionTimeoutInMs(0));
+ SubscriptionSessionConnection connection = null;
+ try {
+ session.open();
+ connection = session.getSessionConnection();
+ Assert.assertTrue(connection.setTimeout(READ_TIMEOUT_MS));
+
+ final long startNanos = System.nanoTime();
+ try {
+ connection.pipeSubscribe(PipeSubscribeCloseReq.toTPipeSubscribeReq());
+ Assert.fail("Expected the unresponsive pipeSubscribe RPC to time out");
+ } catch (final TException expected) {
+ final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()
- startNanos);
+ Assert.assertTrue("RPC did not honor the read timeout: " + elapsedMs,
elapsedMs < 3_000);
+ }
+ Assert.assertTrue(pipeSubscribeStarted.await(1, TimeUnit.SECONDS));
+ } finally {
+ if (connection != null) {
+ connection.forceClose();
+ }
+ session.close();
+ releasePipeSubscribe.countDown();
+ server.stop();
+ serverThread.join(2_000);
+ }
+ }
+
+ private static TSStatus successStatus() {
+ return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+ }
+
+ private static Object defaultValue(final Class<?> returnType) {
+ if (!returnType.isPrimitive() || returnType == Void.TYPE) {
+ return null;
+ }
+ if (returnType == Boolean.TYPE) {
+ return false;
+ }
+ if (returnType == Character.TYPE) {
+ return '\0';
+ }
+ if (returnType == Byte.TYPE) {
+ return (byte) 0;
+ }
+ if (returnType == Short.TYPE) {
+ return (short) 0;
+ }
+ if (returnType == Integer.TYPE) {
+ return 0;
+ }
+ if (returnType == Long.TYPE) {
+ return 0L;
+ }
+ if (returnType == Float.TYPE) {
+ return 0.0F;
+ }
+ return 0.0D;
+ }
+}
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 68f714f2ce5..df42e2b244c 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
@@ -32,6 +32,11 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+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.function.BooleanSupplier;
public class SubscriptionConsumerLifecycleTest {
@@ -92,6 +97,34 @@ public class SubscriptionConsumerLifecycleTest {
Assert.assertTrue(consumer.closedStatesDuringClose.get(0));
}
+ @Test
+ public void testConcurrentPullConsumerCloseReturnsWithoutWaiting() throws
Exception {
+ final CountDownLatch providerCloseStarted = new CountDownLatch(1);
+ final CountDownLatch allowProviderClose = new CountDownLatch(1);
+ final TestPullConsumer consumer =
+ new TestPullConsumer(providerCloseStarted, allowProviderClose);
+ final ExecutorService executor = Executors.newFixedThreadPool(2);
+
+ try {
+ consumer.open();
+
+ final Future<?> firstClose = executor.submit(consumer::close);
+ Assert.assertTrue(providerCloseStarted.await(5, TimeUnit.SECONDS));
+
+ final Future<?> concurrentClose = executor.submit(consumer::close);
+ concurrentClose.get(1, TimeUnit.SECONDS);
+ Assert.assertFalse(firstClose.isDone());
+
+ allowProviderClose.countDown();
+ firstClose.get(5, TimeUnit.SECONDS);
+ Assert.assertEquals(1, consumer.closedStatesDuringClose.size());
+ } finally {
+ allowProviderClose.countDown();
+ executor.shutdownNow();
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+ }
+ }
+
private static class TestPushConsumer extends
AbstractSubscriptionPushConsumer {
private final List<Boolean> closedStatesDuringHandshake = new
ArrayList<>();
@@ -144,8 +177,15 @@ public class SubscriptionConsumerLifecycleTest {
private final List<Boolean> closedStatesDuringHandshake = new
ArrayList<>();
private final List<Boolean> closedStatesDuringClose = new ArrayList<>();
+ private final CountDownLatch providerCloseStarted;
+ private final CountDownLatch allowProviderClose;
private TestPullConsumer() {
+ this(null, null);
+ }
+
+ private TestPullConsumer(
+ final CountDownLatch providerCloseStarted, final CountDownLatch
allowProviderClose) {
super(
new AbstractSubscriptionPullConsumerBuilder()
.host(HOST)
@@ -155,6 +195,8 @@ public class SubscriptionConsumerLifecycleTest {
.heartbeatIntervalMs(LONG_INTERVAL_MS)
.endpointsSyncIntervalMs(LONG_INTERVAL_MS)
.autoCommit(false));
+ this.providerCloseStarted = providerCloseStarted;
+ this.allowProviderClose = allowProviderClose;
}
@Override
@@ -184,7 +226,9 @@ public class SubscriptionConsumerLifecycleTest {
connectionTimeoutInMs,
this::isClosed,
closedStatesDuringHandshake,
- closedStatesDuringClose);
+ closedStatesDuringClose,
+ providerCloseStarted,
+ allowProviderClose);
}
}
@@ -193,6 +237,8 @@ public class SubscriptionConsumerLifecycleTest {
private final BooleanSupplier consumerClosedSupplier;
private final List<Boolean> closedStatesDuringHandshake;
private final List<Boolean> closedStatesDuringClose;
+ private final CountDownLatch providerCloseStarted;
+ private final CountDownLatch allowProviderClose;
private TestSubscriptionProvider(
final TEndPoint endPoint,
@@ -209,6 +255,42 @@ public class SubscriptionConsumerLifecycleTest {
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,
+ 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,
+ final CountDownLatch providerCloseStarted,
+ final CountDownLatch allowProviderClose) {
super(
endPoint,
username,
@@ -224,6 +306,8 @@ public class SubscriptionConsumerLifecycleTest {
this.consumerClosedSupplier = consumerClosedSupplier;
this.closedStatesDuringHandshake = closedStatesDuringHandshake;
this.closedStatesDuringClose = closedStatesDuringClose;
+ this.providerCloseStarted = providerCloseStarted;
+ this.allowProviderClose = allowProviderClose;
}
@Override
@@ -255,6 +339,16 @@ public class SubscriptionConsumerLifecycleTest {
@Override
synchronized void close() {
closedStatesDuringClose.add(consumerClosedSupplier.getAsBoolean());
+ if (Objects.nonNull(providerCloseStarted)) {
+ providerCloseStarted.countDown();
+ }
+ if (Objects.nonNull(allowProviderClose)) {
+ try {
+ allowProviderClose.await();
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
setUnavailable();
}