From 25c34c2bfe8ecb68172ef2dc351e49cbad42f62a Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 28 May 2026 05:53:30 +0000 Subject: [PATCH 01/14] Subscription: add topic owner epoch fencing --- .../local/IoTDBSubscriptionTopicOwnerIT.java | 150 ++++++++++++++++ .../org/apache/iotdb/rpc/TSStatusCode.java | 5 + .../subscription/config/ConsumerConfig.java | 10 ++ .../subscription/config/ConsumerConstant.java | 2 + .../subscription/config/TopicConstant.java | 4 + .../SubscriptionOwnerFencedException.java | 45 +++++ .../base/AbstractSubscriptionConsumer.java | 26 +++ .../AbstractSubscriptionConsumerBuilder.java | 23 +++ .../base/AbstractSubscriptionProvider.java | 24 +++ .../table/SubscriptionTableProvider.java | 4 + .../table/SubscriptionTablePullConsumer.java | 4 + .../SubscriptionTablePullConsumerBuilder.java | 18 ++ .../table/SubscriptionTablePushConsumer.java | 4 + .../SubscriptionTablePushConsumerBuilder.java | 18 ++ .../tree/SubscriptionTreeProvider.java | 4 + .../tree/SubscriptionTreePullConsumer.java | 21 +++ .../SubscriptionTreePullConsumerBuilder.java | 18 ++ .../tree/SubscriptionTreePushConsumer.java | 21 +++ .../SubscriptionTreePushConsumerBuilder.java | 18 ++ .../subscription/SubscriptionInfo.java | 8 + .../subscription/SubscriptionInfoTest.java | 70 ++++++++ .../agent/SubscriptionTopicAgent.java | 72 ++++++++ .../receiver/SubscriptionReceiverV1.java | 57 ++++++ .../receiver/SubscriptionReceiverV1Test.java | 94 ++++++++++ .../subscription/meta/topic/TopicMeta.java | 167 +++++++++++++++++- .../subscription/topic/TopicDeSerTest.java | 61 +++++++ 26 files changed, 946 insertions(+), 2 deletions(-) create mode 100644 integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java create mode 100644 iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionOwnerFencedException.java create mode 100644 iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java new file mode 100644 index 0000000000000..4e89755048665 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.subscription.it.local; + +import org.apache.iotdb.isession.ISession; +import org.apache.iotdb.it.env.EnvFactory; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.LocalStandaloneIT; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; +import org.apache.iotdb.rpc.subscription.exception.SubscriptionOwnerFencedException; +import org.apache.iotdb.session.subscription.SubscriptionTreeSession; +import org.apache.iotdb.session.subscription.consumer.tree.SubscriptionTreePullConsumer; +import org.apache.iotdb.session.subscription.payload.SubscriptionMessage; +import org.apache.iotdb.session.subscription.payload.SubscriptionRecordHandler; +import org.apache.iotdb.subscription.it.IoTDBSubscriptionITConstant; + +import org.apache.tsfile.read.query.dataset.ResultSet; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +@RunWith(IoTDBTestRunner.class) +@Category({LocalStandaloneIT.class}) +public class IoTDBSubscriptionTopicOwnerIT extends AbstractSubscriptionLocalIT { + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + } + + @Ignore + @Test + public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws Exception { + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + final String topicName = "topic_owner_fencing"; + + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Properties properties = new Properties(); + properties.put(TopicConstant.PATH_KEY, "root.topic_owner.**"); + properties.put(TopicConstant.START_TIME_KEY, "0"); + properties.put(TopicConstant.OWNER_ID_KEY, "sn2"); + properties.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + session.createTopic(topicName, properties); + } + + try { + try (final SubscriptionTreePullConsumer staleOwnerConsumer = + new SubscriptionTreePullConsumer.Builder() + .host(host) + .port(port) + .consumerId("stale_sn") + .consumerGroupId("topic_owner_group") + .ownerId("sn1") + .ownerEpoch(5L) + .autoCommit(false) + .buildPullConsumer()) { + staleOwnerConsumer.open(); + Assert.assertThrows( + SubscriptionOwnerFencedException.class, () -> staleOwnerConsumer.subscribe(topicName)); + } + + try (final SubscriptionTreePullConsumer currentOwnerConsumer = + new SubscriptionTreePullConsumer.Builder() + .host(host) + .port(port) + .consumerId("current_sn") + .consumerGroupId("topic_owner_group") + .ownerId("sn2") + .ownerEpoch(6L) + .autoCommit(false) + .buildPullConsumer()) { + currentOwnerConsumer.open(); + currentOwnerConsumer.subscribe(topicName); + + insertData(); + + final AtomicReference> polledMessages = + new AtomicReference<>(Collections.emptyList()); + IoTDBSubscriptionITConstant.AWAIT.untilAsserted( + () -> { + final List messages = + currentOwnerConsumer.poll(Duration.ofMillis(1000)); + polledMessages.set(messages); + Assert.assertFalse(messages.isEmpty()); + Assert.assertTrue(countRows(messages) > 0); + }); + + currentOwnerConsumer.commitSync(polledMessages.get()); + currentOwnerConsumer.unsubscribe(topicName); + } + } finally { + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + session.dropTopicIfExists(topicName); + } + } + } + + private void insertData() throws Exception { + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + for (int i = 0; i < 10; i++) { + session.executeNonQueryStatement( + String.format("insert into root.topic_owner.d1(time, s1) values (%s, %s)", i, i)); + } + session.executeNonQueryStatement("flush"); + } + } + + private static int countRows(final List messages) throws Exception { + int rowCount = 0; + for (final SubscriptionMessage message : messages) { + for (final ResultSet resultSet : message.getResultSets()) { + while (((SubscriptionRecordHandler.SubscriptionResultSet) resultSet).hasNext()) { + resultSet.next(); + rowCount++; + } + } + } + return rowCount; + } +} 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 e5014681fa724..97a70963d720c 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 @@ -316,6 +316,11 @@ public enum TSStatusCode { SHOW_SUBSCRIPTION_ERROR(1910), SUBSCRIPTION_PIPE_TIMEOUT_ERROR(1911), SUBSCRIPTION_NOT_ENABLED_ERROR(1912), + SUBSCRIPTION_OWNER_FENCED(1913), + SUBSCRIPTION_OWNER_REQUIRED(1914), + SUBSCRIPTION_OWNER_EPOCH_REQUIRED(1915), + SUBSCRIPTION_OWNER_LEASE_EXPIRED(1916), + SUBSCRIPTION_OWNER_EPOCH_CONFLICT(1917), // 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 3cb0087d6827e..13f2a9ee3fb03 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,16 @@ public String getConsumerGroupId() { return getString(ConsumerConstant.CONSUMER_GROUP_ID_KEY); } + public String getOwnerId() { + return getString(ConsumerConstant.OWNER_ID_KEY); + } + + public Long getOwnerEpoch() { + return hasAttribute(ConsumerConstant.OWNER_EPOCH_KEY) + ? getLong(ConsumerConstant.OWNER_EPOCH_KEY) + : null; + } + public String getUsername() { return getString(ConsumerConstant.USERNAME_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 90d2ea7a01fb0..3df95facf367c 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,8 @@ 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 OWNER_ID_KEY = "owner-id"; + public static final String OWNER_EPOCH_KEY = "owner-epoch"; public static final String HEARTBEAT_INTERVAL_MS_KEY = "heartbeat-interval-ms"; public static final long HEARTBEAT_INTERVAL_MS_DEFAULT_VALUE = 30_000L; diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java index 52c8e4de75221..73036684ccde9 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java @@ -61,6 +61,10 @@ public class TopicConstant { public static final String STRICT_KEY = "strict"; public static final String STRICT_DEFAULT_VALUE = "true"; + public static final String OWNER_ID_KEY = "owner-id"; + public static final String OWNER_EPOCH_KEY = "owner-epoch"; + public static final String OWNER_LEASE_EXPIRE_TIME_MS_KEY = "owner-lease-expire-time-ms"; + private TopicConstant() { throw new IllegalStateException(SubscriptionMessages.UTILITY_CLASS); } diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionOwnerFencedException.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionOwnerFencedException.java new file mode 100644 index 0000000000000..f00c3ba1dcb0c --- /dev/null +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/exception/SubscriptionOwnerFencedException.java @@ -0,0 +1,45 @@ +/* + * 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; + +public class SubscriptionOwnerFencedException extends SubscriptionRuntimeNonCriticalException { + + public SubscriptionOwnerFencedException(final String message) { + super(message); + } + + public SubscriptionOwnerFencedException(final String message, final Throwable cause) { + super(message, cause); + } + + @Override + public boolean equals(final Object obj) { + return obj instanceof SubscriptionOwnerFencedException + && Objects.equals(getMessage(), ((SubscriptionOwnerFencedException) obj).getMessage()) + && Objects.equals(getTimeStamp(), ((SubscriptionOwnerFencedException) 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 63e5a263f22b3..ed59935b7f2b4 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 @@ -25,6 +25,7 @@ import org.apache.iotdb.rpc.subscription.config.TopicConfig; import org.apache.iotdb.rpc.subscription.exception.SubscriptionConnectionException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionException; +import org.apache.iotdb.rpc.subscription.exception.SubscriptionOwnerFencedException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionPipeTimeoutException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionPollTimeoutException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionRuntimeCriticalException; @@ -105,6 +106,8 @@ abstract class AbstractSubscriptionConsumer implements AutoCloseable { protected String consumerId; protected String consumerGroupId; + protected String ownerId; + protected Long ownerEpoch; private final long heartbeatIntervalMs; private final long endpointsSyncIntervalMs; @@ -154,6 +157,14 @@ public String getConsumerGroupId() { return consumerGroupId; } + public String getOwnerId() { + return ownerId; + } + + public Long getOwnerEpoch() { + return ownerEpoch; + } + /////////////////////////////// ctor /////////////////////////////// protected AbstractSubscriptionConsumer(final AbstractSubscriptionConsumerBuilder builder) { @@ -183,6 +194,8 @@ protected AbstractSubscriptionConsumer(final AbstractSubscriptionConsumerBuilder this.consumerId = builder.consumerId; this.consumerGroupId = builder.consumerGroupId; + this.ownerId = builder.ownerId; + this.ownerEpoch = builder.ownerEpoch; this.heartbeatIntervalMs = builder.heartbeatIntervalMs; this.endpointsSyncIntervalMs = builder.endpointsSyncIntervalMs; @@ -213,6 +226,8 @@ protected AbstractSubscriptionConsumer( .encryptedPassword((String) properties.get(ConsumerConstant.ENCRYPTED_PASSWORD_KEY)) .consumerId((String) properties.get(ConsumerConstant.CONSUMER_ID_KEY)) .consumerGroupId((String) properties.get(ConsumerConstant.CONSUMER_GROUP_ID_KEY)) + .ownerId((String) properties.get(ConsumerConstant.OWNER_ID_KEY)) + .ownerEpoch((Long) properties.get(ConsumerConstant.OWNER_EPOCH_KEY)) .heartbeatIntervalMs( (Long) properties.getOrDefault( @@ -394,6 +409,8 @@ protected abstract AbstractSubscriptionProvider constructSubscriptionProvider( final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs); @@ -408,6 +425,8 @@ AbstractSubscriptionProvider constructProviderAndHandshake(final TEndPoint endPo this.encryptedPassword, this.consumerId, this.consumerGroupId, + this.ownerId, + this.ownerEpoch, this.thriftMaxFrameSize, this.heartbeatIntervalMs, this.connectionTimeoutInMs); @@ -1341,6 +1360,9 @@ private void subscribeWithRedirection(final Set topicNames) throws Subsc subscribedTopics = provider.subscribe(topicNames); return; } catch (final Exception e) { + if (e instanceof SubscriptionOwnerFencedException) { + throw (SubscriptionOwnerFencedException) e; + } if (e instanceof SubscriptionPipeTimeoutException) { // degrade exception to log for pipe timeout LOGGER.warn(e.getMessage()); @@ -1429,6 +1451,8 @@ protected Map coreReportMessage() { final Map result = new HashMap<>(); result.put("consumerId", consumerId); result.put("consumerGroupId", consumerGroupId); + result.put("ownerId", ownerId); + result.put("ownerEpoch", String.valueOf(ownerEpoch)); result.put("isClosed", isClosed.toString()); result.put("fileSaveDir", fileSaveDir); result.put( @@ -1443,6 +1467,8 @@ protected Map allReportMessage() { final Map result = new HashMap<>(); result.put("consumerId", consumerId); result.put("consumerGroupId", consumerGroupId); + result.put("ownerId", ownerId); + result.put("ownerEpoch", String.valueOf(ownerEpoch)); result.put("heartbeatIntervalMs", String.valueOf(heartbeatIntervalMs)); result.put("endpointsSyncIntervalMs", String.valueOf(endpointsSyncIntervalMs)); result.put("providers", providers.toString()); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumerBuilder.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumerBuilder.java index 991d237ed2b89..a0c4b421ed18b 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumerBuilder.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionConsumerBuilder.java @@ -40,6 +40,8 @@ public class AbstractSubscriptionConsumerBuilder { protected String consumerId; protected String consumerGroupId; + protected String ownerId; + protected Long ownerEpoch; protected long heartbeatIntervalMs = ConsumerConstant.HEARTBEAT_INTERVAL_MS_DEFAULT_VALUE; protected long endpointsSyncIntervalMs = @@ -111,6 +113,27 @@ public AbstractSubscriptionConsumerBuilder consumerGroupId( return this; } + public AbstractSubscriptionConsumerBuilder ownerId(@Nullable final String ownerId) { + if (Objects.isNull(ownerId)) { + return this; + } + this.ownerId = ownerId; + return this; + } + + public AbstractSubscriptionConsumerBuilder ownerEpoch(final long ownerEpoch) { + this.ownerEpoch = ownerEpoch; + return this; + } + + public AbstractSubscriptionConsumerBuilder ownerEpoch(@Nullable final Long ownerEpoch) { + if (Objects.isNull(ownerEpoch)) { + return this; + } + this.ownerEpoch = ownerEpoch; + return this; + } + public AbstractSubscriptionConsumerBuilder heartbeatIntervalMs(final long heartbeatIntervalMs) { this.heartbeatIntervalMs = Math.max(heartbeatIntervalMs, ConsumerConstant.HEARTBEAT_INTERVAL_MS_MIN_VALUE); 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 413c609abbff3..3751d019fe779 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 @@ -27,6 +27,7 @@ import org.apache.iotdb.rpc.subscription.config.TopicConfig; import org.apache.iotdb.rpc.subscription.exception.SubscriptionConnectionException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionException; +import org.apache.iotdb.rpc.subscription.exception.SubscriptionOwnerFencedException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionPipeTimeoutException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionRuntimeCriticalException; import org.apache.iotdb.rpc.subscription.exception.SubscriptionRuntimeNonCriticalException; @@ -81,6 +82,8 @@ public abstract class AbstractSubscriptionProvider { private String consumerId; private String consumerGroupId; + private final String ownerId; + private final Long ownerEpoch; private final AtomicBoolean isClosed = new AtomicBoolean(true); private final AtomicBoolean isAvailable = new AtomicBoolean(false); @@ -109,6 +112,8 @@ protected AbstractSubscriptionProvider( final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -124,6 +129,8 @@ protected AbstractSubscriptionProvider( this.endPoint = endPoint; this.consumerId = consumerId; this.consumerGroupId = consumerGroupId; + this.ownerId = ownerId; + this.ownerEpoch = ownerEpoch; this.username = username; this.password = password; this.encryptedPassword = encryptedPassword; @@ -176,6 +183,12 @@ synchronized void handshake() throws SubscriptionException, IoTDBConnectionExcep final Map consumerAttributes = new HashMap<>(); consumerAttributes.put(ConsumerConstant.CONSUMER_GROUP_ID_KEY, consumerGroupId); consumerAttributes.put(ConsumerConstant.CONSUMER_ID_KEY, consumerId); + if (ownerId != null) { + consumerAttributes.put(ConsumerConstant.OWNER_ID_KEY, ownerId); + } + if (ownerEpoch != null) { + consumerAttributes.put(ConsumerConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + } consumerAttributes.put(ConsumerConstant.USERNAME_KEY, username); consumerAttributes.put(ConsumerConstant.PASSWORD_KEY, password); if (encryptedPassword != null) { @@ -451,6 +464,17 @@ private static void verifyPipeSubscribeSuccess(final TSStatus status) case 1911: // SUBSCRIPTION_PIPE_TIMEOUT_ERROR throw new SubscriptionPipeTimeoutException( String.format(SUBSCRIPTION_PIPE_TIMEOUT_FORMATTER, status.code, status.message)); + case 1913: // SUBSCRIPTION_OWNER_FENCED + case 1914: // SUBSCRIPTION_OWNER_REQUIRED + case 1915: // SUBSCRIPTION_OWNER_EPOCH_REQUIRED + case 1916: // SUBSCRIPTION_OWNER_LEASE_EXPIRED + case 1917: // SUBSCRIPTION_OWNER_EPOCH_CONFLICT + { + final String errorMessage = + String.format(INTERNAL_ERROR_FORMATTER, status.code, status.message); + LOGGER.warn(errorMessage); + throw new SubscriptionOwnerFencedException(errorMessage); + } case 1900: // SUBSCRIPTION_VERSION_ERROR case 1901: // SUBSCRIPTION_TYPE_ERROR case 1909: // SUBSCRIPTION_MISSING_CUSTOMER diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTableProvider.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTableProvider.java index 84470d283c21b..ff67e3e532c3d 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTableProvider.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTableProvider.java @@ -33,6 +33,8 @@ final class SubscriptionTableProvider extends AbstractSubscriptionProvider { final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -43,6 +45,8 @@ final class SubscriptionTableProvider extends AbstractSubscriptionProvider { encryptedPassword, consumerId, consumerGroupId, + ownerId, + ownerEpoch, thriftMaxFrameSize, heartbeatIntervalMs, connectionTimeoutInMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumer.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumer.java index 8f712782fb5f0..4c390c96420e3 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumer.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumer.java @@ -45,6 +45,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -55,6 +57,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( encryptedPassword, consumerId, consumerGroupId, + ownerId, + ownerEpoch, thriftMaxFrameSize, heartbeatIntervalMs, connectionTimeoutInMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumerBuilder.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumerBuilder.java index 939228a7f49e9..efc99debb4977 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumerBuilder.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePullConsumerBuilder.java @@ -74,6 +74,24 @@ public SubscriptionTablePullConsumerBuilder consumerGroupId(final String consume return this; } + @Override + public SubscriptionTablePullConsumerBuilder ownerId(final String ownerId) { + super.ownerId(ownerId); + return this; + } + + @Override + public SubscriptionTablePullConsumerBuilder ownerEpoch(final long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + + @Override + public SubscriptionTablePullConsumerBuilder ownerEpoch(final Long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + @Override public SubscriptionTablePullConsumerBuilder heartbeatIntervalMs(final long heartbeatIntervalMs) { super.heartbeatIntervalMs(heartbeatIntervalMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumer.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumer.java index e90afc1d8d175..4c85993a933dc 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumer.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumer.java @@ -41,6 +41,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -51,6 +53,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( encryptedPassword, consumerId, consumerGroupId, + ownerId, + ownerEpoch, thriftMaxFrameSize, heartbeatIntervalMs, connectionTimeoutInMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumerBuilder.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumerBuilder.java index 27bf328fea9e6..143b34056af9e 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumerBuilder.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/table/SubscriptionTablePushConsumerBuilder.java @@ -76,6 +76,24 @@ public SubscriptionTablePushConsumerBuilder consumerGroupId(final String consume return this; } + @Override + public SubscriptionTablePushConsumerBuilder ownerId(final String ownerId) { + super.ownerId(ownerId); + return this; + } + + @Override + public SubscriptionTablePushConsumerBuilder ownerEpoch(final long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + + @Override + public SubscriptionTablePushConsumerBuilder ownerEpoch(final Long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + @Override public SubscriptionTablePushConsumerBuilder heartbeatIntervalMs(final long heartbeatIntervalMs) { super.heartbeatIntervalMs(heartbeatIntervalMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreeProvider.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreeProvider.java index 3589fbbcf749a..8720c577d4dfa 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreeProvider.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreeProvider.java @@ -33,6 +33,8 @@ final class SubscriptionTreeProvider extends AbstractSubscriptionProvider { final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -43,6 +45,8 @@ final class SubscriptionTreeProvider extends AbstractSubscriptionProvider { encryptedPassword, consumerId, consumerGroupId, + ownerId, + ownerEpoch, thriftMaxFrameSize, heartbeatIntervalMs, connectionTimeoutInMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumer.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumer.java index 7225036aaa4cf..d4d615130625b 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumer.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumer.java @@ -52,6 +52,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -62,6 +64,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( encryptedPassword, consumerId, consumerGroupId, + ownerId, + ownerEpoch, thriftMaxFrameSize, heartbeatIntervalMs, connectionTimeoutInMs); @@ -85,6 +89,8 @@ private SubscriptionTreePullConsumer(final SubscriptionTreePullConsumer.Builder .encryptedPassword(builder.encryptedPassword) .consumerId(builder.consumerId) .consumerGroupId(builder.consumerGroupId) + .ownerId(builder.ownerId) + .ownerEpoch(builder.ownerEpoch) .heartbeatIntervalMs(builder.heartbeatIntervalMs) .endpointsSyncIntervalMs(builder.endpointsSyncIntervalMs) .fileSaveDir(builder.fileSaveDir) @@ -238,6 +244,8 @@ public static class Builder { private String consumerId; private String consumerGroupId; + private String ownerId; + private Long ownerEpoch; private long heartbeatIntervalMs = ConsumerConstant.HEARTBEAT_INTERVAL_MS_DEFAULT_VALUE; private long endpointsSyncIntervalMs = @@ -299,6 +307,19 @@ public Builder consumerGroupId(@Nullable final String consumerGroupId) { return this; } + public Builder ownerId(@Nullable final String ownerId) { + if (Objects.isNull(ownerId)) { + return this; + } + this.ownerId = ownerId; + return this; + } + + public Builder ownerEpoch(final long ownerEpoch) { + this.ownerEpoch = ownerEpoch; + return this; + } + public Builder heartbeatIntervalMs(final long heartbeatIntervalMs) { this.heartbeatIntervalMs = Math.max(heartbeatIntervalMs, ConsumerConstant.HEARTBEAT_INTERVAL_MS_MIN_VALUE); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumerBuilder.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumerBuilder.java index cbceb95d77f90..2d057b7bfbd80 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumerBuilder.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePullConsumerBuilder.java @@ -74,6 +74,24 @@ public SubscriptionTreePullConsumerBuilder consumerGroupId(final String consumer return this; } + @Override + public SubscriptionTreePullConsumerBuilder ownerId(final String ownerId) { + super.ownerId(ownerId); + return this; + } + + @Override + public SubscriptionTreePullConsumerBuilder ownerEpoch(final long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + + @Override + public SubscriptionTreePullConsumerBuilder ownerEpoch(final Long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + @Override public SubscriptionTreePullConsumerBuilder heartbeatIntervalMs(final long heartbeatIntervalMs) { super.heartbeatIntervalMs(heartbeatIntervalMs); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumer.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumer.java index 4d8a5ef3e169f..8fbc33d14ce8a 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumer.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumer.java @@ -51,6 +51,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( final String encryptedPassword, final String consumerId, final String consumerGroupId, + final String ownerId, + final Long ownerEpoch, final int thriftMaxFrameSize, final long heartbeatIntervalMs, final int connectionTimeoutInMs) { @@ -61,6 +63,8 @@ protected AbstractSubscriptionProvider constructSubscriptionProvider( encryptedPassword, consumerId, consumerGroupId, + ownerId, + ownerEpoch, thriftMaxFrameSize, heartbeatIntervalMs, connectionTimeoutInMs); @@ -84,6 +88,8 @@ private SubscriptionTreePushConsumer(final Builder builder) { .encryptedPassword(builder.encryptedPassword) .consumerId(builder.consumerId) .consumerGroupId(builder.consumerGroupId) + .ownerId(builder.ownerId) + .ownerEpoch(builder.ownerEpoch) .heartbeatIntervalMs(builder.heartbeatIntervalMs) .endpointsSyncIntervalMs(builder.endpointsSyncIntervalMs) .fileSaveDir(builder.fileSaveDir) @@ -192,6 +198,8 @@ public static class Builder { private String consumerId; private String consumerGroupId; + private String ownerId; + private Long ownerEpoch; private long heartbeatIntervalMs = ConsumerConstant.HEARTBEAT_INTERVAL_MS_DEFAULT_VALUE; private long endpointsSyncIntervalMs = @@ -256,6 +264,19 @@ public Builder consumerGroupId(@Nullable final String consumerGroupId) { return this; } + public Builder ownerId(@Nullable final String ownerId) { + if (Objects.isNull(ownerId)) { + return this; + } + this.ownerId = ownerId; + return this; + } + + public Builder ownerEpoch(final long ownerEpoch) { + this.ownerEpoch = ownerEpoch; + return this; + } + public Builder heartbeatIntervalMs(final long heartbeatIntervalMs) { this.heartbeatIntervalMs = Math.max(heartbeatIntervalMs, ConsumerConstant.HEARTBEAT_INTERVAL_MS_MIN_VALUE); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumerBuilder.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumerBuilder.java index 86594433e77e0..9067f3e7ae564 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumerBuilder.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/tree/SubscriptionTreePushConsumerBuilder.java @@ -76,6 +76,24 @@ public SubscriptionTreePushConsumerBuilder consumerGroupId(final String consumer return this; } + @Override + public SubscriptionTreePushConsumerBuilder ownerId(final String ownerId) { + super.ownerId(ownerId); + return this; + } + + @Override + public SubscriptionTreePushConsumerBuilder ownerEpoch(final long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + + @Override + public SubscriptionTreePushConsumerBuilder ownerEpoch(final Long ownerEpoch) { + super.ownerEpoch(ownerEpoch); + return this; + } + @Override public SubscriptionTreePushConsumerBuilder heartbeatIntervalMs(final long heartbeatIntervalMs) { super.heartbeatIntervalMs(heartbeatIntervalMs); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java index 6fcdcf28ebd0f..1849823888f0a 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java @@ -338,6 +338,14 @@ public TSStatus alterTopic(AlterTopicPlan plan) { } private TSStatus alterTopicInternal(final AlterTopicPlan plan) { + try { + TopicMeta.validateOwnerProgression( + topicMetaKeeper.getTopicMeta(plan.getTopicMeta().getTopicName()), plan.getTopicMeta()); + } catch (final IllegalArgumentException e) { + return new TSStatus(TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode()) + .setMessage(e.getMessage()); + } + topicMetaKeeper.removeTopicMeta(plan.getTopicMeta().getTopicName()); topicMetaKeeper.addTopicMeta(plan.getTopicMeta().getTopicName(), plan.getTopicMeta()); return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java new file mode 100644 index 0000000000000..768eab6ea50d8 --- /dev/null +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -0,0 +1,70 @@ +/* + * 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.confignode.persistence.subscription; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.AlterTopicPlan; +import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class SubscriptionInfoTest { + + @Test + public void testAlterTopicRejectsOwnerEpochRollback() { + final String topicName = "topic-" + UUID.randomUUID(); + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); + + final TopicMeta transferredTopicMeta = initialTopicMeta.deepCopy(); + transferredTopicMeta.transferOwner("sn2", 6L); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.alterTopic(new AlterTopicPlan(transferredTopicMeta)).getCode()); + + final TSStatus rollbackStatus = + subscriptionInfo.alterTopic(new AlterTopicPlan(createTopicMeta(topicName, "sn1", 5L))); + + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode(), rollbackStatus.getCode()); + Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + } + + private TopicMeta createTopicMeta( + final String topicName, final String ownerId, final long ownerEpoch) { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, ownerId); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + return new TopicMeta(topicName, 1, topicAttributes); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java index 9b629ab9c8dde..d524b38d4c13c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java @@ -19,10 +19,14 @@ package org.apache.iotdb.db.subscription.agent; +import org.apache.iotdb.common.rpc.thrift.TSStatus; import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; import org.apache.iotdb.commons.subscription.meta.topic.TopicMetaKeeper; import org.apache.iotdb.db.i18n.DataNodeMiscMessages; import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaRespExceptionMessage; +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.TopicConfig; import org.apache.iotdb.rpc.subscription.config.TopicConstant; @@ -31,6 +35,7 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -87,6 +92,8 @@ public TPushTopicMetaRespExceptionMessage handleSingleTopicMetaChanges( private void handleSingleTopicMetaChangesInternal(final TopicMeta metaFromCoordinator) { final String topicName = metaFromCoordinator.getTopicName(); + TopicMeta.validateOwnerProgression( + topicMetaKeeper.getTopicMeta(topicName), metaFromCoordinator); topicMetaKeeper.removeTopicMeta(topicName); topicMetaKeeper.addTopicMeta(topicName, metaFromCoordinator); } @@ -188,4 +195,69 @@ public Map getTopicConfigs(final Set topicNames) { releaseReadLock(); } } + + public TSStatus checkTopicOwner(final ConsumerConfig consumerConfig, final String topicName) { + acquireReadLock(); + try { + final TopicMeta topicMeta = topicMetaKeeper.getTopicMeta(topicName); + if (Objects.isNull(topicMeta) || !topicMeta.isOwnerFencingEnabled()) { + return RpcUtils.SUCCESS_STATUS; + } + + final String requestOwnerId = consumerConfig.getOwnerId(); + if (Objects.isNull(requestOwnerId)) { + return RpcUtils.getStatus( + TSStatusCode.SUBSCRIPTION_OWNER_REQUIRED, + String.format( + "Subscription: topic %s enables owner fencing, but consumer %s does not carry owner-id.", + topicName, consumerConfig)); + } + + final Long requestOwnerEpoch = consumerConfig.getOwnerEpoch(); + if (Objects.isNull(requestOwnerEpoch)) { + return RpcUtils.getStatus( + TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_REQUIRED, + String.format( + "Subscription: topic %s enables owner fencing, but consumer %s does not carry owner-epoch.", + topicName, consumerConfig)); + } + + if (Objects.nonNull(topicMeta.getOwnerLeaseExpireTimeMs()) + && System.currentTimeMillis() > topicMeta.getOwnerLeaseExpireTimeMs()) { + return RpcUtils.getStatus( + TSStatusCode.SUBSCRIPTION_OWNER_LEASE_EXPIRED, + String.format( + "Subscription: owner lease for topic %s has expired, owner-id: %s, owner-epoch: %s.", + topicName, topicMeta.getOwnerId(), topicMeta.getOwnerEpoch())); + } + + if (!topicMeta.matchesOwner(requestOwnerId, requestOwnerEpoch)) { + return RpcUtils.getStatus( + TSStatusCode.SUBSCRIPTION_OWNER_FENCED, + String.format( + "Subscription: consumer owner is fenced for topic %s, request owner-id: %s," + + " request owner-epoch: %s, current owner-id: %s, current owner-epoch: %s.", + topicName, + requestOwnerId, + requestOwnerEpoch, + topicMeta.getOwnerId(), + topicMeta.getOwnerEpoch())); + } + + return RpcUtils.SUCCESS_STATUS; + } finally { + releaseReadLock(); + } + } + + public TSStatus checkTopicOwners( + final ConsumerConfig consumerConfig, final Iterable topicNames) { + for (final String topicName : topicNames) { + final TSStatus status = checkTopicOwner(consumerConfig, topicName); + if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return status; + } + } + return RpcUtils.SUCCESS_STATUS; + } } 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 2c4accecf45f0..fe08ffc43ca39 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 @@ -326,6 +326,16 @@ private TPipeSubscribeResp handlePipeSubscribeHeartbeatInternal( } // TODO: do something + final TSStatus ownerStatus = + SubscriptionAgent.topic() + .checkTopicOwners( + consumerConfig, + SubscriptionAgent.consumer() + .getTopicNamesSubscribedByConsumer( + consumerConfig.getConsumerGroupId(), consumerConfig.getConsumerId())); + if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribeHeartbeatResp.toTPipeSubscribeResp(ownerStatus); + } LOGGER.info(DataNodeMiscMessages.SUBSCRIPTION_CONSUMER_HEARTBEAT_SUCCESS, consumerConfig); @@ -406,6 +416,11 @@ private TPipeSubscribeResp handlePipeSubscribeSubscribeInternal( // subscribe topics final Set topicNames = req.getTopicNames(); + final TSStatus ownerStatus = + SubscriptionAgent.topic().checkTopicOwners(consumerConfig, topicNames); + if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribeSubscribeResp.toTPipeSubscribeResp(ownerStatus); + } subscribe(consumerConfig, topicNames); LOGGER.info( @@ -498,16 +513,48 @@ private TPipeSubscribeResp handlePipeSubscribePollInternal(final PipeSubscribePo if (SubscriptionPollRequestType.isValidatedRequestType(requestType)) { switch (SubscriptionPollRequestType.valueOf(requestType)) { case POLL: + final Set pollTopicNames = ((PollPayload) request.getPayload()).getTopicNames(); + final Set subscribedTopicNames = + SubscriptionAgent.consumer() + .getTopicNamesSubscribedByConsumer( + consumerConfig.getConsumerGroupId(), consumerConfig.getConsumerId()); + final Set topicNamesToCheck = new HashSet<>(pollTopicNames); + topicNamesToCheck.removeIf(topicName -> !subscribedTopicNames.contains(topicName)); + final TSStatus ownerStatus = + SubscriptionAgent.topic().checkTopicOwners(consumerConfig, topicNamesToCheck); + if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribePollResp.toTPipeSubscribeResp(ownerStatus, Collections.emptyList()); + } events = handlePipeSubscribePollRequest( consumerConfig, (PollPayload) request.getPayload(), maxBytes); break; case POLL_FILE: + final TSStatus tsFileOwnerStatus = + SubscriptionAgent.topic() + .checkTopicOwner( + consumerConfig, + ((PollFilePayload) request.getPayload()).getCommitContext().getTopicName()); + if (tsFileOwnerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribePollResp.toTPipeSubscribeResp( + tsFileOwnerStatus, Collections.emptyList()); + } events = handlePipeSubscribePollTsFileRequest( consumerConfig, (PollFilePayload) request.getPayload()); break; case POLL_TABLETS: + final TSStatus tabletsOwnerStatus = + SubscriptionAgent.topic() + .checkTopicOwner( + consumerConfig, + ((PollTabletsPayload) request.getPayload()) + .getCommitContext() + .getTopicName()); + if (tabletsOwnerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribePollResp.toTPipeSubscribeResp( + tabletsOwnerStatus, Collections.emptyList()); + } events = handlePipeSubscribePollTabletsRequest( consumerConfig, (PollTabletsPayload) request.getPayload()); @@ -666,6 +713,16 @@ private TPipeSubscribeResp handlePipeSubscribeCommitInternal(final PipeSubscribe // commit (ack or nack) final List commitContexts = req.getCommitContexts(); + final TSStatus ownerStatus = + SubscriptionAgent.topic() + .checkTopicOwners( + consumerConfig, + commitContexts.stream() + .map(SubscriptionCommitContext::getTopicName) + .collect(Collectors.toSet())); + if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribeCommitResp.toTPipeSubscribeResp(ownerStatus); + } final boolean nack = req.isNack(); final List successfulCommitContexts = SubscriptionAgent.broker().commit(consumerConfig, commitContexts, nack); 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 ba0070187e368..c28111ec9d110 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 @@ -20,14 +20,19 @@ package org.apache.iotdb.db.subscription.receiver; import org.apache.iotdb.commons.subscription.config.SubscriptionConfig; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.db.subscription.agent.SubscriptionAgent; +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.junit.Assert; import org.junit.Test; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -82,6 +87,76 @@ public void testCalculateConsumerInactivityTimeoutUsesHeartbeatMultiple() throws invokeCalculateConsumerInactivityTimeoutMs(receiver, createConsumerConfig(5_000L))); } + @Test + public void testTopicOwnerFencingStatus() { + final String topicName = "topic-" + UUID.randomUUID(); + + SubscriptionAgent.topic().handleSingleTopicMetaChanges(createTopicMeta(topicName, "sn1", 7L)); + try { + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + SubscriptionAgent.topic() + .checkTopicOwner(createConsumerConfig(1_000L, "sn1", 7L), topicName) + .getCode()); + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), + SubscriptionAgent.topic() + .checkTopicOwner(createConsumerConfig(1_000L, "sn2", 7L), topicName) + .getCode()); + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_REQUIRED.getStatusCode(), + SubscriptionAgent.topic() + .checkTopicOwner(createConsumerConfig(1_000L), topicName) + .getCode()); + } finally { + SubscriptionAgent.topic().handleDropTopic(topicName); + } + } + + @Test + public void testOldOwnerFencedAfterNetworkPartitionAndTopicOwnerTransfer() { + final String topicName = "topic-" + UUID.randomUUID(); + final TopicMeta topicMeta = createTopicMeta(topicName, "sn1", 5L); + final ConsumerConfig oldSnConsumer = createConsumerConfig(1_000L, "sn1", 5L); + final ConsumerConfig newSnConsumer = createConsumerConfig(1_000L, "sn2", 6L); + + SubscriptionAgent.topic().handleSingleTopicMetaChanges(topicMeta); + try { + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + SubscriptionAgent.topic().checkTopicOwner(oldSnConsumer, topicName).getCode()); + + final TopicMeta transferredTopicMeta = topicMeta.deepCopy(); + transferredTopicMeta.transferOwner("sn2", 6L); + SubscriptionAgent.topic().handleSingleTopicMetaChanges(transferredTopicMeta); + + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), + SubscriptionAgent.topic().checkTopicOwner(oldSnConsumer, topicName).getCode()); + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), + SubscriptionAgent.topic() + .checkTopicOwners(oldSnConsumer, Collections.singleton(topicName)) + .getCode()); + Assert.assertNotNull( + SubscriptionAgent.topic() + .handleSingleTopicMetaChanges(createTopicMeta(topicName, "sn1", 5L))); + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), + SubscriptionAgent.topic().checkTopicOwner(oldSnConsumer, topicName).getCode()); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + SubscriptionAgent.topic().checkTopicOwner(newSnConsumer, topicName).getCode()); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + SubscriptionAgent.topic() + .checkTopicOwners(newSnConsumer, Collections.singleton(topicName)) + .getCode()); + } finally { + SubscriptionAgent.topic().handleDropTopic(topicName); + } + } + private long invokeCalculateConsumerInactivityTimeoutMs( final SubscriptionReceiverV1 receiver, final ConsumerConfig consumerConfig) throws Exception { final Method method = @@ -91,11 +166,30 @@ private long invokeCalculateConsumerInactivityTimeoutMs( return (long) method.invoke(receiver, consumerConfig); } + private TopicMeta createTopicMeta( + final String topicName, final String ownerId, final long ownerEpoch) { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, ownerId); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + return new TopicMeta(topicName, 1, topicAttributes); + } + private ConsumerConfig createConsumerConfig(final long heartbeatIntervalMs) { + return createConsumerConfig(heartbeatIntervalMs, null, null); + } + + private ConsumerConfig createConsumerConfig( + final long heartbeatIntervalMs, final String ownerId, final Long ownerEpoch) { final Map attributes = new HashMap<>(); attributes.put(ConsumerConstant.CONSUMER_ID_KEY, "consumer-" + UUID.randomUUID()); attributes.put(ConsumerConstant.CONSUMER_GROUP_ID_KEY, "group-" + UUID.randomUUID()); attributes.put(ConsumerConstant.HEARTBEAT_INTERVAL_MS_KEY, String.valueOf(heartbeatIntervalMs)); + if (ownerId != null) { + attributes.put(ConsumerConstant.OWNER_ID_KEY, ownerId); + } + if (ownerEpoch != null) { + attributes.put(ConsumerConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + } return new ConsumerConfig(attributes); } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index badb77d6f486d..44222a5a697fc 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -24,6 +24,7 @@ import org.apache.iotdb.commons.pipe.datastructure.visibility.VisibilityUtils; import org.apache.iotdb.commons.utils.TestOnly; import org.apache.iotdb.rpc.subscription.config.TopicConfig; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.apache.tsfile.utils.PublicBAOS; import org.apache.tsfile.utils.ReadWriteIOUtils; @@ -45,11 +46,18 @@ public class TopicMeta { private long creationTime; // unit in ms private TopicConfig config; + private String ownerId; + private long ownerEpoch; + private long ownerLastTransferTimeMs; + private Long ownerLeaseExpireTimeMs; + // TODO: remove this variable later private Set subscribedConsumerGroupIds; // unused now private TopicMeta() { this.config = new TopicConfig(new HashMap<>()); + this.ownerEpoch = -1L; + this.ownerLastTransferTimeMs = -1L; this.subscribedConsumerGroupIds = new HashSet<>(); } @@ -59,6 +67,9 @@ public TopicMeta( this.topicName = topicName; this.creationTime = creationTime; this.config = new TopicConfig(topicAttributes); + this.ownerEpoch = -1L; + this.ownerLastTransferTimeMs = -1L; + initOwnerFromTopicAttributes(topicAttributes); this.subscribedConsumerGroupIds = new HashSet<>(); } @@ -68,6 +79,10 @@ public TopicMeta deepCopy() { copied.topicName = topicName; copied.creationTime = creationTime; copied.config = new TopicConfig(new HashMap<>(config.getAttribute())); + copied.ownerId = ownerId; + copied.ownerEpoch = ownerEpoch; + copied.ownerLastTransferTimeMs = ownerLastTransferTimeMs; + copied.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; copied.subscribedConsumerGroupIds = new HashSet<>(subscribedConsumerGroupIds); return copied; @@ -85,6 +100,81 @@ public TopicConfig getConfig() { return config; } + public boolean isOwnerFencingEnabled() { + return Objects.nonNull(ownerId) && ownerEpoch >= 0; + } + + public String getOwnerId() { + return ownerId; + } + + public long getOwnerEpoch() { + return ownerEpoch; + } + + public long getOwnerLastTransferTimeMs() { + return ownerLastTransferTimeMs; + } + + public Long getOwnerLeaseExpireTimeMs() { + return ownerLeaseExpireTimeMs; + } + + public void transferOwner(final String ownerId, final long ownerEpoch) { + transferOwner(ownerId, ownerEpoch, null); + } + + public void transferOwner( + final String ownerId, final long ownerEpoch, final Long ownerLeaseExpireTimeMs) { + if (Objects.isNull(ownerId) || ownerId.isEmpty()) { + throw new IllegalArgumentException("Subscription topic owner id should not be empty"); + } + if (ownerEpoch < 0) { + throw new IllegalArgumentException("Subscription topic owner epoch should not be negative"); + } + if (isOwnerFencingEnabled() && ownerEpoch <= this.ownerEpoch) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner epoch should increase monotonically, current epoch is %s," + + " incoming epoch is %s", + this.ownerEpoch, ownerEpoch)); + } + + this.ownerId = ownerId; + this.ownerEpoch = ownerEpoch; + this.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; + this.ownerLastTransferTimeMs = System.currentTimeMillis(); + + config.getAttribute().put(TopicConstant.OWNER_ID_KEY, ownerId); + config.getAttribute().put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + if (Objects.nonNull(ownerLeaseExpireTimeMs)) { + config + .getAttribute() + .put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + } else { + config.getAttribute().remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + } + } + + public void clearOwner() { + ownerId = null; + ownerEpoch = -1L; + ownerLastTransferTimeMs = -1L; + ownerLeaseExpireTimeMs = null; + config.getAttribute().remove(TopicConstant.OWNER_ID_KEY); + config.getAttribute().remove(TopicConstant.OWNER_EPOCH_KEY); + config.getAttribute().remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + } + + public boolean matchesOwner(final String requestOwnerId, final Long requestOwnerEpoch) { + return !isOwnerFencingEnabled() + || (Objects.equals(ownerId, requestOwnerId) + && Objects.equals(ownerEpoch, requestOwnerEpoch) + && (Objects.isNull(ownerLeaseExpireTimeMs) + || System.currentTimeMillis() <= ownerLeaseExpireTimeMs)); + } + /** * @return true if the consumer group did not already subscribe this topic */ @@ -156,6 +246,8 @@ public static TopicMeta deserialize(final InputStream inputStream) throws IOExce topicMeta.subscribedConsumerGroupIds.add(ReadWriteIOUtils.readString(inputStream)); } + topicMeta.initOwnerFromTopicAttributes(topicMeta.config.getAttribute()); + return topicMeta; } @@ -177,9 +269,68 @@ public static TopicMeta deserialize(final ByteBuffer byteBuffer) { topicMeta.subscribedConsumerGroupIds.add(ReadWriteIOUtils.readString(byteBuffer)); } + topicMeta.initOwnerFromTopicAttributes(topicMeta.config.getAttribute()); + return topicMeta; } + public static void validateOwnerProgression( + final TopicMeta currentTopicMeta, final TopicMeta updatedTopicMeta) { + if (Objects.isNull(currentTopicMeta) + || Objects.isNull(updatedTopicMeta) + || !currentTopicMeta.isOwnerFencingEnabled()) { + return; + } + + if (!updatedTopicMeta.isOwnerFencingEnabled()) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner should not be cleared by stale topic meta, topic: %s," + + " current owner-id: %s, current owner-epoch: %s", + currentTopicMeta.getTopicName(), + currentTopicMeta.getOwnerId(), + currentTopicMeta.getOwnerEpoch())); + } + + final boolean epochRollback = + updatedTopicMeta.getOwnerEpoch() < currentTopicMeta.getOwnerEpoch(); + final boolean sameEpochOwnerChanged = + updatedTopicMeta.getOwnerEpoch() == currentTopicMeta.getOwnerEpoch() + && !Objects.equals(updatedTopicMeta.getOwnerId(), currentTopicMeta.getOwnerId()); + if (epochRollback || sameEpochOwnerChanged) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner epoch should not roll back, topic: %s, current owner-id:" + + " %s, current owner-epoch: %s, incoming owner-id: %s, incoming owner-epoch:" + + " %s", + currentTopicMeta.getTopicName(), + currentTopicMeta.getOwnerId(), + currentTopicMeta.getOwnerEpoch(), + updatedTopicMeta.getOwnerId(), + updatedTopicMeta.getOwnerEpoch())); + } + } + + private void initOwnerFromTopicAttributes(final Map topicAttributes) { + final TopicConfig topicConfig = new TopicConfig(topicAttributes); + final String configuredOwnerId = topicConfig.getString(TopicConstant.OWNER_ID_KEY); + if (Objects.isNull(configuredOwnerId)) { + return; + } + + final Long configuredOwnerEpoch = topicConfig.getLong(TopicConstant.OWNER_EPOCH_KEY); + if (Objects.isNull(configuredOwnerEpoch)) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner epoch should be set when %s is set", + TopicConstant.OWNER_ID_KEY)); + } + transferOwner( + configuredOwnerId, + configuredOwnerEpoch, + topicConfig.getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); + } + /////////////////////////////// utilities /////////////////////////////// public Map generateExtractorAttributes(final String username) { @@ -257,12 +408,16 @@ public boolean equals(final Object obj) { final TopicMeta that = (TopicMeta) obj; return creationTime == that.creationTime && Objects.equals(topicName, that.topicName) - && Objects.equals(config, that.config); + && Objects.equals(config, that.config) + && Objects.equals(ownerId, that.ownerId) + && ownerEpoch == that.ownerEpoch + && Objects.equals(ownerLeaseExpireTimeMs, that.ownerLeaseExpireTimeMs); } @Override public int hashCode() { - return Objects.hash(topicName, creationTime, config); + return Objects.hash( + topicName, creationTime, config, ownerId, ownerEpoch, ownerLeaseExpireTimeMs); } @Override @@ -274,6 +429,14 @@ public String toString() { + creationTime + ", config=" + config + + ", ownerId='" + + ownerId + + "', ownerEpoch=" + + ownerEpoch + + ", ownerLastTransferTimeMs=" + + ownerLastTransferTimeMs + + ", ownerLeaseExpireTimeMs=" + + ownerLeaseExpireTimeMs + '}'; } } diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java index d9c280e14938c..0511edff6203e 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java @@ -20,10 +20,13 @@ package org.apache.iotdb.commons.subscription.topic; import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.junit.Assert; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.HashMap; @@ -55,6 +58,64 @@ public void test() throws IOException { topicMeta.getSubscribedConsumerGroupIds(), topicMeta2.getSubscribedConsumerGroupIds()); } + @Test + public void testTopicOwnerDeSer() throws IOException { + Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + + TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + Assert.assertTrue(topicMeta.isOwnerFencingEnabled()); + Assert.assertEquals("sn1", topicMeta.getOwnerId()); + Assert.assertEquals(5L, topicMeta.getOwnerEpoch()); + Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); + Assert.assertFalse(topicMeta.matchesOwner("sn2", 5L)); + Assert.assertFalse(topicMeta.matchesOwner("sn1", 4L)); + + TopicMeta topicMeta1 = TopicMeta.deserialize(topicMeta.serialize()); + TopicMeta topicMeta2 = topicMeta1.deepCopy(); + + Assert.assertEquals(topicMeta, topicMeta1); + Assert.assertEquals(topicMeta, topicMeta2); + Assert.assertEquals(topicMeta.getOwnerId(), topicMeta2.getOwnerId()); + Assert.assertEquals(topicMeta.getOwnerEpoch(), topicMeta2.getOwnerEpoch()); + Assert.assertEquals( + topicMeta.getOwnerLeaseExpireTimeMs(), topicMeta2.getOwnerLeaseExpireTimeMs()); + + topicMeta.transferOwner("sn2", 6L, 100L); + Assert.assertEquals("sn2", topicMeta.getOwnerId()); + Assert.assertEquals(6L, topicMeta.getOwnerEpoch()); + Assert.assertEquals("sn2", topicMeta.getConfig().getString(TopicConstant.OWNER_ID_KEY)); + Assert.assertEquals( + 6L, topicMeta.getConfig().getLong(TopicConstant.OWNER_EPOCH_KEY).longValue()); + Assert.assertEquals( + 100L, + topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); + + topicMeta.clearOwner(); + Assert.assertFalse(topicMeta.isOwnerFencingEnabled()); + Assert.assertFalse(topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_ID_KEY)); + Assert.assertFalse(topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_EPOCH_KEY)); + Assert.assertFalse( + topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); + } + + @Test + public void testSequentialTopicMetaDeserializeDoesNotConsumeNextTopic() throws IOException { + final TopicMeta firstTopicMeta = new TopicMeta("first_topic", 1, new HashMap<>()); + final TopicMeta secondTopicMeta = new TopicMeta("second_topic", 2, new HashMap<>()); + + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + firstTopicMeta.serialize(outputStream); + secondTopicMeta.serialize(outputStream); + + final ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + Assert.assertEquals(firstTopicMeta, TopicMeta.deserialize(inputStream)); + Assert.assertEquals(secondTopicMeta, TopicMeta.deserialize(inputStream)); + Assert.assertEquals(0, inputStream.available()); + } + @Test public void testGenerateExtractorAttributesWithEncryptedPassword() { final TopicMeta topicMeta = new TopicMeta("test_topic", 1, new HashMap<>()); From 0ea029f71a490f4f2854bf734cef2f9292cf1780 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 4 Jun 2026 05:16:21 +0000 Subject: [PATCH 02/14] Implement alter topic owner entrypoints --- .../AbstractSubscriptionSession.java | 82 +++++++++++++---- .../ISubscriptionTableSession.java | 43 +++++++++ .../ISubscriptionTreeSession.java | 43 +++++++++ .../SubscriptionTableSession.java | 22 +++++ .../subscription/SubscriptionTreeSession.java | 22 +++++ .../apache/iotdb/db/qp/sql/IoTDBSqlParser.g4 | 6 +- .../confignode/manager/ConfigManager.java | 9 ++ .../iotdb/confignode/manager/IManager.java | 4 + .../confignode/manager/ProcedureManager.java | 32 +++++++ .../subscription/SubscriptionCoordinator.java | 21 +++++ .../thrift/ConfigNodeRPCServiceProcessor.java | 6 ++ .../subscription/SubscriptionInfoTest.java | 26 ++++++ .../db/protocol/client/ConfigNodeClient.java | 7 ++ .../config/TableConfigTaskVisitor.java | 13 +++ .../config/TreeConfigTaskVisitor.java | 12 +++ .../executor/ClusterConfigTaskExecutor.java | 30 ++++++ .../config/executor/IConfigTaskExecutor.java | 3 + .../sys/subscription/AlterTopicTask.java | 49 ++++++++++ .../queryengine/plan/parser/ASTVisitor.java | 18 ++++ .../analyzer/StatementAnalyzer.java | 6 ++ .../security/TreeAccessCheckVisitor.java | 7 ++ .../plan/relational/sql/ast/AlterTopic.java | 92 +++++++++++++++++++ .../plan/relational/sql/ast/AstVisitor.java | 4 + .../relational/sql/parser/AstBuilder.java | 8 ++ .../sql/util/DataNodeSqlFormatter.java | 26 ++++++ .../plan/statement/StatementType.java | 1 + .../plan/statement/StatementVisitor.java | 5 + .../subscription/AlterTopicStatement.java | 73 +++++++++++++++ .../plan/parser/StatementGeneratorTest.java | 15 +++ .../sql/parser/TopicStatementTest.java | 48 ++++++++++ .../subscription/meta/topic/TopicMeta.java | 11 +++ .../subscription/topic/TopicDeSerTest.java | 23 +++++ .../relational/grammar/sql/RelationalSql.g4 | 5 + .../src/main/thrift/confignode.thrift | 4 +- 34 files changed, 758 insertions(+), 18 deletions(-) create mode 100644 iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/subscription/AlterTopicTask.java create mode 100644 iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AlterTopic.java create mode 100644 iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/subscription/AlterTopicStatement.java create mode 100644 iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java index 6683a1c5adf2e..5bddd082e4e78 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java @@ -22,6 +22,7 @@ import org.apache.iotdb.isession.SessionDataSet; import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.apache.iotdb.rpc.subscription.exception.SubscriptionException; import org.apache.iotdb.session.subscription.model.Subscription; import org.apache.iotdb.session.subscription.model.Topic; @@ -103,27 +104,53 @@ private void createTopic( } return; } - final StringBuilder sb = new StringBuilder(); - sb.append('('); - properties.forEach( - (k, v) -> - sb.append('\'') - .append(k) - .append('\'') - .append('=') - .append('\'') - .append(v) - .append('\'') - .append(',')); - sb.deleteCharAt(sb.length() - 1); - sb.append(')'); final String sql = isSetIfNotExistsCondition - ? String.format("CREATE TOPIC IF NOT EXISTS %s WITH %s", topicName, sb) - : String.format("CREATE TOPIC %s WITH %s", topicName, sb); + ? String.format( + "CREATE TOPIC IF NOT EXISTS %s WITH %s", + topicName, buildTopicAttributesClause(properties)) + : String.format( + "CREATE TOPIC %s WITH %s", topicName, buildTopicAttributesClause(properties)); session.executeNonQueryStatement(sql); } + protected void alterTopic(final String topicName, final Properties properties) + throws IoTDBConnectionException, StatementExecutionException { + IdentifierUtils.checkAndParseIdentifier(topicName); // ignore the parse result + if (Objects.isNull(properties) || properties.isEmpty()) { + throw new StatementExecutionException("Topic attributes should not be empty in ALTER TOPIC."); + } + final String sql = + String.format("ALTER TOPIC %s WITH %s", topicName, buildTopicAttributesClause(properties)); + session.executeNonQueryStatement(sql); + } + + protected void alterTopicOwner( + final String topicName, final String ownerId, final long ownerEpoch) + throws IoTDBConnectionException, StatementExecutionException { + alterTopicOwner(topicName, ownerId, ownerEpoch, null); + } + + protected void alterTopicOwner( + final String topicName, + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseExpireTimeMs) + throws IoTDBConnectionException, StatementExecutionException { + if (Objects.isNull(ownerId) || ownerId.isEmpty()) { + throw new StatementExecutionException("Topic owner id should not be empty."); + } + + final Properties properties = new Properties(); + properties.put(TopicConstant.OWNER_ID_KEY, ownerId); + properties.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + if (Objects.nonNull(ownerLeaseExpireTimeMs)) { + properties.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + } + alterTopic(topicName, properties); + } + protected void dropTopic(final String topicName) throws IoTDBConnectionException, StatementExecutionException { IdentifierUtils.checkAndParseIdentifier(topicName); // ignore the parse result @@ -231,4 +258,27 @@ private Set convertDataSetToSubscriptions(final SessionDataSet dat } return subscriptions; } + + private static String buildTopicAttributesClause(final Properties properties) { + final StringBuilder builder = new StringBuilder(); + builder.append('('); + properties.forEach( + (key, value) -> + builder + .append('\'') + .append(escapeSqlStringLiteral(String.valueOf(key))) + .append('\'') + .append('=') + .append('\'') + .append(escapeSqlStringLiteral(String.valueOf(value))) + .append('\'') + .append(',')); + builder.deleteCharAt(builder.length() - 1); + builder.append(')'); + return builder.toString(); + } + + private static String escapeSqlStringLiteral(final String value) { + return value.replace("'", "''"); + } } diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java index 2dac0f5106b96..2ec7471127add 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java @@ -99,6 +99,49 @@ void createTopic(final String topicName, final Properties properties) void createTopicIfNotExists(final String topicName, final Properties properties) throws IoTDBConnectionException, StatementExecutionException; + /** + * Alters a topic with the specified properties. + * + * @param topicName If the topic name contains single quotes, the passed parameter needs to be + * enclosed in backticks. + * @param properties A {@link Properties} object containing the topic properties to alter. + * @throws IoTDBConnectionException If a connection issue occurs. + * @throws StatementExecutionException If the SQL statement execution fails. + */ + void alterTopic(final String topicName, final Properties properties) + throws IoTDBConnectionException, StatementExecutionException; + + /** + * Transfers a topic owner to the specified owner id and epoch. + * + * @param topicName If the topic name contains single quotes, the passed parameter needs to be + * enclosed in backticks. + * @param ownerId The new topic owner id. + * @param ownerEpoch The new monotonically increasing topic owner epoch. + * @throws IoTDBConnectionException If a connection issue occurs. + * @throws StatementExecutionException If the SQL statement execution fails. + */ + void alterTopicOwner(final String topicName, final String ownerId, final long ownerEpoch) + throws IoTDBConnectionException, StatementExecutionException; + + /** + * Transfers a topic owner to the specified owner id, epoch, and lease expire time. + * + * @param topicName If the topic name contains single quotes, the passed parameter needs to be + * enclosed in backticks. + * @param ownerId The new topic owner id. + * @param ownerEpoch The new monotonically increasing topic owner epoch. + * @param ownerLeaseExpireTimeMs The owner lease expire time in milliseconds. + * @throws IoTDBConnectionException If a connection issue occurs. + * @throws StatementExecutionException If the SQL statement execution fails. + */ + void alterTopicOwner( + final String topicName, + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseExpireTimeMs) + throws IoTDBConnectionException, StatementExecutionException; + /** * Drops the specified topic. * diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java index 799314769e8eb..7a827a207e843 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java @@ -99,6 +99,49 @@ void createTopic(final String topicName, final Properties properties) void createTopicIfNotExists(final String topicName, final Properties properties) throws IoTDBConnectionException, StatementExecutionException; + /** + * Alters a topic with the specified properties. + * + * @param topicName If the topic name contains single quotes, the passed parameter needs to be + * enclosed in backticks. + * @param properties A {@link Properties} object containing the topic properties to alter. + * @throws IoTDBConnectionException If a connection issue occurs. + * @throws StatementExecutionException If the SQL statement execution fails. + */ + void alterTopic(final String topicName, final Properties properties) + throws IoTDBConnectionException, StatementExecutionException; + + /** + * Transfers a topic owner to the specified owner id and epoch. + * + * @param topicName If the topic name contains single quotes, the passed parameter needs to be + * enclosed in backticks. + * @param ownerId The new topic owner id. + * @param ownerEpoch The new monotonically increasing topic owner epoch. + * @throws IoTDBConnectionException If a connection issue occurs. + * @throws StatementExecutionException If the SQL statement execution fails. + */ + void alterTopicOwner(final String topicName, final String ownerId, final long ownerEpoch) + throws IoTDBConnectionException, StatementExecutionException; + + /** + * Transfers a topic owner to the specified owner id, epoch, and lease expire time. + * + * @param topicName If the topic name contains single quotes, the passed parameter needs to be + * enclosed in backticks. + * @param ownerId The new topic owner id. + * @param ownerEpoch The new monotonically increasing topic owner epoch. + * @param ownerLeaseExpireTimeMs The owner lease expire time in milliseconds. + * @throws IoTDBConnectionException If a connection issue occurs. + * @throws StatementExecutionException If the SQL statement execution fails. + */ + void alterTopicOwner( + final String topicName, + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseExpireTimeMs) + throws IoTDBConnectionException, StatementExecutionException; + /** * Drops the specified topic. * diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java index bf21a43b26f81..b666297052d83 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java @@ -74,6 +74,28 @@ public void createTopicIfNotExists(final String topicName, final Properties prop super.createTopicIfNotExists(topicName, properties); } + @Override + public void alterTopic(final String topicName, final Properties properties) + throws IoTDBConnectionException, StatementExecutionException { + super.alterTopic(topicName, properties); + } + + @Override + public void alterTopicOwner(final String topicName, final String ownerId, final long ownerEpoch) + throws IoTDBConnectionException, StatementExecutionException { + super.alterTopicOwner(topicName, ownerId, ownerEpoch); + } + + @Override + public void alterTopicOwner( + final String topicName, + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseExpireTimeMs) + throws IoTDBConnectionException, StatementExecutionException { + super.alterTopicOwner(topicName, ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + } + @Override public void dropTopic(final String topicName) throws IoTDBConnectionException, StatementExecutionException { diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java index 900a243434d1c..3b921c5270a3e 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java @@ -97,6 +97,28 @@ public void createTopicIfNotExists(final String topicName, final Properties prop super.createTopicIfNotExists(topicName, properties); } + @Override + public void alterTopic(final String topicName, final Properties properties) + throws IoTDBConnectionException, StatementExecutionException { + super.alterTopic(topicName, properties); + } + + @Override + public void alterTopicOwner(final String topicName, final String ownerId, final long ownerEpoch) + throws IoTDBConnectionException, StatementExecutionException { + super.alterTopicOwner(topicName, ownerId, ownerEpoch); + } + + @Override + public void alterTopicOwner( + final String topicName, + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseExpireTimeMs) + throws IoTDBConnectionException, StatementExecutionException { + super.alterTopicOwner(topicName, ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + } + @Override public void dropTopic(final String topicName) throws IoTDBConnectionException, StatementExecutionException { diff --git a/iotdb-core/antlr/src/main/antlr4/org/apache/iotdb/db/qp/sql/IoTDBSqlParser.g4 b/iotdb-core/antlr/src/main/antlr4/org/apache/iotdb/db/qp/sql/IoTDBSqlParser.g4 index c6271c134cb00..186675d88a3c9 100644 --- a/iotdb-core/antlr/src/main/antlr4/org/apache/iotdb/db/qp/sql/IoTDBSqlParser.g4 +++ b/iotdb-core/antlr/src/main/antlr4/org/apache/iotdb/db/qp/sql/IoTDBSqlParser.g4 @@ -59,7 +59,7 @@ ddlStatement // Pipe Plugin | createPipePlugin | dropPipePlugin | showPipePlugins // Subscription - | createTopic | dropTopic | showTopics | showSubscriptions | dropSubscription + | createTopic | alterTopic | dropTopic | showTopics | showSubscriptions | dropSubscription // CQ | createContinuousQuery | dropContinuousQuery | showContinuousQueries // Cluster @@ -718,6 +718,10 @@ createTopic : CREATE TOPIC (IF NOT EXISTS)? topicName=identifier topicAttributesClause? ; +alterTopic + : ALTER TOPIC topicName=identifier topicAttributesClause + ; + topicAttributesClause : WITH LR_BRACKET topicAttributeClause (COMMA topicAttributeClause)* RR_BRACKET ; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java index 2db0255e35ac4..9e59bb9e51565 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java @@ -149,6 +149,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterSchemaTemplateReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterTimeSeriesReq; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TAuthizedPatternTreeResp; import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TClusterParameters; @@ -2459,6 +2460,14 @@ public TSStatus createTopic(TCreateTopicReq req) { : status; } + @Override + public TSStatus alterTopic(TAlterTopicReq req) { + TSStatus status = confirmLeader(); + return status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode() + ? subscriptionManager.getSubscriptionCoordinator().alterTopic(req) + : status; + } + @Override public TSStatus dropTopic(TDropTopicReq req) { TSStatus status = confirmLeader(); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java index 4dce39a9e98f0..fa708dbaeca83 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java @@ -70,6 +70,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterSchemaTemplateReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterTimeSeriesReq; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TConfigNodeRegisterReq; import org.apache.iotdb.confignode.rpc.thrift.TConfigNodeRegisterResp; @@ -803,6 +804,9 @@ TPermissionInfoResp login( /** Create Topic. */ TSStatus createTopic(TCreateTopicReq topic); + /** Alter Topic. */ + TSStatus alterTopic(TAlterTopicReq req); + /** Drop Topic. */ TSStatus dropTopic(TDropTopicReq req); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java index 3f0ba82fa768c..78e43ce8f470a 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java @@ -42,6 +42,7 @@ import org.apache.iotdb.commons.schema.template.Template; import org.apache.iotdb.commons.schema.view.viewExpression.ViewExpression; import org.apache.iotdb.commons.service.metric.MetricService; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; import org.apache.iotdb.commons.trigger.TriggerInformation; import org.apache.iotdb.commons.utils.StatusUtils; import org.apache.iotdb.commons.utils.TestOnly; @@ -119,6 +120,7 @@ import org.apache.iotdb.confignode.procedure.impl.subscription.consumer.runtime.ConsumerGroupMetaSyncProcedure; import org.apache.iotdb.confignode.procedure.impl.subscription.subscription.CreateSubscriptionProcedure; import org.apache.iotdb.confignode.procedure.impl.subscription.subscription.DropSubscriptionProcedure; +import org.apache.iotdb.confignode.procedure.impl.subscription.topic.AlterTopicProcedure; import org.apache.iotdb.confignode.procedure.impl.subscription.topic.CreateTopicProcedure; import org.apache.iotdb.confignode.procedure.impl.subscription.topic.DropTopicProcedure; import org.apache.iotdb.confignode.procedure.impl.subscription.topic.runtime.TopicMetaSyncProcedure; @@ -136,6 +138,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TAlterLogicalViewReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterOrDropTableReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TConfigNodeRegisterReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateCQReq; @@ -1733,6 +1736,35 @@ public TSStatus createTopic(TCreateTopicReq req) { } } + public TSStatus alterTopic(TAlterTopicReq req) { + try { + final TopicMeta updatedTopicMeta = + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .buildAlteredTopicMeta(req); + if (updatedTopicMeta == null) { + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage( + String.format( + "Failed to alter topic %s, the topic is not existed", req.getTopicName())); + } + + AlterTopicProcedure procedure = new AlterTopicProcedure(updatedTopicMeta); + executor.submitProcedure(procedure); + TSStatus status = waitingProcedureFinished(procedure); + if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return status; + } else { + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage(wrapTimeoutMessageForPipeProcedure(status.getMessage())); + } + } catch (Exception e) { + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage(e.getMessage()); + } + } + public TSStatus dropTopic(String topicName) { try { DropTopicProcedure procedure = new DropTopicProcedure(topicName); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index 83031570fa61e..5f0cabfff8a53 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -20,6 +20,7 @@ package org.apache.iotdb.confignode.manager.subscription; import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; import org.apache.iotdb.confignode.consensus.request.read.subscription.ShowSubscriptionPlan; import org.apache.iotdb.confignode.consensus.request.read.subscription.ShowTopicPlan; import org.apache.iotdb.confignode.consensus.response.subscription.SubscriptionTableResp; @@ -28,6 +29,7 @@ import org.apache.iotdb.confignode.manager.ConfigManager; import org.apache.iotdb.confignode.manager.pipe.coordinator.task.PipeTaskCoordinatorLock; import org.apache.iotdb.confignode.persistence.subscription.SubscriptionInfo; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateTopicReq; @@ -149,6 +151,25 @@ public TSStatus createTopic(TCreateTopicReq req) { return status; } + public TSStatus alterTopic(TAlterTopicReq req) { + final TSStatus status = configManager.getProcedureManager().alterTopic(req); + if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + LOGGER.warn( + "Failed to alter topic {} with attributes {}, result status is {}.", + req.getTopicName(), + req.getTopicAttributes(), + status); + } + return status; + } + + public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) { + final TopicMeta existedTopicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); + return existedTopicMeta == null + ? null + : existedTopicMeta.deepCopyWithUpdatedAttributes(req.getTopicAttributes()); + } + public TSStatus dropTopic(TDropTopicReq req) { final String topicName = req.getTopicName(); final boolean isSetIfExistsCondition = diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java index dbb35839045d0..4a4c4f59e2161 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java @@ -102,6 +102,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterSchemaTemplateReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterTimeSeriesReq; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TAuthizedPatternTreeResp; import org.apache.iotdb.confignode.rpc.thrift.TAuthorizerRelationalReq; import org.apache.iotdb.confignode.rpc.thrift.TAuthorizerReq; @@ -1268,6 +1269,11 @@ public TSStatus createTopic(TCreateTopicReq req) { return configManager.createTopic(req); } + @Override + public TSStatus alterTopic(TAlterTopicReq req) { + return configManager.alterTopic(req); + } + @Override public TSStatus dropTopic(String topicName) { return configManager.dropTopic( diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index 768eab6ea50d8..d78fe77af9d86 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -60,6 +60,32 @@ public void testAlterTopicRejectsOwnerEpochRollback() { Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); } + @Test + public void testAlterTopicTransfersOwnerWithUpdatedAttributes() { + final String topicName = "topic-" + UUID.randomUUID(); + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); + initialTopicMeta.getConfig().getAttribute().put(TopicConstant.PATH_KEY, "root.sg.**"); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); + + final Map updatedAttributes = new HashMap<>(); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TSStatus alterStatus = + subscriptionInfo.alterTopic( + new AlterTopicPlan(initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))); + + Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); + Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + Assert.assertEquals( + "root.sg.**", + subscriptionInfo.getTopicMeta(topicName).getConfig().getString(TopicConstant.PATH_KEY)); + } + private TopicMeta createTopicMeta( final String topicName, final String ownerId, final long ownerEpoch) { final Map topicAttributes = new HashMap<>(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java index 1f0b09f0b8689..7685864080164 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java @@ -59,6 +59,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterSchemaTemplateReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterTimeSeriesReq; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TAuthizedPatternTreeResp; import org.apache.iotdb.confignode.rpc.thrift.TAuthorizerRelationalReq; import org.apache.iotdb.confignode.rpc.thrift.TAuthorizerReq; @@ -1207,6 +1208,12 @@ public TSStatus createTopic(TCreateTopicReq req) throws TException { () -> client.createTopic(req), status -> !updateConfigNodeLeader(status)); } + @Override + public TSStatus alterTopic(TAlterTopicReq req) throws TException { + return executeRemoteCallWithRetry( + () -> client.alterTopic(req), status -> !updateConfigNodeLeader(status)); + } + @Override public TSStatus dropTopic(String topicName) throws TException { return executeRemoteCallWithRetry( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java index 827590e86a011..4eff93bfc12d2 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java @@ -140,6 +140,7 @@ import org.apache.iotdb.db.queryengine.plan.execution.config.sys.pipe.ShowPipeTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.pipe.StartPipeTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.pipe.StopPipeTask; +import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.AlterTopicTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.CreateTopicTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.DropSubscriptionTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.DropTopicTask; @@ -154,6 +155,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterColumnDataType; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterDB; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterPipe; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterTopic; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AstVisitor; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ClearCache; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ColumnDefinition; @@ -1392,6 +1394,17 @@ public IConfigTask visitCreateTopic(CreateTopic node, MPPQueryContext context) { return new CreateTopicTask(node); } + @Override + public IConfigTask visitAlterTopic(AlterTopic node, MPPQueryContext context) { + context.setQueryType(QueryType.OTHER); + accessControl.checkUserGlobalSysPrivilege(context); + + node.getTopicAttributes() + .put(SystemConstant.SQL_DIALECT_KEY, SystemConstant.SQL_DIALECT_TABLE_VALUE); + + return new AlterTopicTask(node); + } + @Override public IConfigTask visitDropTopic(DropTopic node, MPPQueryContext context) { context.setQueryType(QueryType.OTHER); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java index 8b7792bca985a..28745712d220c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java @@ -123,6 +123,7 @@ import org.apache.iotdb.db.queryengine.plan.execution.config.sys.quota.SetThrottleQuotaTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.quota.ShowSpaceQuotaTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.quota.ShowThrottleQuotaTask; +import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.AlterTopicTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.CreateTopicTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.DropSubscriptionTask; import org.apache.iotdb.db.queryengine.plan.execution.config.sys.subscription.DropTopicTask; @@ -192,6 +193,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.MigrateRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.ReconstructRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.RemoveRegionStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.CreateTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropSubscriptionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropTopicStatement; @@ -730,6 +732,16 @@ public IConfigTask visitCreateTopic( return new CreateTopicTask(createTopicStatement); } + @Override + public IConfigTask visitAlterTopic( + AlterTopicStatement alterTopicStatement, MPPQueryContext context) { + alterTopicStatement + .getTopicAttributes() + .put(SystemConstant.SQL_DIALECT_KEY, SystemConstant.SQL_DIALECT_TREE_VALUE); + + return new AlterTopicTask(alterTopicStatement); + } + @Override public IConfigTask visitDropTopic( DropTopicStatement dropTopicStatement, MPPQueryContext context) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java index 15a538b98b42b..59f50d8d6cb69 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java @@ -106,6 +106,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterSchemaTemplateReq; import org.apache.iotdb.confignode.rpc.thrift.TAlterTimeSeriesReq; +import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TCountDatabaseResp; import org.apache.iotdb.confignode.rpc.thrift.TCountTimeSlotListReq; import org.apache.iotdb.confignode.rpc.thrift.TCountTimeSlotListResp; @@ -291,6 +292,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.pipe.ShowPipesStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.pipe.StartPipeStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.pipe.StopPipeStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.CreateTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropSubscriptionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropTopicStatement; @@ -2879,6 +2881,34 @@ public SettableFuture createTopic( return future; } + @Override + public SettableFuture alterTopic( + final AlterTopicStatement alterTopicStatement) { + if (!SubscriptionConfig.getInstance().getSubscriptionEnabled()) { + return SUBSCRIPTION_NOT_ENABLED_ERROR_FUTURE; + } + + final SettableFuture future = SettableFuture.create(); + + try (final ConfigNodeClient configNodeClient = + CONFIG_NODE_CLIENT_MANAGER.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) { + final TSStatus tsStatus = + configNodeClient.alterTopic( + new TAlterTopicReq() + .setTopicName(alterTopicStatement.getTopicName()) + .setTopicAttributes(alterTopicStatement.getTopicAttributes()) + .setSubscribedConsumerGroupIds(Collections.emptySet())); + if (TSStatusCode.SUCCESS_STATUS.getStatusCode() != tsStatus.getCode()) { + future.setException(new IoTDBException(tsStatus)); + } else { + future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS)); + } + } catch (Exception e) { + future.setException(e); + } + return future; + } + @Override public SettableFuture dropTopic(final DropTopicStatement dropTopicStatement) { if (!SubscriptionConfig.getInstance().getSubscriptionEnabled()) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/IConfigTaskExecutor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/IConfigTaskExecutor.java index b45209aeffed8..4e551983d735d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/IConfigTaskExecutor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/IConfigTaskExecutor.java @@ -76,6 +76,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.pipe.ShowPipesStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.pipe.StartPipeStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.pipe.StopPipeStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.CreateTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropSubscriptionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropTopicStatement; @@ -243,6 +244,8 @@ SettableFuture dropSubscription( SettableFuture createTopic(CreateTopicStatement createTopicStatement); + SettableFuture alterTopic(AlterTopicStatement alterTopicStatement); + SettableFuture dropTopic(DropTopicStatement dropTopicStatement); SettableFuture showTopics(ShowTopicsStatement showTopicsStatement); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/subscription/AlterTopicTask.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/subscription/AlterTopicTask.java new file mode 100644 index 0000000000000..33ae342267be2 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/subscription/AlterTopicTask.java @@ -0,0 +1,49 @@ +/* + * 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.db.queryengine.plan.execution.config.sys.subscription; + +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; +import org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterTopic; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; + +import com.google.common.util.concurrent.ListenableFuture; + +public class AlterTopicTask implements IConfigTask { + + private final AlterTopicStatement alterTopicStatement; + + public AlterTopicTask(final AlterTopicStatement alterTopicStatement) { + this.alterTopicStatement = alterTopicStatement; + } + + public AlterTopicTask(final AlterTopic alterTopic) { + this.alterTopicStatement = new AlterTopicStatement(); + this.alterTopicStatement.setTopicName(alterTopic.getTopicName()); + this.alterTopicStatement.setTopicAttributes(alterTopic.getTopicAttributes()); + } + + @Override + public ListenableFuture execute(final IConfigTaskExecutor configTaskExecutor) + throws InterruptedException { + return configTaskExecutor.alterTopic(alterTopicStatement); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java index 4a82438b5deec..1df09d9641142 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java @@ -214,6 +214,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.MigrateRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.ReconstructRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.RemoveRegionStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.CreateTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropSubscriptionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropTopicStatement; @@ -4425,6 +4426,23 @@ public Statement visitCreateTopic(IoTDBSqlParser.CreateTopicContext ctx) { return createTopicStatement; } + @Override + public Statement visitAlterTopic(IoTDBSqlParser.AlterTopicContext ctx) { + final AlterTopicStatement alterTopicStatement = new AlterTopicStatement(); + + if (ctx.topicName != null) { + alterTopicStatement.setTopicName(parseIdentifier(ctx.topicName.getText())); + } else { + throw new SemanticException( + "Not support for this sql in ALTER TOPIC, please enter topicName."); + } + + alterTopicStatement.setTopicAttributes( + parseTopicAttributesClause(ctx.topicAttributesClause().topicAttributeClause())); + + return alterTopicStatement; + } + private Map parseTopicAttributesClause( List contexts) { final Map collectorMap = new HashMap<>(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java index d0779ee24a5b8..ac1e8dfc473bf 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java @@ -147,6 +147,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AddColumn; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterDB; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterPipe; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterTopic; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AsofJoinOn; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AstVisitor; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.CopyTo; @@ -4837,6 +4838,11 @@ public Scope visitCreateTopic(CreateTopic node, Optional context) { return createAndAssignScope(node, context); } + @Override + public Scope visitAlterTopic(AlterTopic node, Optional context) { + return createAndAssignScope(node, context); + } + @Override public Scope visitDropTopic(DropTopic node, Optional context) { return createAndAssignScope(node, context); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java index 68658529e71c4..49544f28d1080 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java @@ -120,6 +120,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.MigrateRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.ReconstructRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.RemoveRegionStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.CreateTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropSubscriptionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropTopicStatement; @@ -882,6 +883,12 @@ public TSStatus visitCreateTopic(CreateTopicStatement statement, TreeAccessCheck context.setAuditLogOperation(AuditLogOperation.DDL), statement::getTopicName); } + @Override + public TSStatus visitAlterTopic(AlterTopicStatement statement, TreeAccessCheckContext context) { + return checkPipeManagement( + context.setAuditLogOperation(AuditLogOperation.DDL), statement::getTopicName); + } + @Override public TSStatus visitShowTopics(ShowTopicsStatement statement, TreeAccessCheckContext context) { return checkPipeManagement( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AlterTopic.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AlterTopic.java new file mode 100644 index 0000000000000..8598c1525266d --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AlterTopic.java @@ -0,0 +1,92 @@ +/* + * 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.db.queryengine.plan.relational.sql.ast; + +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.AstMemoryEstimationHelper; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.IAstVisitor; + +import org.apache.tsfile.utils.RamUsageEstimator; + +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class AlterTopic extends SubscriptionStatement { + private static final long INSTANCE_SIZE = + RamUsageEstimator.shallowSizeOfInstance(AlterTopic.class); + + private final String topicName; + private final Map topicAttributes; + + public AlterTopic(final String topicName, final Map topicAttributes) { + this.topicName = requireNonNull(topicName, "topic name can not be null"); + this.topicAttributes = requireNonNull(topicAttributes, "topic attributes can not be null"); + } + + public String getTopicName() { + return topicName; + } + + public Map getTopicAttributes() { + return topicAttributes; + } + + @Override + public R accept(final IAstVisitor visitor, final C context) { + return ((AstVisitor) visitor).visitAlterTopic(this, context); + } + + @Override + public int hashCode() { + return Objects.hash(topicName, topicAttributes); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final AlterTopic that = (AlterTopic) obj; + return Objects.equals(this.topicName, that.topicName) + && Objects.equals(this.topicAttributes, that.topicAttributes); + } + + @Override + public String toString() { + return toStringHelper(this) + .add("topicName", topicName) + .add("topicAttributes", topicAttributes) + .toString(); + } + + @Override + public long ramBytesUsed() { + long size = INSTANCE_SIZE; + size += AstMemoryEstimationHelper.getEstimatedSizeOfNodeLocation(getLocationInternal()); + size += RamUsageEstimator.sizeOf(topicName); + size += RamUsageEstimator.sizeOfMap(topicAttributes); + return size; + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AstVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AstVisitor.java index 7c311fa7f62b6..c00b7f4d492a8 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AstVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/AstVisitor.java @@ -343,6 +343,10 @@ default R visitCreateTopic(CreateTopic node, C context) { return visitStatement(node, context); } + default R visitAlterTopic(AlterTopic node, C context) { + return visitStatement(node, context); + } + default R visitDropTopic(DropTopic node, C context) { return visitStatement(node, context); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java index 25d68eba6beb5..30b61967e1a10 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java @@ -158,6 +158,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterColumnDataType; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterDB; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterPipe; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterTopic; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AsofJoinOn; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ClearCache; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ColumnDefinition; @@ -1390,6 +1391,13 @@ public Node visitCreateTopicStatement(RelationalSqlParser.CreateTopicStatementCo return new CreateTopic(topicName, hasIfNotExistsCondition, topicAttributes); } + @Override + public Node visitAlterTopicStatement(RelationalSqlParser.AlterTopicStatementContext ctx) { + final String topicName = ((Identifier) visit(ctx.identifier())).getValue(); + return new AlterTopic( + topicName, parseTopicAttributesClause(ctx.topicAttributesClause().topicAttributeClause())); + } + private Map parseTopicAttributesClause( List contexts) { final Map tppicMap = new HashMap<>(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/util/DataNodeSqlFormatter.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/util/DataNodeSqlFormatter.java index b806fd725928e..887a6082a5d5d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/util/DataNodeSqlFormatter.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/util/DataNodeSqlFormatter.java @@ -29,6 +29,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AddColumn; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterDB; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterPipe; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterTopic; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AstVisitor; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ColumnDefinition; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.CopyTo; @@ -771,6 +772,31 @@ public Void visitCreateTopic(CreateTopic node, Integer context) { return null; } + @Override + public Void visitAlterTopic(AlterTopic node, Integer context) { + builder.append("ALTER TOPIC "); + builder.append(node.getTopicName()); + builder.append(" \n"); + + builder + .append("WITH (") + .append("\n") + .append( + node.getTopicAttributes().entrySet().stream() + .map( + entry -> + indentString(1) + + "\"" + + entry.getKey() + + "\" = \"" + + entry.getValue() + + "\"") + .collect(joining(", " + "\n"))) + .append(")\n"); + + return null; + } + @Override public Void visitDropTopic(DropTopic node, Integer context) { builder.append("DROP TOPIC "); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java index b40c6444816fc..88a2959f1475b 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java @@ -184,6 +184,7 @@ public enum StatementType { REPAIR_DATA_PARTITION_TABLE, CREATE_TOPIC, + ALTER_TOPIC, DROP_TOPIC, SHOW_TOPICS, SHOW_SUBSCRIPTIONS, diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java index 847e850c52172..39af259928b6b 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java @@ -107,6 +107,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.MigrateRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.ReconstructRegionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.region.RemoveRegionStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.CreateTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropSubscriptionStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.DropTopicStatement; @@ -644,6 +645,10 @@ public R visitCreateTopic(CreateTopicStatement createTopicStatement, C context) return visitStatement(createTopicStatement, context); } + public R visitAlterTopic(AlterTopicStatement alterTopicStatement, C context) { + return visitStatement(alterTopicStatement, context); + } + public R visitDropTopic(DropTopicStatement dropTopicStatement, C context) { return visitStatement(dropTopicStatement, context); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/subscription/AlterTopicStatement.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/subscription/AlterTopicStatement.java new file mode 100644 index 0000000000000..dd06b3034b00b --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/metadata/subscription/AlterTopicStatement.java @@ -0,0 +1,73 @@ +/* + * 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.db.queryengine.plan.statement.metadata.subscription; + +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.db.queryengine.plan.analyze.QueryType; +import org.apache.iotdb.db.queryengine.plan.statement.IConfigStatement; +import org.apache.iotdb.db.queryengine.plan.statement.Statement; +import org.apache.iotdb.db.queryengine.plan.statement.StatementType; +import org.apache.iotdb.db.queryengine.plan.statement.StatementVisitor; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class AlterTopicStatement extends Statement implements IConfigStatement { + + private String topicName; + private Map topicAttributes; + + public AlterTopicStatement() { + super(); + statementType = StatementType.ALTER_TOPIC; + } + + public String getTopicName() { + return topicName; + } + + public Map getTopicAttributes() { + return topicAttributes; + } + + public void setTopicName(final String topicName) { + this.topicName = topicName; + } + + public void setTopicAttributes(final Map topicAttributes) { + this.topicAttributes = topicAttributes; + } + + @Override + public QueryType getQueryType() { + return QueryType.OTHER; + } + + @Override + public List getPaths() { + return Collections.emptyList(); + } + + @Override + public R accept(final StatementVisitor visitor, final C context) { + return visitor.visitAlterTopic(this, context); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java index b98f34a2484d9..574308f5ec45d 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java @@ -56,6 +56,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.DatabaseSchemaStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.DeleteDatabaseStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.DeleteTimeSeriesStatement; +import org.apache.iotdb.db.queryengine.plan.statement.metadata.subscription.AlterTopicStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.template.BatchActivateTemplateStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.template.CreateSchemaTemplateStatement; import org.apache.iotdb.db.queryengine.plan.statement.metadata.template.DropSchemaTemplateStatement; @@ -121,6 +122,20 @@ public class StatementGeneratorTest { + @Test + public void testAlterTopicStatement() { + final Statement statement = + StatementGenerator.createStatement( + "ALTER TOPIC topic1 WITH ('owner-id'='sn2','owner-epoch'='6')", + ZonedDateTime.now().getOffset()); + + Assert.assertTrue(statement instanceof AlterTopicStatement); + final AlterTopicStatement alterTopicStatement = (AlterTopicStatement) statement; + Assert.assertEquals("topic1", alterTopicStatement.getTopicName()); + Assert.assertEquals("sn2", alterTopicStatement.getTopicAttributes().get("owner-id")); + Assert.assertEquals("6", alterTopicStatement.getTopicAttributes().get("owner-epoch")); + } + @Test public void testShowDiskUsage() { diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java new file mode 100644 index 0000000000000..66aac145968ef --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java @@ -0,0 +1,48 @@ +/* + * 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.db.queryengine.plan.relational.sql.parser; + +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AlterTopic; + +import org.junit.Assert; +import org.junit.Test; + +import java.time.ZoneId; + +public class TopicStatementTest { + + private final SqlParser sqlParser = new SqlParser(); + + @Test + public void testAlterTopicStatement() { + final Statement statement = + sqlParser.createStatement( + "ALTER TOPIC topic1 WITH ('owner-id'='sn2','owner-epoch'='6')", + ZoneId.systemDefault(), + null); + + Assert.assertTrue(statement instanceof AlterTopic); + final AlterTopic alterTopic = (AlterTopic) statement; + Assert.assertEquals("topic1", alterTopic.getTopicName()); + Assert.assertEquals("sn2", alterTopic.getTopicAttributes().get("owner-id")); + Assert.assertEquals("6", alterTopic.getTopicAttributes().get("owner-epoch")); + } +} diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index 44222a5a697fc..2c1ddc6e763d8 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -88,6 +88,17 @@ public TopicMeta deepCopy() { return copied; } + public TopicMeta deepCopyWithUpdatedAttributes(final Map updatedAttributes) { + final Map copiedAttributes = new HashMap<>(config.getAttribute()); + if (Objects.nonNull(updatedAttributes)) { + copiedAttributes.putAll(updatedAttributes); + } + + final TopicMeta copied = new TopicMeta(topicName, creationTime, copiedAttributes); + copied.subscribedConsumerGroupIds = new HashSet<>(subscribedConsumerGroupIds); + return copied; + } + public String getTopicName() { return topicName; } diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java index 0511edff6203e..d20464ee9c6ee 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java @@ -101,6 +101,29 @@ public void testTopicOwnerDeSer() throws IOException { topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); } + @Test + public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndPreservesExistingConfig() { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.PATH_KEY, "root.sg.**"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + topicMeta.addSubscribedConsumerGroup("group1"); + + final Map updatedAttributes = new HashMap<>(); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TopicMeta updatedTopicMeta = topicMeta.deepCopyWithUpdatedAttributes(updatedAttributes); + + Assert.assertEquals( + "root.sg.**", updatedTopicMeta.getConfig().getString(TopicConstant.PATH_KEY)); + Assert.assertEquals("sn2", updatedTopicMeta.getOwnerId()); + Assert.assertEquals(6L, updatedTopicMeta.getOwnerEpoch()); + Assert.assertTrue(updatedTopicMeta.isSubscribedByConsumerGroup("group1")); + Assert.assertEquals("sn1", topicMeta.getOwnerId()); + Assert.assertEquals(5L, topicMeta.getOwnerEpoch()); + } + @Test public void testSequentialTopicMetaDeserializeDoesNotConsumeNextTopic() throws IOException { final TopicMeta firstTopicMeta = new TopicMeta("first_topic", 1, new HashMap<>()); diff --git a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 index f984e825fc7ad..af2d4986a4917 100644 --- a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 +++ b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4 @@ -108,6 +108,7 @@ statement // Subscription Statement | createTopicStatement + | alterTopicStatement | dropTopicStatement | showTopicsStatement | showSubscriptionsStatement @@ -537,6 +538,10 @@ createTopicStatement : CREATE TOPIC (IF NOT EXISTS)? topicName=identifier topicAttributesClause? ; +alterTopicStatement + : ALTER TOPIC topicName=identifier topicAttributesClause + ; + topicAttributesClause : WITH '(' topicAttributeClause (',' topicAttributeClause)* ')' ; diff --git a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift index 22529ffbb737a..2af5e4c6ccc10 100644 --- a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift +++ b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift @@ -1930,6 +1930,9 @@ service IConfigNodeRPCService { /** Create Topic */ common.TSStatus createTopic(TCreateTopicReq req) + /** Alter Topic */ + common.TSStatus alterTopic(TAlterTopicReq req) + /** Drop Topic */ common.TSStatus dropTopic(string topicName) @@ -2071,4 +2074,3 @@ service IConfigNodeRPCService { common.TSStatus createTableView(TCreateTableViewReq req) } - From dbbf7977f18aff606fb90baf9b52470ccc9d2a56 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 4 Jun 2026 05:45:04 +0000 Subject: [PATCH 03/14] Add topic owner update show test --- .../subscription/SubscriptionInfoTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index d78fe77af9d86..a197c1a46f3f7 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -23,6 +23,9 @@ import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.AlterTopicPlan; import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan; +import org.apache.iotdb.confignode.consensus.response.subscription.TopicTableResp; +import org.apache.iotdb.confignode.rpc.thrift.TShowTopicInfo; +import org.apache.iotdb.confignode.rpc.thrift.TShowTopicResp; import org.apache.iotdb.rpc.TSStatusCode; import org.apache.iotdb.rpc.subscription.config.TopicConstant; @@ -86,6 +89,51 @@ public void testAlterTopicTransfersOwnerWithUpdatedAttributes() { subscriptionInfo.getTopicMeta(topicName).getConfig().getString(TopicConstant.PATH_KEY)); } + @Test + public void testAlterTopicOwnerAndShowTopicOwner() { + final String topicName = "topic-" + UUID.randomUUID(); + final long ownerLeaseExpireTimeMs = 123456789L; + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); + initialTopicMeta.getConfig().getAttribute().put(TopicConstant.PATH_KEY, "root.sg.**"); + initialTopicMeta.getConfig().getAttribute().put(TopicConstant.START_TIME_KEY, "0"); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); + + final Map updatedAttributes = new HashMap<>(); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + updatedAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo + .alterTopic( + new AlterTopicPlan( + initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))) + .getCode()); + + final TShowTopicResp showTopicResp = + ((TopicTableResp) subscriptionInfo.showTopics()).convertToTShowTopicResp(); + + Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), showTopicResp.status.code); + Assert.assertEquals(1, showTopicResp.getTopicInfoListSize()); + + final TShowTopicInfo showTopicInfo = showTopicResp.getTopicInfoList().get(0); + Assert.assertEquals(topicName, showTopicInfo.getTopicName()); + Assert.assertEquals(1L, showTopicInfo.getCreationTime()); + Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("path=root.sg.**")); + Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("start-time=0")); + Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("owner-id=sn2")); + Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("owner-epoch=6")); + Assert.assertTrue( + showTopicInfo + .getTopicAttributes() + .contains("owner-lease-expire-time-ms=" + ownerLeaseExpireTimeMs)); + } + private TopicMeta createTopicMeta( final String topicName, final String ownerId, final long ownerEpoch) { final Map topicAttributes = new HashMap<>(); From 82049d7b46a801e1f930ca6bc07eaafffbd406c1 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 8 Jun 2026 06:24:16 +0000 Subject: [PATCH 04/14] Subscription: address topic owner review comments --- .../AbstractSubscriptionSession.java | 18 +++++- .../confignode/i18n/ManagerMessages.java | 2 + .../confignode/i18n/ManagerMessages.java | 2 + .../confignode/manager/ProcedureManager.java | 3 +- .../subscription/SubscriptionCoordinator.java | 6 +- .../subscription/SubscriptionInfo.java | 13 ++++ .../subscription/SubscriptionInfoTest.java | 61 +++++++++++++++++++ .../iotdb/db/i18n/DataNodeQueryMessages.java | 2 + .../iotdb/db/i18n/DataNodeQueryMessages.java | 2 + .../queryengine/plan/parser/ASTVisitor.java | 3 +- .../subscription/meta/topic/TopicMeta.java | 28 +++++++++ 11 files changed, 132 insertions(+), 8 deletions(-) diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java index 5bddd082e4e78..53cd730b8cfbb 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java @@ -116,10 +116,20 @@ topicName, buildTopicAttributesClause(properties)) protected void alterTopic(final String topicName, final Properties properties) throws IoTDBConnectionException, StatementExecutionException { + alterTopic(topicName, properties, false); + } + + private void alterTopic( + final String topicName, final Properties properties, final boolean allowOwnerAttributes) + throws IoTDBConnectionException, StatementExecutionException { IdentifierUtils.checkAndParseIdentifier(topicName); // ignore the parse result if (Objects.isNull(properties) || properties.isEmpty()) { throw new StatementExecutionException("Topic attributes should not be empty in ALTER TOPIC."); } + if (!allowOwnerAttributes && containsOwnerAttribute(properties)) { + throw new StatementExecutionException( + "Topic owner attributes should be modified by alterTopicOwner only."); + } final String sql = String.format("ALTER TOPIC %s WITH %s", topicName, buildTopicAttributesClause(properties)); session.executeNonQueryStatement(sql); @@ -148,7 +158,7 @@ protected void alterTopicOwner( properties.put( TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); } - alterTopic(topicName, properties); + alterTopic(topicName, properties, true); } protected void dropTopic(final String topicName) @@ -281,4 +291,10 @@ private static String buildTopicAttributesClause(final Properties properties) { private static String escapeSqlStringLiteral(final String value) { return value.replace("'", "''"); } + + private static boolean containsOwnerAttribute(final Properties properties) { + return properties.containsKey(TopicConstant.OWNER_ID_KEY) + || properties.containsKey(TopicConstant.OWNER_EPOCH_KEY) + || properties.containsKey(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + } } diff --git a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java index 5dbfe15157668..97c70d8de1631 100644 --- a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java +++ b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java @@ -134,6 +134,8 @@ public final class ManagerMessages { "Failed to create subtask for pipe %s, creation time %d"; public static final String FAILED_TO_CREATE_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS = "Failed to create topic {} with attributes {}. Result status: {}."; + public static final String FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED = + "Failed to alter topic %s, the topic is not existed"; public static final String FAILED_TO_DEEP_COPY_PIPEMETA = "failed to deep copy pipeMeta"; public static final String FAILED_TO_DEREGISTER_PIPE_CONFIG_REGION_CONNECTOR = "Failed to deregister pipe config region connector metrics, PipeConfigNodeSubtask({}) does not exist"; diff --git a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java index 836fe7dd60b21..87a092d597cfa 100644 --- a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java +++ b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java @@ -133,6 +133,8 @@ public final class ManagerMessages { "Failed to create subtask for pipe %s, creation time %d"; public static final String FAILED_TO_CREATE_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS = "Failed to create topic {} with attributes {}. Result status: {}."; + public static final String FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED = + "修改 topic %s 失败,该 topic 不存在"; public static final String FAILED_TO_DEEP_COPY_PIPEMETA = "深拷贝 pipeMeta 失败"; public static final String FAILED_TO_DEREGISTER_PIPE_CONFIG_REGION_CONNECTOR = "Failed to deregister pipe config region connector metrics, PipeConfigNodeSubtask({}) does not exist"; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java index 78e43ce8f470a..af6384c7fa84a 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java @@ -1747,7 +1747,8 @@ public TSStatus alterTopic(TAlterTopicReq req) { return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) .setMessage( String.format( - "Failed to alter topic %s, the topic is not existed", req.getTopicName())); + ManagerMessages.FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED, + req.getTopicName())); } AlterTopicProcedure procedure = new AlterTopicProcedure(updatedTopicMeta); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index 5f0cabfff8a53..e9766b938a428 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -164,10 +164,8 @@ public TSStatus alterTopic(TAlterTopicReq req) { } public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) { - final TopicMeta existedTopicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); - return existedTopicMeta == null - ? null - : existedTopicMeta.deepCopyWithUpdatedAttributes(req.getTopicAttributes()); + return subscriptionInfo.deepCopyTopicMetaWithUpdatedAttributes( + req.getTopicName(), req.getTopicAttributes()); } public TSStatus dropTopic(TDropTopicReq req) { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java index 1849823888f0a..2ac8827fbce9d 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java @@ -56,6 +56,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -306,6 +307,18 @@ public TopicMeta deepCopyTopicMeta(String topicName) { } } + public TopicMeta deepCopyTopicMetaWithUpdatedAttributes( + String topicName, Map updatedAttributes) { + acquireReadLock(); + try { + return topicMetaKeeper.containsTopicMeta(topicName) + ? topicMetaKeeper.getTopicMeta(topicName).deepCopyWithUpdatedAttributes(updatedAttributes) + : null; + } finally { + releaseReadLock(); + } + } + public DataSet showTopics() { acquireReadLock(); try { diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index a197c1a46f3f7..d34ef9d58f4b5 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -89,6 +89,54 @@ public void testAlterTopicTransfersOwnerWithUpdatedAttributes() { subscriptionInfo.getTopicMeta(topicName).getConfig().getString(TopicConstant.PATH_KEY)); } + @Test + public void testAlterTopicRejectsTransferBeforeOwnerLeaseExpires() { + final String topicName = "topic-" + UUID.randomUUID(); + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final TopicMeta initialTopicMeta = + createTopicMeta(topicName, "sn1", 5L, System.currentTimeMillis() + 60000); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); + + final Map updatedAttributes = new HashMap<>(); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TSStatus alterStatus = + subscriptionInfo.alterTopic( + new AlterTopicPlan(initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))); + + Assert.assertEquals( + TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode(), alterStatus.getCode()); + Assert.assertTrue(alterStatus.getMessage().contains("owner lease has not expired")); + Assert.assertEquals("sn1", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals(5L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + } + + @Test + public void testAlterTopicTransfersOwnerAfterOwnerLeaseExpires() { + final String topicName = "topic-" + UUID.randomUUID(); + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final TopicMeta initialTopicMeta = + createTopicMeta(topicName, "sn1", 5L, System.currentTimeMillis() - 1); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); + + final Map updatedAttributes = new HashMap<>(); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TSStatus alterStatus = + subscriptionInfo.alterTopic( + new AlterTopicPlan(initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))); + + Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); + Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + } + @Test public void testAlterTopicOwnerAndShowTopicOwner() { final String topicName = "topic-" + UUID.randomUUID(); @@ -141,4 +189,17 @@ private TopicMeta createTopicMeta( topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); return new TopicMeta(topicName, 1, topicAttributes); } + + private TopicMeta createTopicMeta( + final String topicName, + final String ownerId, + final long ownerEpoch, + final long ownerLeaseExpireTimeMs) { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, ownerId); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + topicAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + return new TopicMeta(topicName, 1, topicAttributes); + } } diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index c1c26550f9da1..41f0ccc620ea5 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -547,6 +547,8 @@ public final class DataNodeQueryMessages { "Not support for this sql in START PIPE, please enter pipename."; public static final String NOT_SUPPORT_FOR_THIS_SQL_IN_STOP_PIPE = "Not support for this sql in STOP PIPE, please enter pipename."; + public static final String NOT_SUPPORT_FOR_THIS_SQL_IN_ALTER_TOPIC = + "Not support for this sql in ALTER TOPIC, please enter topicName."; public static final String GET_REGION_ID_STATEMENT_EXPRESSION_MUST_BE_A = "Get region id statement‘ expression must be a time expression"; public static final String WRONG_SPACE_QUOTA_TYPE = diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 3d2783c81b631..8a4591ca39731 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -546,6 +546,8 @@ public final class DataNodeQueryMessages { "START PIPE 不支持此 SQL,请输入管道名。"; public static final String NOT_SUPPORT_FOR_THIS_SQL_IN_STOP_PIPE = "STOP PIPE 不支持此 SQL,请输入管道名。"; + public static final String NOT_SUPPORT_FOR_THIS_SQL_IN_ALTER_TOPIC = + "ALTER TOPIC 不支持此 SQL,请输入 topic 名。"; public static final String GET_REGION_ID_STATEMENT_EXPRESSION_MUST_BE_A = "GET REGION ID 语句的表达式必须是时间表达式"; public static final String WRONG_SPACE_QUOTA_TYPE = diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java index 1df09d9641142..b74379fc82459 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java @@ -4433,8 +4433,7 @@ public Statement visitAlterTopic(IoTDBSqlParser.AlterTopicContext ctx) { if (ctx.topicName != null) { alterTopicStatement.setTopicName(parseIdentifier(ctx.topicName.getText())); } else { - throw new SemanticException( - "Not support for this sql in ALTER TOPIC, please enter topicName."); + throw new SemanticException(DataNodeQueryMessages.NOT_SUPPORT_FOR_THIS_SQL_IN_ALTER_TOPIC); } alterTopicStatement.setTopicAttributes( diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index 2c1ddc6e763d8..0f787f6101000 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -150,6 +150,13 @@ public void transferOwner( + " incoming epoch is %s", this.ownerEpoch, ownerEpoch)); } + if (isActiveOwnerLease() && isOwnerTransfer(ownerId)) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease has not expired, topic: %s, current owner-id: %s," + + " current owner-epoch: %s, current owner-lease-expire-time-ms: %s", + topicName, this.ownerId, this.ownerEpoch, this.ownerLeaseExpireTimeMs)); + } this.ownerId = ownerId; this.ownerEpoch = ownerEpoch; @@ -320,6 +327,27 @@ public static void validateOwnerProgression( updatedTopicMeta.getOwnerId(), updatedTopicMeta.getOwnerEpoch())); } + + if (currentTopicMeta.isActiveOwnerLease() + && currentTopicMeta.isOwnerTransfer(updatedTopicMeta.getOwnerId())) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease has not expired, topic: %s, current owner-id: %s," + + " current owner-epoch: %s, current owner-lease-expire-time-ms: %s", + currentTopicMeta.getTopicName(), + currentTopicMeta.getOwnerId(), + currentTopicMeta.getOwnerEpoch(), + currentTopicMeta.getOwnerLeaseExpireTimeMs())); + } + } + + private boolean isActiveOwnerLease() { + return Objects.nonNull(ownerLeaseExpireTimeMs) + && System.currentTimeMillis() <= ownerLeaseExpireTimeMs; + } + + private boolean isOwnerTransfer(final String incomingOwnerId) { + return !Objects.equals(ownerId, incomingOwnerId); } private void initOwnerFromTopicAttributes(final Map topicAttributes) { From ed4188448518da838f486e78f62e93ec538b1b7d Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 9 Jun 2026 10:49:28 +0000 Subject: [PATCH 05/14] Implement subscription topic owner lease renewal --- .../env/cluster/config/MppCommonConfig.java | 6 ++ .../cluster/config/MppSharedCommonConfig.java | 7 ++ .../env/remote/config/RemoteCommonConfig.java | 5 ++ .../apache/iotdb/itbase/env/CommonConfig.java | 2 + .../it/local/AbstractSubscriptionLocalIT.java | 1 + .../local/IoTDBSubscriptionTopicOwnerIT.java | 88 ++++++++++++++++++ .../confignode/manager/ConfigManager.java | 9 ++ .../iotdb/confignode/manager/IManager.java | 4 + .../confignode/manager/ProcedureManager.java | 58 +++++++++++- .../subscription/SubscriptionCoordinator.java | 76 ++++++++++++++++ .../thrift/ConfigNodeRPCServiceProcessor.java | 6 ++ .../subscription/SubscriptionInfoTest.java | 26 ++++++ .../db/protocol/client/ConfigNodeClient.java | 7 ++ .../agent/SubscriptionTopicAgent.java | 62 +++++++++++++ .../receiver/SubscriptionReceiverV1.java | 83 ++++++++++++++--- .../iotdb/commons/conf/CommonConfig.java | 8 ++ .../iotdb/commons/conf/CommonDescriptor.java | 4 + .../config/SubscriptionConfig.java | 2 +- .../subscription/meta/topic/TopicMeta.java | 59 ++++++++++++ .../subscription/topic/TopicDeSerTest.java | 90 +++++++++++++++++++ .../src/main/thrift/confignode.thrift | 10 +++ 21 files changed, 598 insertions(+), 15 deletions(-) diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java index 4852b9d116e25..5477113f21fb2 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java @@ -477,6 +477,12 @@ public CommonConfig setIsPipeEnableMemoryCheck(boolean isPipeEnableMemoryCheck) return this; } + @Override + public CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled) { + setProperty("subscription_enabled", String.valueOf(subscriptionEnabled)); + return this; + } + @Override public CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled) { setProperty("pipe_air_gap_receiver_enabled", String.valueOf(isPipeAirGapReceiverEnabled)); diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java index 582c9a049e492..28d308bf7a1d2 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java @@ -493,6 +493,13 @@ public CommonConfig setIsPipeEnableMemoryCheck(boolean isPipeEnableMemoryCheck) return this; } + @Override + public CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled) { + dnConfig.setSubscriptionEnabled(subscriptionEnabled); + cnConfig.setSubscriptionEnabled(subscriptionEnabled); + return this; + } + @Override public CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled) { dnConfig.setPipeAirGapReceiverEnabled(isPipeAirGapReceiverEnabled); diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java index 48c157e957be8..fe1c267cf20a4 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java @@ -348,6 +348,11 @@ public CommonConfig setIsPipeEnableMemoryCheck(boolean isPipeEnableMemoryCheck) return this; } + @Override + public CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled) { + return this; + } + @Override public CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled) { return this; diff --git a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java index dc21234e2bad2..bc8626ef96634 100644 --- a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java @@ -154,6 +154,8 @@ CommonConfig setEnableAutoLeaderBalanceForIoTConsensus( CommonConfig setIsPipeEnableMemoryCheck(boolean isPipeEnableMemoryCheck); + CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled); + CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled); CommonConfig setDriverTaskExecutionTimeSliceInMs(long driverTaskExecutionTimeSliceInMs); diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/AbstractSubscriptionLocalIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/AbstractSubscriptionLocalIT.java index 9ee6a8d4e8d0d..fe667480c86bf 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/AbstractSubscriptionLocalIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/AbstractSubscriptionLocalIT.java @@ -36,6 +36,7 @@ public void setUp() throws Exception { EnvFactory.getEnv() .getConfig() .getCommonConfig() + .setSubscriptionEnabled(true) .setPipeMemoryManagementEnabled(false) .setIsPipeEnableMemoryCheck(false); diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index 4e89755048665..fb116b03e5734 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -27,6 +27,7 @@ import org.apache.iotdb.rpc.subscription.exception.SubscriptionOwnerFencedException; import org.apache.iotdb.session.subscription.SubscriptionTreeSession; import org.apache.iotdb.session.subscription.consumer.tree.SubscriptionTreePullConsumer; +import org.apache.iotdb.session.subscription.model.Topic; import org.apache.iotdb.session.subscription.payload.SubscriptionMessage; import org.apache.iotdb.session.subscription.payload.SubscriptionRecordHandler; import org.apache.iotdb.subscription.it.IoTDBSubscriptionITConstant; @@ -44,11 +45,16 @@ import java.util.List; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @RunWith(IoTDBTestRunner.class) @Category({LocalStandaloneIT.class}) public class IoTDBSubscriptionTopicOwnerIT extends AbstractSubscriptionLocalIT { + private static final Pattern OWNER_LEASE_EXPIRE_TIME_MS_PATTERN = + Pattern.compile("owner-lease-expire-time-ms=([0-9]+)"); + @Override @Before public void setUp() throws Exception { @@ -125,6 +131,70 @@ public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws } } + @Test + public void testHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() throws Exception { + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + final String topicName = "topic_owner_lease_renewal"; + final long heartbeatIntervalMs = 1000L; + final long initialOwnerLeaseExpireTimeMs = System.currentTimeMillis() + 2500L; + + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Properties properties = new Properties(); + properties.put(TopicConstant.PATH_KEY, "root.topic_owner.**"); + properties.put(TopicConstant.START_TIME_KEY, "0"); + properties.put(TopicConstant.OWNER_ID_KEY, "sn1"); + properties.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + properties.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, + String.valueOf(initialOwnerLeaseExpireTimeMs)); + session.createTopic(topicName, properties); + + try (final SubscriptionTreePullConsumer currentOwnerConsumer = + new SubscriptionTreePullConsumer.Builder() + .host(host) + .port(port) + .consumerId("current_sn") + .consumerGroupId("topic_owner_lease_group") + .ownerId("sn1") + .ownerEpoch(5L) + .heartbeatIntervalMs(heartbeatIntervalMs) + .autoCommit(false) + .buildPullConsumer()) { + currentOwnerConsumer.open(); + currentOwnerConsumer.subscribe(topicName); + + final AtomicReference renewedOwnerLeaseExpireTimeMs = + new AtomicReference<>(initialOwnerLeaseExpireTimeMs); + IoTDBSubscriptionITConstant.AWAIT.untilAsserted( + () -> { + final Long ownerLeaseExpireTimeMs = getOwnerLeaseExpireTimeMs(session, topicName); + Assert.assertNotNull(ownerLeaseExpireTimeMs); + Assert.assertTrue(ownerLeaseExpireTimeMs > initialOwnerLeaseExpireTimeMs); + renewedOwnerLeaseExpireTimeMs.set(ownerLeaseExpireTimeMs); + }); + + final long alterStartTimeMs = System.currentTimeMillis(); + session.alterTopicOwner(topicName, "sn2", 6L); + final long alterElapsedTimeMs = System.currentTimeMillis() - alterStartTimeMs; + + final long ownerLeaseRemainingTimeMs = + renewedOwnerLeaseExpireTimeMs.get() - alterStartTimeMs; + if (ownerLeaseRemainingTimeMs > 500L) { + Assert.assertTrue(alterElapsedTimeMs >= ownerLeaseRemainingTimeMs - 250L); + } + + final String topicAttributes = getTopicAttributes(session, topicName); + Assert.assertTrue(topicAttributes.contains("owner-id=sn2")); + Assert.assertTrue(topicAttributes.contains("owner-epoch=6")); + Assert.assertFalse(topicAttributes.contains("owner-lease-expire-time-ms=")); + } finally { + session.dropTopicIfExists(topicName); + } + } + } + private void insertData() throws Exception { try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { for (int i = 0; i < 10; i++) { @@ -147,4 +217,22 @@ private static int countRows(final List messages) throws Ex } return rowCount; } + + private static String getTopicAttributes( + final SubscriptionTreeSession session, final String topicName) throws Exception { + for (final Topic topic : session.getTopics()) { + if (topicName.equals(topic.getTopicName())) { + return topic.getTopicAttributes(); + } + } + Assert.fail("Topic " + topicName + " should exist."); + return ""; + } + + private static Long getOwnerLeaseExpireTimeMs( + final SubscriptionTreeSession session, final String topicName) throws Exception { + final Matcher matcher = + OWNER_LEASE_EXPIRE_TIME_MS_PATTERN.matcher(getTopicAttributes(session, topicName)); + return matcher.find() ? Long.parseLong(matcher.group(1)) : null; + } } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java index 9e59bb9e51565..5196ad722bf26 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java @@ -221,6 +221,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionTableResp; import org.apache.iotdb.confignode.rpc.thrift.TSetDataNodeStatusReq; @@ -2468,6 +2469,14 @@ public TSStatus alterTopic(TAlterTopicReq req) { : status; } + @Override + public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { + TSStatus status = confirmLeader(); + return status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode() + ? subscriptionManager.getSubscriptionCoordinator().renewTopicOwnerLease(req) + : status; + } + @Override public TSStatus dropTopic(TDropTopicReq req) { TSStatus status = confirmLeader(); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java index fa708dbaeca83..ae55ec211e273 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java @@ -138,6 +138,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionTableResp; import org.apache.iotdb.confignode.rpc.thrift.TSetDataNodeStatusReq; @@ -807,6 +808,9 @@ TPermissionInfoResp login( /** Alter Topic. */ TSStatus alterTopic(TAlterTopicReq req); + /** Renew Topic Owner Lease. */ + TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req); + /** Drop Topic. */ TSStatus dropTopic(TDropTopicReq req); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java index af6384c7fa84a..7645c437485c3 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java @@ -154,6 +154,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TMigrateRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSubscribeReq; import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; import org.apache.iotdb.consensus.ConsensusFactory; @@ -1737,12 +1738,59 @@ public TSStatus createTopic(TCreateTopicReq req) { } public TSStatus alterTopic(TAlterTopicReq req) { + boolean isOwnerLeaseRenewalBlocked = false; + try { + isOwnerLeaseRenewalBlocked = + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .blockOwnerLeaseRenewalIfOwnerTransfer(req); + while (true) { + final TopicMeta updatedTopicMeta = + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .buildAlteredTopicMetaAfterOwnerLeaseExpired(req); + if (updatedTopicMeta == null) { + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage( + String.format( + ManagerMessages.FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED, + req.getTopicName())); + } + + AlterTopicProcedure procedure = new AlterTopicProcedure(updatedTopicMeta); + executor.submitProcedure(procedure); + TSStatus status = waitingProcedureFinished(procedure); + if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return status; + } + if (isOwnerLeaseRenewalBlocked && isOwnerLeaseNotExpiredConflict(status)) { + continue; + } + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage(wrapTimeoutMessageForPipeProcedure(status.getMessage())); + } + } catch (Exception e) { + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage(e.getMessage()); + } finally { + if (isOwnerLeaseRenewalBlocked) { + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .unblockOwnerLeaseRenewal(req.getTopicName()); + } + } + } + + public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { try { final TopicMeta updatedTopicMeta = configManager .getSubscriptionManager() .getSubscriptionCoordinator() - .buildAlteredTopicMeta(req); + .buildRenewedTopicOwnerLeaseMeta(req); if (updatedTopicMeta == null) { return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) .setMessage( @@ -1761,7 +1809,7 @@ public TSStatus alterTopic(TAlterTopicReq req) { .setMessage(wrapTimeoutMessageForPipeProcedure(status.getMessage())); } } catch (Exception e) { - return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + return new TSStatus(TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode()) .setMessage(e.getMessage()); } } @@ -1982,6 +2030,12 @@ private static String wrapTimeoutMessageForPipeProcedure(String message) { return message; } + private static boolean isOwnerLeaseNotExpiredConflict(final TSStatus status) { + return Objects.nonNull(status) + && Objects.nonNull(status.getMessage()) + && status.getMessage().contains("owner lease has not expired"); + } + public static void sleepWithoutInterrupt(final long timeToSleep) { long currentTime = System.currentTimeMillis(); final long endTime = timeToSleep + currentTime; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index e9766b938a428..a7ff36441b783 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -37,6 +37,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TDropTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TGetAllSubscriptionInfoResp; import org.apache.iotdb.confignode.rpc.thrift.TGetAllTopicInfoResp; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TShowSubscriptionReq; import org.apache.iotdb.confignode.rpc.thrift.TShowSubscriptionResp; import org.apache.iotdb.confignode.rpc.thrift.TShowTopicReq; @@ -51,7 +52,10 @@ import org.slf4j.LoggerFactory; import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; public class SubscriptionCoordinator { @@ -67,6 +71,8 @@ public class SubscriptionCoordinator { private AtomicReference subscriptionInfoHolder; private final SubscriptionMetaSyncer subscriptionMetaSyncer; + private final Set blockedOwnerLeaseRenewalTopics = + Collections.synchronizedSet(new HashSet<>()); public SubscriptionCoordinator(ConfigManager configManager, SubscriptionInfo subscriptionInfo) { this.configManager = configManager; @@ -163,11 +169,81 @@ public TSStatus alterTopic(TAlterTopicReq req) { return status; } + public boolean blockOwnerLeaseRenewalIfOwnerTransfer(TAlterTopicReq req) { + final TopicMeta currentTopicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); + final TopicMeta updatedTopicMeta = buildAlteredTopicMeta(req); + if (Objects.isNull(currentTopicMeta) + || Objects.isNull(updatedTopicMeta) + || Objects.equals(currentTopicMeta.getOwnerId(), updatedTopicMeta.getOwnerId())) { + return false; + } + + blockedOwnerLeaseRenewalTopics.add(req.getTopicName()); + return true; + } + + public void unblockOwnerLeaseRenewal(String topicName) { + blockedOwnerLeaseRenewalTopics.remove(topicName); + } + + public TopicMeta buildAlteredTopicMetaAfterOwnerLeaseExpired(TAlterTopicReq req) + throws InterruptedException { + while (true) { + final TopicMeta currentTopicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); + final TopicMeta updatedTopicMeta = buildAlteredTopicMeta(req); + if (Objects.isNull(currentTopicMeta) + || Objects.isNull(updatedTopicMeta) + || Objects.equals(currentTopicMeta.getOwnerId(), updatedTopicMeta.getOwnerId())) { + return updatedTopicMeta; + } + + final long ownerLeaseRemainingTimeMs = currentTopicMeta.getOwnerLeaseRemainingTimeMs(); + if (Objects.isNull(currentTopicMeta.getOwnerLeaseExpireTimeMs()) + || ownerLeaseRemainingTimeMs <= 0) { + return updatedTopicMeta; + } + + Thread.sleep(Math.min(ownerLeaseRemainingTimeMs + 1, 1000L)); + } + } + public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) { return subscriptionInfo.deepCopyTopicMetaWithUpdatedAttributes( req.getTopicName(), req.getTopicAttributes()); } + public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { + if (blockedOwnerLeaseRenewalTopics.contains(req.getTopicName())) { + return RpcUtils.getStatus( + TSStatusCode.SUBSCRIPTION_OWNER_FENCED, + String.format( + "Subscription: topic %s owner lease renewal is blocked by a pending owner transfer.", + req.getTopicName())); + } + + final TSStatus status = configManager.getProcedureManager().renewTopicOwnerLease(req); + if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + LOGGER.warn( + "Failed to renew topic {} owner lease for owner {} epoch {}, result status is {}.", + req.getTopicName(), + req.getOwnerId(), + req.getOwnerEpoch(), + status); + } + return status; + } + + public TopicMeta buildRenewedTopicOwnerLeaseMeta(TRenewTopicOwnerLeaseReq req) { + final TopicMeta topicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); + if (Objects.isNull(topicMeta)) { + return null; + } + + topicMeta.renewOwnerLease( + req.getOwnerId(), req.getOwnerEpoch(), req.getOwnerLeaseExpireTimeMs()); + return topicMeta; + } + public TSStatus dropTopic(TDropTopicReq req) { final String topicName = req.getTopicName(); final boolean isSetIfExistsCondition = diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java index 4a4c4f59e2161..8a87724363a18 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java @@ -189,6 +189,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionReq; @@ -1274,6 +1275,11 @@ public TSStatus alterTopic(TAlterTopicReq req) { return configManager.alterTopic(req); } + @Override + public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { + return configManager.renewTopicOwnerLease(req); + } + @Override public TSStatus dropTopic(String topicName) { return configManager.dropTopic( diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index d34ef9d58f4b5..961da7c63914b 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -135,6 +135,32 @@ public void testAlterTopicTransfersOwnerAfterOwnerLeaseExpires() { Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + Assert.assertNull(subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs()); + } + + @Test + public void testAlterTopicRenewsOwnerLease() { + final String topicName = "topic-" + UUID.randomUUID(); + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; + final long renewedOwnerLeaseExpireTimeMs = ownerLeaseExpireTimeMs + 60000; + final TopicMeta initialTopicMeta = + createTopicMeta(topicName, "sn1", 5L, ownerLeaseExpireTimeMs); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); + + final TopicMeta renewedTopicMeta = initialTopicMeta.deepCopy(); + renewedTopicMeta.renewOwnerLease("sn1", 5L, renewedOwnerLeaseExpireTimeMs); + final TSStatus alterStatus = subscriptionInfo.alterTopic(new AlterTopicPlan(renewedTopicMeta)); + + Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); + Assert.assertEquals("sn1", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals(5L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + Assert.assertEquals( + renewedOwnerLeaseExpireTimeMs, + subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs().longValue()); } @Test diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java index 7685864080164..5755cd2780ba0 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java @@ -146,6 +146,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionReq; @@ -1214,6 +1215,12 @@ public TSStatus alterTopic(TAlterTopicReq req) throws TException { () -> client.alterTopic(req), status -> !updateConfigNodeLeader(status)); } + @Override + public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) throws TException { + return executeRemoteCallWithRetry( + () -> client.renewTopicOwnerLease(req), status -> !updateConfigNodeLeader(status)); + } + @Override public TSStatus dropTopic(String topicName) throws TException { return executeRemoteCallWithRetry( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java index d524b38d4c13c..6bcb8448f25be 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java @@ -33,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -260,4 +261,65 @@ public TSStatus checkTopicOwners( } return RpcUtils.SUCCESS_STATUS; } + + public Map getTopicOwnerLeaseRenewalExpireTimeMs( + final ConsumerConfig consumerConfig, final Iterable topicNames) { + final String requestOwnerId = consumerConfig.getOwnerId(); + final Long requestOwnerEpoch = consumerConfig.getOwnerEpoch(); + if (Objects.isNull(requestOwnerId) || Objects.isNull(requestOwnerEpoch)) { + return new HashMap<>(); + } + + final long now = System.currentTimeMillis(); + final long ownerLeaseRenewalThresholdMs = consumerConfig.getHeartbeatIntervalMs(); + final long ownerLeaseExtensionMs = consumerConfig.getHeartbeatIntervalMs() * 3; + final Map renewedTopicOwnerLeaseExpireTimeMs = new HashMap<>(); + acquireReadLock(); + try { + for (final String topicName : topicNames) { + final TopicMeta topicMeta = topicMetaKeeper.getTopicMeta(topicName); + if (Objects.isNull(topicMeta) + || !topicMeta.isOwnerFencingEnabled() + || !Objects.equals(topicMeta.getOwnerId(), requestOwnerId) + || topicMeta.getOwnerEpoch() != requestOwnerEpoch + || Objects.isNull(topicMeta.getOwnerLeaseExpireTimeMs())) { + continue; + } + + final long ownerLeaseRemainingTimeMs = topicMeta.getOwnerLeaseExpireTimeMs() - now; + if (ownerLeaseRemainingTimeMs >= 0 + && ownerLeaseRemainingTimeMs <= ownerLeaseRenewalThresholdMs) { + renewedTopicOwnerLeaseExpireTimeMs.put(topicName, now + ownerLeaseExtensionMs); + } + } + } finally { + releaseReadLock(); + } + return renewedTopicOwnerLeaseExpireTimeMs; + } + + public TSStatus renewLocalTopicOwnerLease( + final String topicName, + final String ownerId, + final long ownerEpoch, + final long ownerLeaseExpireTimeMs) { + acquireWriteLock(); + try { + final TopicMeta topicMeta = topicMetaKeeper.getTopicMeta(topicName); + if (Objects.isNull(topicMeta)) { + return RpcUtils.getStatus( + TSStatusCode.TOPIC_NOT_EXIST_ERROR, + String.format( + "Subscription: failed to renew topic %s owner lease locally because topic does not" + + " exist.", + topicName)); + } + topicMeta.renewOwnerLease(ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + return RpcUtils.SUCCESS_STATUS; + } catch (final IllegalArgumentException e) { + return RpcUtils.getStatus(TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT, e.getMessage()); + } finally { + releaseWriteLock(); + } + } } 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 fe08ffc43ca39..a76c20c0ef80a 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 @@ -29,6 +29,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo; +import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TShowDataNodesResp; import org.apache.iotdb.confignode.rpc.thrift.TSubscribeReq; import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; @@ -325,14 +326,17 @@ private TPipeSubscribeResp handlePipeSubscribeHeartbeatInternal( return SUBSCRIPTION_MISSING_CUSTOMER_RESP; } - // TODO: do something + final Set subscribedTopicNames = + SubscriptionAgent.consumer() + .getTopicNamesSubscribedByConsumer( + consumerConfig.getConsumerGroupId(), consumerConfig.getConsumerId()); + final TSStatus renewalStatus = renewTopicOwnerLeases(consumerConfig, subscribedTopicNames); + if (renewalStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribeHeartbeatResp.toTPipeSubscribeResp(renewalStatus); + } + final TSStatus ownerStatus = - SubscriptionAgent.topic() - .checkTopicOwners( - consumerConfig, - SubscriptionAgent.consumer() - .getTopicNamesSubscribedByConsumer( - consumerConfig.getConsumerGroupId(), consumerConfig.getConsumerId())); + SubscriptionAgent.topic().checkTopicOwners(consumerConfig, subscribedTopicNames); if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { return PipeSubscribeHeartbeatResp.toTPipeSubscribeResp(ownerStatus); } @@ -341,11 +345,7 @@ private TPipeSubscribeResp handlePipeSubscribeHeartbeatInternal( // fetch subscribed topics final Map topics = - SubscriptionAgent.topic() - .getTopicConfigs( - SubscriptionAgent.consumer() - .getTopicNamesSubscribedByConsumer( - consumerConfig.getConsumerGroupId(), consumerConfig.getConsumerId())); + SubscriptionAgent.topic().getTopicConfigs(subscribedTopicNames); // fetch available endpoints final Map endPoints = new HashMap<>(); @@ -387,6 +387,65 @@ private TPipeSubscribeResp handlePipeSubscribeHeartbeatInternal( RpcUtils.SUCCESS_STATUS, topics, endPoints, topicNamesToUnsubscribe); } + private TSStatus renewTopicOwnerLeases( + final ConsumerConfig consumerConfig, final Set subscribedTopicNames) + throws SubscriptionException { + final Map renewedTopicOwnerLeaseExpireTimeMs = + SubscriptionAgent.topic() + .getTopicOwnerLeaseRenewalExpireTimeMs(consumerConfig, subscribedTopicNames); + if (renewedTopicOwnerLeaseExpireTimeMs.isEmpty()) { + return RpcUtils.SUCCESS_STATUS; + } + + try (final ConfigNodeClient configNodeClient = + CONFIG_NODE_CLIENT_MANAGER.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) { + for (final Map.Entry entry : renewedTopicOwnerLeaseExpireTimeMs.entrySet()) { + final String topicName = entry.getKey(); + final long ownerLeaseExpireTimeMs = entry.getValue(); + final TSStatus status = + configNodeClient.renewTopicOwnerLease( + new TRenewTopicOwnerLeaseReq( + topicName, + consumerConfig.getOwnerId(), + consumerConfig.getOwnerEpoch(), + ownerLeaseExpireTimeMs)); + if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + LOGGER.warn( + "Subscription: failed to renew owner lease for topic {}, owner-id: {}," + + " owner-epoch: {}, result status is {}.", + topicName, + consumerConfig.getOwnerId(), + consumerConfig.getOwnerEpoch(), + status); + return status; + } + + final TSStatus localStatus = + SubscriptionAgent.topic() + .renewLocalTopicOwnerLease( + topicName, + consumerConfig.getOwnerId(), + consumerConfig.getOwnerEpoch(), + ownerLeaseExpireTimeMs); + if (localStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + LOGGER.warn( + "Subscription: failed to renew local owner lease for topic {}, owner-id: {}," + + " owner-epoch: {}, result status is {}.", + topicName, + consumerConfig.getOwnerId(), + consumerConfig.getOwnerEpoch(), + localStatus); + } + } + } catch (final ClientManagerException | TException e) { + throw new SubscriptionException( + String.format( + "Subscription: failed to renew owner leases for consumer %s, exception is %s.", + consumerConfig, e)); + } + return RpcUtils.SUCCESS_STATUS; + } + private TPipeSubscribeResp handlePipeSubscribeSubscribe(final PipeSubscribeSubscribeReq req) { try { return handlePipeSubscribeSubscribeInternal(req); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java index 6a8956e423b48..65237f3e1f54d 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java @@ -2351,6 +2351,14 @@ public void setPipeAutoSplitFullEnabled(boolean pipeAutoSplitFullEnabled) { this.pipeAutoSplitFullEnabled = pipeAutoSplitFullEnabled; } + public boolean getSubscriptionEnabled() { + return subscriptionEnabled; + } + + public void setSubscriptionEnabled(boolean subscriptionEnabled) { + this.subscriptionEnabled = subscriptionEnabled; + } + public float getSubscriptionCacheMemoryUsagePercentage() { return subscriptionCacheMemoryUsagePercentage; } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java index 5cd954a09f7b8..13a00a657de7b 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java @@ -311,6 +311,10 @@ public void loadCommonProps(TrimProperties properties) throws IOException { } private void loadSubscriptionProps(TrimProperties properties) { + config.setSubscriptionEnabled( + Boolean.parseBoolean( + properties.getProperty( + "subscription_enabled", String.valueOf(config.getSubscriptionEnabled())))); config.setSubscriptionCacheMemoryUsagePercentage( Float.parseFloat( properties.getProperty( diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java index 7802d37a69d26..c2365527350ea 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java @@ -31,7 +31,7 @@ public class SubscriptionConfig { private static final CommonConfig COMMON_CONFIG = CommonDescriptor.getInstance().getConfig(); public boolean getSubscriptionEnabled() { - return false; + return COMMON_CONFIG.getSubscriptionEnabled(); } public float getSubscriptionCacheMemoryUsagePercentage() { diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index 0f787f6101000..fa7e3ecb8cccb 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -92,6 +92,11 @@ public TopicMeta deepCopyWithUpdatedAttributes(final Map updated final Map copiedAttributes = new HashMap<>(config.getAttribute()); if (Objects.nonNull(updatedAttributes)) { copiedAttributes.putAll(updatedAttributes); + if ((updatedAttributes.containsKey(TopicConstant.OWNER_ID_KEY) + || updatedAttributes.containsKey(TopicConstant.OWNER_EPOCH_KEY)) + && !updatedAttributes.containsKey(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)) { + copiedAttributes.remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + } } final TopicMeta copied = new TopicMeta(topicName, creationTime, copiedAttributes); @@ -131,6 +136,12 @@ public Long getOwnerLeaseExpireTimeMs() { return ownerLeaseExpireTimeMs; } + public long getOwnerLeaseRemainingTimeMs() { + return Objects.nonNull(ownerLeaseExpireTimeMs) + ? ownerLeaseExpireTimeMs - System.currentTimeMillis() + : -1L; + } + public void transferOwner(final String ownerId, final long ownerEpoch) { transferOwner(ownerId, ownerEpoch, null); } @@ -175,6 +186,54 @@ public void transferOwner( } } + public void renewOwnerLease( + final String ownerId, final long ownerEpoch, final long ownerLeaseExpireTimeMs) { + if (!isOwnerFencingEnabled()) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease should not be renewed when owner fencing is disabled," + + " topic: %s", + topicName)); + } + if (!Objects.equals(this.ownerId, ownerId) || this.ownerEpoch != ownerEpoch) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease should be renewed by current owner, topic: %s," + + " current owner-id: %s, current owner-epoch: %s, request owner-id: %s," + + " request owner-epoch: %s", + topicName, this.ownerId, this.ownerEpoch, ownerId, ownerEpoch)); + } + if (Objects.nonNull(this.ownerLeaseExpireTimeMs) + && System.currentTimeMillis() > this.ownerLeaseExpireTimeMs) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease has expired, topic: %s, owner-id: %s," + + " owner-epoch: %s, owner-lease-expire-time-ms: %s", + topicName, this.ownerId, this.ownerEpoch, this.ownerLeaseExpireTimeMs)); + } + if (ownerLeaseExpireTimeMs <= System.currentTimeMillis()) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease expire time should be in the future, topic: %s," + + " owner-lease-expire-time-ms: %s", + topicName, ownerLeaseExpireTimeMs)); + } + if (Objects.nonNull(this.ownerLeaseExpireTimeMs) + && ownerLeaseExpireTimeMs <= this.ownerLeaseExpireTimeMs) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease expire time should increase, topic: %s," + + " current owner-lease-expire-time-ms: %s, request owner-lease-expire-time-ms:" + + " %s", + topicName, this.ownerLeaseExpireTimeMs, ownerLeaseExpireTimeMs)); + } + + this.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; + config + .getAttribute() + .put(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + } + public void clearOwner() { ownerId = null; ownerEpoch = -1L; diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java index d20464ee9c6ee..82cf436b92d72 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java @@ -124,6 +124,96 @@ public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndPreservesExistingC Assert.assertEquals(5L, topicMeta.getOwnerEpoch()); } + @Test + public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLease() { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, + String.valueOf(System.currentTimeMillis() + 60000)); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + final Map updatedAttributes = new HashMap<>(); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TopicMeta updatedTopicMeta = topicMeta.deepCopyWithUpdatedAttributes(updatedAttributes); + + Assert.assertEquals("sn2", updatedTopicMeta.getOwnerId()); + Assert.assertEquals(6L, updatedTopicMeta.getOwnerEpoch()); + Assert.assertNull(updatedTopicMeta.getOwnerLeaseExpireTimeMs()); + Assert.assertFalse( + updatedTopicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); + } + + @Test + public void testRenewTopicOwnerLease() { + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; + final long renewedOwnerLeaseExpireTimeMs = ownerLeaseExpireTimeMs + 60000; + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + topicMeta.renewOwnerLease("sn1", 5L, renewedOwnerLeaseExpireTimeMs); + + Assert.assertEquals( + renewedOwnerLeaseExpireTimeMs, topicMeta.getOwnerLeaseExpireTimeMs().longValue()); + Assert.assertEquals( + renewedOwnerLeaseExpireTimeMs, + topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); + } + + @Test + public void testRenewTopicOwnerLeaseRejectsInvalidProgression() { + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + Assert.assertThrows( + IllegalArgumentException.class, + () -> topicMeta.renewOwnerLease("sn2", 5L, ownerLeaseExpireTimeMs + 60000)); + Assert.assertThrows( + IllegalArgumentException.class, + () -> topicMeta.renewOwnerLease("sn1", 4L, ownerLeaseExpireTimeMs + 60000)); + Assert.assertThrows( + IllegalArgumentException.class, + () -> topicMeta.renewOwnerLease("sn1", 5L, ownerLeaseExpireTimeMs)); + Assert.assertThrows( + IllegalArgumentException.class, + () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() - 1)); + } + + @Test + public void testRenewTopicOwnerLeaseRejectsExpiredLease() { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, + String.valueOf(System.currentTimeMillis() - 1)); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + Assert.assertThrows( + IllegalArgumentException.class, + () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() + 60000)); + } + + @Test + public void testRenewTopicOwnerLeaseRejectsDisabledOwnerFencing() { + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, new HashMap<>()); + + Assert.assertThrows( + IllegalArgumentException.class, + () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() + 60000)); + } + @Test public void testSequentialTopicMetaDeserializeDoesNotConsumeNextTopic() throws IOException { final TopicMeta firstTopicMeta = new TopicMeta("first_topic", 1, new HashMap<>()); diff --git a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift index 2af5e4c6ccc10..a3ddc48bc208b 100644 --- a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift +++ b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift @@ -1012,6 +1012,13 @@ struct TAlterTopicReq { 3: required set subscribedConsumerGroupIds } +struct TRenewTopicOwnerLeaseReq { + 1: required string topicName + 2: required string ownerId + 3: required i64 ownerEpoch + 4: required i64 ownerLeaseExpireTimeMs +} + struct TGetAllTopicInfoResp { 1: required common.TSStatus status 2: required list allTopicInfo @@ -1933,6 +1940,9 @@ service IConfigNodeRPCService { /** Alter Topic */ common.TSStatus alterTopic(TAlterTopicReq req) + /** Renew topic owner lease */ + common.TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) + /** Drop Topic */ common.TSStatus dropTopic(string topicName) From 8621c5d32b5321725226b9e22a009f230e91c0d8 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 10 Jun 2026 17:45:07 +0000 Subject: [PATCH 06/14] Use duration-based subscription owner lease renewal --- .../local/IoTDBSubscriptionTopicOwnerIT.java | 10 +- .../subscription/config/TopicConstant.java | 1 + .../AbstractSubscriptionSession.java | 7 +- .../ISubscriptionTableSession.java | 6 +- .../ISubscriptionTreeSession.java | 6 +- .../SubscriptionTableSession.java | 4 +- .../subscription/SubscriptionTreeSession.java | 4 +- .../base/AbstractSubscriptionProvider.java | 11 ++- .../subscription/SubscriptionCoordinator.java | 4 +- .../subscription/SubscriptionInfoTest.java | 44 ++++++--- .../agent/SubscriptionTopicAgent.java | 42 +-------- .../receiver/SubscriptionReceiverV1.java | 29 ++---- .../subscription/meta/topic/TopicMeta.java | 91 +++++++++++++++++-- .../subscription/topic/TopicDeSerTest.java | 68 +++++++++++++- .../src/main/thrift/confignode.thrift | 2 +- 15 files changed, 223 insertions(+), 106 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index fb116b03e5734..6a03739b0e29c 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -137,7 +137,7 @@ public void testHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); final String topicName = "topic_owner_lease_renewal"; final long heartbeatIntervalMs = 1000L; - final long initialOwnerLeaseExpireTimeMs = System.currentTimeMillis() + 2500L; + final long ownerLeaseDurationMs = 2500L; try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { session.open(); @@ -147,10 +147,13 @@ public void testHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() properties.put(TopicConstant.OWNER_ID_KEY, "sn1"); properties.put(TopicConstant.OWNER_EPOCH_KEY, "5"); properties.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, - String.valueOf(initialOwnerLeaseExpireTimeMs)); + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); session.createTopic(topicName, properties); + final Long initialOwnerLeaseExpireTimeMs = getOwnerLeaseExpireTimeMs(session, topicName); + Assert.assertNotNull(initialOwnerLeaseExpireTimeMs); + Assert.assertTrue(initialOwnerLeaseExpireTimeMs > System.currentTimeMillis()); + try (final SubscriptionTreePullConsumer currentOwnerConsumer = new SubscriptionTreePullConsumer.Builder() .host(host) @@ -188,6 +191,7 @@ public void testHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() final String topicAttributes = getTopicAttributes(session, topicName); Assert.assertTrue(topicAttributes.contains("owner-id=sn2")); Assert.assertTrue(topicAttributes.contains("owner-epoch=6")); + Assert.assertFalse(topicAttributes.contains("owner-lease-duration-ms=")); Assert.assertFalse(topicAttributes.contains("owner-lease-expire-time-ms=")); } finally { session.dropTopicIfExists(topicName); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java index a982df359db9b..13dfc555fff9e 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java @@ -74,6 +74,7 @@ public class TopicConstant { public static final String OWNER_ID_KEY = "owner-id"; public static final String OWNER_EPOCH_KEY = "owner-epoch"; + public static final String OWNER_LEASE_DURATION_MS_KEY = "owner-lease-duration-ms"; public static final String OWNER_LEASE_EXPIRE_TIME_MS_KEY = "owner-lease-expire-time-ms"; private TopicConstant() { diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java index 53cd730b8cfbb..bcdc279dcff38 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java @@ -145,7 +145,7 @@ protected void alterTopicOwner( final String topicName, final String ownerId, final long ownerEpoch, - final Long ownerLeaseExpireTimeMs) + final Long ownerLeaseDurationMs) throws IoTDBConnectionException, StatementExecutionException { if (Objects.isNull(ownerId) || ownerId.isEmpty()) { throw new StatementExecutionException("Topic owner id should not be empty."); @@ -154,9 +154,9 @@ protected void alterTopicOwner( final Properties properties = new Properties(); properties.put(TopicConstant.OWNER_ID_KEY, ownerId); properties.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); - if (Objects.nonNull(ownerLeaseExpireTimeMs)) { + if (Objects.nonNull(ownerLeaseDurationMs)) { properties.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); } alterTopic(topicName, properties, true); } @@ -295,6 +295,7 @@ private static String escapeSqlStringLiteral(final String value) { private static boolean containsOwnerAttribute(final Properties properties) { return properties.containsKey(TopicConstant.OWNER_ID_KEY) || properties.containsKey(TopicConstant.OWNER_EPOCH_KEY) + || properties.containsKey(TopicConstant.OWNER_LEASE_DURATION_MS_KEY) || properties.containsKey(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); } } diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java index 2ec7471127add..a61ecd60ce39c 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTableSession.java @@ -125,13 +125,13 @@ void alterTopicOwner(final String topicName, final String ownerId, final long ow throws IoTDBConnectionException, StatementExecutionException; /** - * Transfers a topic owner to the specified owner id, epoch, and lease expire time. + * Transfers a topic owner to the specified owner id, epoch, and lease duration. * * @param topicName If the topic name contains single quotes, the passed parameter needs to be * enclosed in backticks. * @param ownerId The new topic owner id. * @param ownerEpoch The new monotonically increasing topic owner epoch. - * @param ownerLeaseExpireTimeMs The owner lease expire time in milliseconds. + * @param ownerLeaseDurationMs The owner lease duration in milliseconds. * @throws IoTDBConnectionException If a connection issue occurs. * @throws StatementExecutionException If the SQL statement execution fails. */ @@ -139,7 +139,7 @@ void alterTopicOwner( final String topicName, final String ownerId, final long ownerEpoch, - final Long ownerLeaseExpireTimeMs) + final Long ownerLeaseDurationMs) throws IoTDBConnectionException, StatementExecutionException; /** diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java index 7a827a207e843..eb443575857ca 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/ISubscriptionTreeSession.java @@ -125,13 +125,13 @@ void alterTopicOwner(final String topicName, final String ownerId, final long ow throws IoTDBConnectionException, StatementExecutionException; /** - * Transfers a topic owner to the specified owner id, epoch, and lease expire time. + * Transfers a topic owner to the specified owner id, epoch, and lease duration. * * @param topicName If the topic name contains single quotes, the passed parameter needs to be * enclosed in backticks. * @param ownerId The new topic owner id. * @param ownerEpoch The new monotonically increasing topic owner epoch. - * @param ownerLeaseExpireTimeMs The owner lease expire time in milliseconds. + * @param ownerLeaseDurationMs The owner lease duration in milliseconds. * @throws IoTDBConnectionException If a connection issue occurs. * @throws StatementExecutionException If the SQL statement execution fails. */ @@ -139,7 +139,7 @@ void alterTopicOwner( final String topicName, final String ownerId, final long ownerEpoch, - final Long ownerLeaseExpireTimeMs) + final Long ownerLeaseDurationMs) throws IoTDBConnectionException, StatementExecutionException; /** diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java index b666297052d83..123deea2fa45d 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTableSession.java @@ -91,9 +91,9 @@ public void alterTopicOwner( final String topicName, final String ownerId, final long ownerEpoch, - final Long ownerLeaseExpireTimeMs) + final Long ownerLeaseDurationMs) throws IoTDBConnectionException, StatementExecutionException { - super.alterTopicOwner(topicName, ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + super.alterTopicOwner(topicName, ownerId, ownerEpoch, ownerLeaseDurationMs); } @Override diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java index 3b921c5270a3e..f429bf455f31f 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionTreeSession.java @@ -114,9 +114,9 @@ public void alterTopicOwner( final String topicName, final String ownerId, final long ownerEpoch, - final Long ownerLeaseExpireTimeMs) + final Long ownerLeaseDurationMs) throws IoTDBConnectionException, StatementExecutionException { - super.alterTopicOwner(topicName, ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + super.alterTopicOwner(topicName, ownerId, ownerEpoch, ownerLeaseDurationMs); } @Override 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 33a31d485dfeb..4aad7fe3681b0 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 @@ -606,6 +606,7 @@ private static void verifyPipeSubscribeSuccess(final TSStatus status) case 1906: // SUBSCRIPTION_CLOSE_ERROR case 1907: // SUBSCRIPTION_SUBSCRIBE_ERROR case 1908: // SUBSCRIPTION_UNSUBSCRIBE_ERROR + case 1913: // SUBSCRIPTION_SEEK_ERROR { final String errorMessage = String.format(INTERNAL_ERROR_FORMATTER, status.code, status.message); @@ -615,11 +616,11 @@ private static void verifyPipeSubscribeSuccess(final TSStatus status) case 1911: // SUBSCRIPTION_PIPE_TIMEOUT_ERROR throw new SubscriptionPipeTimeoutException( String.format(SUBSCRIPTION_PIPE_TIMEOUT_FORMATTER, status.code, status.message)); - case 1913: // SUBSCRIPTION_OWNER_FENCED - case 1914: // SUBSCRIPTION_OWNER_REQUIRED - case 1915: // SUBSCRIPTION_OWNER_EPOCH_REQUIRED - case 1916: // SUBSCRIPTION_OWNER_LEASE_EXPIRED - case 1917: // SUBSCRIPTION_OWNER_EPOCH_CONFLICT + case 1914: // SUBSCRIPTION_OWNER_FENCED + case 1915: // SUBSCRIPTION_OWNER_REQUIRED + case 1916: // SUBSCRIPTION_OWNER_EPOCH_REQUIRED + case 1917: // SUBSCRIPTION_OWNER_LEASE_EXPIRED + case 1918: // SUBSCRIPTION_OWNER_EPOCH_CONFLICT { final String errorMessage = String.format(INTERNAL_ERROR_FORMATTER, status.code, status.message); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index a7ff36441b783..a53acdb7e2a8d 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -239,8 +239,8 @@ public TopicMeta buildRenewedTopicOwnerLeaseMeta(TRenewTopicOwnerLeaseReq req) { return null; } - topicMeta.renewOwnerLease( - req.getOwnerId(), req.getOwnerEpoch(), req.getOwnerLeaseExpireTimeMs()); + topicMeta.renewOwnerLeaseWithDuration( + req.getOwnerId(), req.getOwnerEpoch(), req.getOwnerLeaseDurationMs()); return topicMeta; } diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index 961da7c63914b..4d2d71750804f 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -143,21 +143,25 @@ public void testAlterTopicRenewsOwnerLease() { final String topicName = "topic-" + UUID.randomUUID(); final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; - final long renewedOwnerLeaseExpireTimeMs = ownerLeaseExpireTimeMs + 60000; + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 1000; + final long ownerLeaseDurationMs = 60000; final TopicMeta initialTopicMeta = - createTopicMeta(topicName, "sn1", 5L, ownerLeaseExpireTimeMs); + createTopicMeta(topicName, "sn1", 5L, ownerLeaseDurationMs, ownerLeaseExpireTimeMs); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); final TopicMeta renewedTopicMeta = initialTopicMeta.deepCopy(); - renewedTopicMeta.renewOwnerLease("sn1", 5L, renewedOwnerLeaseExpireTimeMs); + final long renewedOwnerLeaseExpireTimeMs = + renewedTopicMeta.renewOwnerLeaseWithDuration("sn1", 5L, ownerLeaseDurationMs); final TSStatus alterStatus = subscriptionInfo.alterTopic(new AlterTopicPlan(renewedTopicMeta)); Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); Assert.assertEquals("sn1", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); Assert.assertEquals(5L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); + Assert.assertEquals( + ownerLeaseDurationMs, + subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseDurationMs().longValue()); Assert.assertEquals( renewedOwnerLeaseExpireTimeMs, subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs().longValue()); @@ -166,7 +170,7 @@ public void testAlterTopicRenewsOwnerLease() { @Test public void testAlterTopicOwnerAndShowTopicOwner() { final String topicName = "topic-" + UUID.randomUUID(); - final long ownerLeaseExpireTimeMs = 123456789L; + final long ownerLeaseDurationMs = 123456L; final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); @@ -180,14 +184,12 @@ public void testAlterTopicOwnerAndShowTopicOwner() { updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); updatedAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + final TopicMeta updatedTopicMeta = + initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), - subscriptionInfo - .alterTopic( - new AlterTopicPlan( - initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))) - .getCode()); + subscriptionInfo.alterTopic(new AlterTopicPlan(updatedTopicMeta)).getCode()); final TShowTopicResp showTopicResp = ((TopicTableResp) subscriptionInfo.showTopics()).convertToTShowTopicResp(); @@ -205,7 +207,12 @@ public void testAlterTopicOwnerAndShowTopicOwner() { Assert.assertTrue( showTopicInfo .getTopicAttributes() - .contains("owner-lease-expire-time-ms=" + ownerLeaseExpireTimeMs)); + .contains("owner-lease-duration-ms=" + ownerLeaseDurationMs)); + Assert.assertTrue( + showTopicInfo + .getTopicAttributes() + .contains( + "owner-lease-expire-time-ms=" + updatedTopicMeta.getOwnerLeaseExpireTimeMs())); } private TopicMeta createTopicMeta( @@ -221,9 +228,22 @@ private TopicMeta createTopicMeta( final String ownerId, final long ownerEpoch, final long ownerLeaseExpireTimeMs) { + return createTopicMeta(topicName, ownerId, ownerEpoch, null, ownerLeaseExpireTimeMs); + } + + private TopicMeta createTopicMeta( + final String topicName, + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseDurationMs, + final long ownerLeaseExpireTimeMs) { final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.OWNER_ID_KEY, ownerId); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + if (ownerLeaseDurationMs != null) { + topicAttributes.put( + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + } topicAttributes.put( TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); return new TopicMeta(topicName, 1, topicAttributes); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java index 6fd552214ad8b..1bb11be3752d6 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java @@ -270,7 +270,7 @@ public TSStatus checkTopicOwners( return RpcUtils.SUCCESS_STATUS; } - public Map getTopicOwnerLeaseRenewalExpireTimeMs( + public Map getTopicOwnerLeaseRenewalDurationMs( final ConsumerConfig consumerConfig, final Iterable topicNames) { final String requestOwnerId = consumerConfig.getOwnerId(); final Long requestOwnerEpoch = consumerConfig.getOwnerEpoch(); @@ -278,10 +278,7 @@ public Map getTopicOwnerLeaseRenewalExpireTimeMs( return new HashMap<>(); } - final long now = System.currentTimeMillis(); - final long ownerLeaseRenewalThresholdMs = consumerConfig.getHeartbeatIntervalMs(); - final long ownerLeaseExtensionMs = consumerConfig.getHeartbeatIntervalMs() * 3; - final Map renewedTopicOwnerLeaseExpireTimeMs = new HashMap<>(); + final Map renewedTopicOwnerLeaseDurationMs = new HashMap<>(); acquireReadLock(); try { for (final String topicName : topicNames) { @@ -290,44 +287,15 @@ public Map getTopicOwnerLeaseRenewalExpireTimeMs( || !topicMeta.isOwnerFencingEnabled() || !Objects.equals(topicMeta.getOwnerId(), requestOwnerId) || topicMeta.getOwnerEpoch() != requestOwnerEpoch - || Objects.isNull(topicMeta.getOwnerLeaseExpireTimeMs())) { + || Objects.isNull(topicMeta.getOwnerLeaseDurationMs())) { continue; } - final long ownerLeaseRemainingTimeMs = topicMeta.getOwnerLeaseExpireTimeMs() - now; - if (ownerLeaseRemainingTimeMs >= 0 - && ownerLeaseRemainingTimeMs <= ownerLeaseRenewalThresholdMs) { - renewedTopicOwnerLeaseExpireTimeMs.put(topicName, now + ownerLeaseExtensionMs); - } + renewedTopicOwnerLeaseDurationMs.put(topicName, topicMeta.getOwnerLeaseDurationMs()); } } finally { releaseReadLock(); } - return renewedTopicOwnerLeaseExpireTimeMs; - } - - public TSStatus renewLocalTopicOwnerLease( - final String topicName, - final String ownerId, - final long ownerEpoch, - final long ownerLeaseExpireTimeMs) { - acquireWriteLock(); - try { - final TopicMeta topicMeta = topicMetaKeeper.getTopicMeta(topicName); - if (Objects.isNull(topicMeta)) { - return RpcUtils.getStatus( - TSStatusCode.TOPIC_NOT_EXIST_ERROR, - String.format( - "Subscription: failed to renew topic %s owner lease locally because topic does not" - + " exist.", - topicName)); - } - topicMeta.renewOwnerLease(ownerId, ownerEpoch, ownerLeaseExpireTimeMs); - return RpcUtils.SUCCESS_STATUS; - } catch (final IllegalArgumentException e) { - return RpcUtils.getStatus(TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT, e.getMessage()); - } finally { - releaseWriteLock(); - } + return renewedTopicOwnerLeaseDurationMs; } } 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 72204392878e9..609b782fff6e0 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 @@ -412,25 +412,25 @@ private TPipeSubscribeResp handleSubscriptionHeartbeatInternal(final Subscriptio private TSStatus renewTopicOwnerLeases( final ConsumerConfig consumerConfig, final Set subscribedTopicNames) throws SubscriptionException { - final Map renewedTopicOwnerLeaseExpireTimeMs = + final Map renewedTopicOwnerLeaseDurationMs = SubscriptionAgent.topic() - .getTopicOwnerLeaseRenewalExpireTimeMs(consumerConfig, subscribedTopicNames); - if (renewedTopicOwnerLeaseExpireTimeMs.isEmpty()) { + .getTopicOwnerLeaseRenewalDurationMs(consumerConfig, subscribedTopicNames); + if (renewedTopicOwnerLeaseDurationMs.isEmpty()) { return RpcUtils.SUCCESS_STATUS; } try (final ConfigNodeClient configNodeClient = CONFIG_NODE_CLIENT_MANAGER.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) { - for (final Map.Entry entry : renewedTopicOwnerLeaseExpireTimeMs.entrySet()) { + for (final Map.Entry entry : renewedTopicOwnerLeaseDurationMs.entrySet()) { final String topicName = entry.getKey(); - final long ownerLeaseExpireTimeMs = entry.getValue(); + final long ownerLeaseDurationMs = entry.getValue(); final TSStatus status = configNodeClient.renewTopicOwnerLease( new TRenewTopicOwnerLeaseReq( topicName, consumerConfig.getOwnerId(), consumerConfig.getOwnerEpoch(), - ownerLeaseExpireTimeMs)); + ownerLeaseDurationMs)); if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { LOGGER.warn( "Subscription: failed to renew owner lease for topic {}, owner-id: {}," @@ -441,23 +441,6 @@ private TSStatus renewTopicOwnerLeases( status); return status; } - - final TSStatus localStatus = - SubscriptionAgent.topic() - .renewLocalTopicOwnerLease( - topicName, - consumerConfig.getOwnerId(), - consumerConfig.getOwnerEpoch(), - ownerLeaseExpireTimeMs); - if (localStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - LOGGER.warn( - "Subscription: failed to renew local owner lease for topic {}, owner-id: {}," - + " owner-epoch: {}, result status is {}.", - topicName, - consumerConfig.getOwnerId(), - consumerConfig.getOwnerEpoch(), - localStatus); - } } } catch (final ClientManagerException | TException e) { throw new SubscriptionException( diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index fa7e3ecb8cccb..427caa1648c2b 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -49,6 +49,7 @@ public class TopicMeta { private String ownerId; private long ownerEpoch; private long ownerLastTransferTimeMs; + private Long ownerLeaseDurationMs; private Long ownerLeaseExpireTimeMs; // TODO: remove this variable later @@ -82,6 +83,7 @@ public TopicMeta deepCopy() { copied.ownerId = ownerId; copied.ownerEpoch = ownerEpoch; copied.ownerLastTransferTimeMs = ownerLastTransferTimeMs; + copied.ownerLeaseDurationMs = ownerLeaseDurationMs; copied.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; copied.subscribedConsumerGroupIds = new HashSet<>(subscribedConsumerGroupIds); @@ -97,6 +99,11 @@ public TopicMeta deepCopyWithUpdatedAttributes(final Map updated && !updatedAttributes.containsKey(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)) { copiedAttributes.remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); } + if ((updatedAttributes.containsKey(TopicConstant.OWNER_ID_KEY) + || updatedAttributes.containsKey(TopicConstant.OWNER_EPOCH_KEY)) + && !updatedAttributes.containsKey(TopicConstant.OWNER_LEASE_DURATION_MS_KEY)) { + copiedAttributes.remove(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); + } } final TopicMeta copied = new TopicMeta(topicName, creationTime, copiedAttributes); @@ -136,6 +143,10 @@ public Long getOwnerLeaseExpireTimeMs() { return ownerLeaseExpireTimeMs; } + public Long getOwnerLeaseDurationMs() { + return ownerLeaseDurationMs; + } + public long getOwnerLeaseRemainingTimeMs() { return Objects.nonNull(ownerLeaseExpireTimeMs) ? ownerLeaseExpireTimeMs - System.currentTimeMillis() @@ -148,6 +159,14 @@ public void transferOwner(final String ownerId, final long ownerEpoch) { public void transferOwner( final String ownerId, final long ownerEpoch, final Long ownerLeaseExpireTimeMs) { + transferOwner(ownerId, ownerEpoch, null, ownerLeaseExpireTimeMs); + } + + public void transferOwner( + final String ownerId, + final long ownerEpoch, + final Long ownerLeaseDurationMs, + final Long ownerLeaseExpireTimeMs) { if (Objects.isNull(ownerId) || ownerId.isEmpty()) { throw new IllegalArgumentException("Subscription topic owner id should not be empty"); } @@ -168,14 +187,29 @@ public void transferOwner( + " current owner-epoch: %s, current owner-lease-expire-time-ms: %s", topicName, this.ownerId, this.ownerEpoch, this.ownerLeaseExpireTimeMs)); } + if (Objects.nonNull(ownerLeaseDurationMs) && ownerLeaseDurationMs <= 0) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease duration should be positive, topic: %s," + + " owner-lease-duration-ms: %s", + topicName, ownerLeaseDurationMs)); + } this.ownerId = ownerId; this.ownerEpoch = ownerEpoch; + this.ownerLeaseDurationMs = ownerLeaseDurationMs; this.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; this.ownerLastTransferTimeMs = System.currentTimeMillis(); config.getAttribute().put(TopicConstant.OWNER_ID_KEY, ownerId); config.getAttribute().put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + if (Objects.nonNull(ownerLeaseDurationMs)) { + config + .getAttribute() + .put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + } else { + config.getAttribute().remove(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); + } if (Objects.nonNull(ownerLeaseExpireTimeMs)) { config .getAttribute() @@ -219,10 +253,10 @@ public void renewOwnerLease( topicName, ownerLeaseExpireTimeMs)); } if (Objects.nonNull(this.ownerLeaseExpireTimeMs) - && ownerLeaseExpireTimeMs <= this.ownerLeaseExpireTimeMs) { + && ownerLeaseExpireTimeMs < this.ownerLeaseExpireTimeMs) { throw new IllegalArgumentException( String.format( - "Subscription topic owner lease expire time should increase, topic: %s," + "Subscription topic owner lease expire time should not decrease, topic: %s," + " current owner-lease-expire-time-ms: %s, request owner-lease-expire-time-ms:" + " %s", topicName, this.ownerLeaseExpireTimeMs, ownerLeaseExpireTimeMs)); @@ -234,13 +268,37 @@ public void renewOwnerLease( .put(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); } + public long renewOwnerLeaseWithDuration( + final String ownerId, final long ownerEpoch, final long ownerLeaseDurationMs) { + if (ownerLeaseDurationMs <= 0) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease duration should be positive, topic: %s," + + " owner-lease-duration-ms: %s", + topicName, ownerLeaseDurationMs)); + } + final long requestedOwnerLeaseExpireTimeMs = System.currentTimeMillis() + ownerLeaseDurationMs; + final long ownerLeaseExpireTimeMs = + Objects.nonNull(this.ownerLeaseExpireTimeMs) + ? Math.max(requestedOwnerLeaseExpireTimeMs, this.ownerLeaseExpireTimeMs) + : requestedOwnerLeaseExpireTimeMs; + renewOwnerLease(ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + this.ownerLeaseDurationMs = ownerLeaseDurationMs; + config + .getAttribute() + .put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + return ownerLeaseExpireTimeMs; + } + public void clearOwner() { ownerId = null; ownerEpoch = -1L; ownerLastTransferTimeMs = -1L; + ownerLeaseDurationMs = null; ownerLeaseExpireTimeMs = null; config.getAttribute().remove(TopicConstant.OWNER_ID_KEY); config.getAttribute().remove(TopicConstant.OWNER_EPOCH_KEY); + config.getAttribute().remove(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); config.getAttribute().remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); } @@ -423,10 +481,22 @@ private void initOwnerFromTopicAttributes(final Map topicAttribu "Subscription topic owner epoch should be set when %s is set", TopicConstant.OWNER_ID_KEY)); } + final Long ownerLeaseDurationMs = + topicConfig.getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); + Long ownerLeaseExpireTimeMs = topicConfig.getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + if (Objects.nonNull(ownerLeaseDurationMs)) { + if (ownerLeaseDurationMs <= 0) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease duration should be positive when %s is set", + TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); + } + if (Objects.isNull(ownerLeaseExpireTimeMs)) { + ownerLeaseExpireTimeMs = System.currentTimeMillis() + ownerLeaseDurationMs; + } + } transferOwner( - configuredOwnerId, - configuredOwnerEpoch, - topicConfig.getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); + configuredOwnerId, configuredOwnerEpoch, ownerLeaseDurationMs, ownerLeaseExpireTimeMs); } /////////////////////////////// utilities /////////////////////////////// @@ -509,13 +579,20 @@ public boolean equals(final Object obj) { && Objects.equals(config, that.config) && Objects.equals(ownerId, that.ownerId) && ownerEpoch == that.ownerEpoch + && Objects.equals(ownerLeaseDurationMs, that.ownerLeaseDurationMs) && Objects.equals(ownerLeaseExpireTimeMs, that.ownerLeaseExpireTimeMs); } @Override public int hashCode() { return Objects.hash( - topicName, creationTime, config, ownerId, ownerEpoch, ownerLeaseExpireTimeMs); + topicName, + creationTime, + config, + ownerId, + ownerEpoch, + ownerLeaseDurationMs, + ownerLeaseExpireTimeMs); } @Override @@ -533,6 +610,8 @@ public String toString() { + ownerEpoch + ", ownerLastTransferTimeMs=" + ownerLastTransferTimeMs + + ", ownerLeaseDurationMs=" + + ownerLeaseDurationMs + ", ownerLeaseExpireTimeMs=" + ownerLeaseExpireTimeMs + '}'; diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java index 82cf436b92d72..f50d9f9ca83a0 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java @@ -80,6 +80,7 @@ public void testTopicOwnerDeSer() throws IOException { Assert.assertEquals(topicMeta, topicMeta2); Assert.assertEquals(topicMeta.getOwnerId(), topicMeta2.getOwnerId()); Assert.assertEquals(topicMeta.getOwnerEpoch(), topicMeta2.getOwnerEpoch()); + Assert.assertEquals(topicMeta.getOwnerLeaseDurationMs(), topicMeta2.getOwnerLeaseDurationMs()); Assert.assertEquals( topicMeta.getOwnerLeaseExpireTimeMs(), topicMeta2.getOwnerLeaseExpireTimeMs()); @@ -97,6 +98,8 @@ public void testTopicOwnerDeSer() throws IOException { Assert.assertFalse(topicMeta.isOwnerFencingEnabled()); Assert.assertFalse(topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_ID_KEY)); Assert.assertFalse(topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_EPOCH_KEY)); + Assert.assertFalse( + topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); Assert.assertFalse( topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); } @@ -132,6 +135,7 @@ public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLeas topicAttributes.put( TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(System.currentTimeMillis() + 60000)); + topicAttributes.put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, "60000"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); final Map updatedAttributes = new HashMap<>(); @@ -141,29 +145,83 @@ public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLeas Assert.assertEquals("sn2", updatedTopicMeta.getOwnerId()); Assert.assertEquals(6L, updatedTopicMeta.getOwnerEpoch()); + Assert.assertNull(updatedTopicMeta.getOwnerLeaseDurationMs()); Assert.assertNull(updatedTopicMeta.getOwnerLeaseExpireTimeMs()); + Assert.assertFalse( + updatedTopicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); Assert.assertFalse( updatedTopicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); } @Test public void testRenewTopicOwnerLease() { - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; - final long renewedOwnerLeaseExpireTimeMs = ownerLeaseExpireTimeMs + 60000; + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 1000; + final long ownerLeaseDurationMs = 60000; final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); topicAttributes.put( TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); - topicMeta.renewOwnerLease("sn1", 5L, renewedOwnerLeaseExpireTimeMs); + final long renewedOwnerLeaseExpireTimeMs = + topicMeta.renewOwnerLeaseWithDuration("sn1", 5L, ownerLeaseDurationMs); + Assert.assertTrue(renewedOwnerLeaseExpireTimeMs > ownerLeaseExpireTimeMs); + Assert.assertEquals(ownerLeaseDurationMs, topicMeta.getOwnerLeaseDurationMs().longValue()); Assert.assertEquals( renewedOwnerLeaseExpireTimeMs, topicMeta.getOwnerLeaseExpireTimeMs().longValue()); Assert.assertEquals( renewedOwnerLeaseExpireTimeMs, topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); + Assert.assertEquals( + ownerLeaseDurationMs, + topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY).longValue()); + } + + @Test + public void testRenewTopicOwnerLeaseWithDurationDoesNotDecreaseExpireTime() { + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; + final long ownerLeaseDurationMs = 1000; + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + topicAttributes.put( + TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + final long renewedOwnerLeaseExpireTimeMs = + topicMeta.renewOwnerLeaseWithDuration("sn1", 5L, ownerLeaseDurationMs); + + Assert.assertEquals(ownerLeaseExpireTimeMs, renewedOwnerLeaseExpireTimeMs); + Assert.assertEquals(ownerLeaseExpireTimeMs, topicMeta.getOwnerLeaseExpireTimeMs().longValue()); + } + + @Test + public void testOwnerLeaseDurationGeneratesExpireTime() { + final long createStartTimeMs = System.currentTimeMillis(); + final long ownerLeaseDurationMs = 60000; + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + topicAttributes.put( + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + Assert.assertEquals(ownerLeaseDurationMs, topicMeta.getOwnerLeaseDurationMs().longValue()); + Assert.assertNotNull(topicMeta.getOwnerLeaseExpireTimeMs()); + Assert.assertTrue( + topicMeta.getOwnerLeaseExpireTimeMs() >= createStartTimeMs + ownerLeaseDurationMs); + Assert.assertTrue( + topicMeta.getOwnerLeaseExpireTimeMs() <= System.currentTimeMillis() + ownerLeaseDurationMs); + Assert.assertEquals( + topicMeta.getOwnerLeaseExpireTimeMs().longValue(), + topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); } @Test @@ -184,10 +242,12 @@ public void testRenewTopicOwnerLeaseRejectsInvalidProgression() { () -> topicMeta.renewOwnerLease("sn1", 4L, ownerLeaseExpireTimeMs + 60000)); Assert.assertThrows( IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn1", 5L, ownerLeaseExpireTimeMs)); + () -> topicMeta.renewOwnerLease("sn1", 5L, ownerLeaseExpireTimeMs - 1)); Assert.assertThrows( IllegalArgumentException.class, () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() - 1)); + Assert.assertThrows( + IllegalArgumentException.class, () -> topicMeta.renewOwnerLeaseWithDuration("sn1", 5L, 0)); } @Test diff --git a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift index d3f18aa806da2..01eb21b43abbd 100644 --- a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift +++ b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift @@ -1016,7 +1016,7 @@ struct TRenewTopicOwnerLeaseReq { 1: required string topicName 2: required string ownerId 3: required i64 ownerEpoch - 4: required i64 ownerLeaseExpireTimeMs + 4: required i64 ownerLeaseDurationMs } struct TGetAllTopicInfoResp { From 19e285247ac57608c6c6fbbbbefc4b0b70632779 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 11 Jun 2026 05:21:42 +0000 Subject: [PATCH 07/14] Renew subscription owner leases in ConfigNode heartbeat --- .../local/IoTDBSubscriptionTopicOwnerIT.java | 3 +- .../confignode/manager/ConfigManager.java | 9 ---- .../iotdb/confignode/manager/IManager.java | 4 -- .../confignode/manager/ProcedureManager.java | 31 ------------ .../load/service/HeartbeatService.java | 10 ++++ .../subscription/SubscriptionCoordinator.java | 47 ++++++++----------- .../subscription/SubscriptionInfo.java | 27 +++++++++++ .../thrift/ConfigNodeRPCServiceProcessor.java | 6 --- .../subscription/SubscriptionInfoTest.java | 40 ++++++++++++++++ .../db/protocol/client/ConfigNodeClient.java | 7 --- .../impl/DataNodeInternalRPCServiceImpl.java | 14 ++++++ .../agent/SubscriptionTopicAgent.java | 30 ------------ .../receiver/SubscriptionReceiverV1.java | 47 ------------------- .../subscription/meta/topic/TopicMeta.java | 10 ++++ .../src/main/thrift/confignode.thrift | 10 ---- .../src/main/thrift/datanode.thrift | 1 + 16 files changed, 123 insertions(+), 173 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index 6a03739b0e29c..7b1a9da59e4e8 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -132,7 +132,8 @@ public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws } @Test - public void testHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() throws Exception { + public void testConfigNodeHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() + throws Exception { final String host = EnvFactory.getEnv().getIP(); final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); final String topicName = "topic_owner_lease_renewal"; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java index 4f92d985f0080..305905bfbbaa3 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java @@ -223,7 +223,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionTableResp; import org.apache.iotdb.confignode.rpc.thrift.TSetDataNodeStatusReq; @@ -2478,14 +2477,6 @@ public TSStatus alterTopic(TAlterTopicReq req) { : status; } - @Override - public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { - TSStatus status = confirmLeader(); - return status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode() - ? subscriptionManager.getSubscriptionCoordinator().renewTopicOwnerLease(req) - : status; - } - @Override public TSStatus dropTopic(TDropTopicReq req) { TSStatus status = confirmLeader(); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java index ae55ec211e273..fa708dbaeca83 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/IManager.java @@ -138,7 +138,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionTableResp; import org.apache.iotdb.confignode.rpc.thrift.TSetDataNodeStatusReq; @@ -808,9 +807,6 @@ TPermissionInfoResp login( /** Alter Topic. */ TSStatus alterTopic(TAlterTopicReq req); - /** Renew Topic Owner Lease. */ - TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req); - /** Drop Topic. */ TSStatus dropTopic(TDropTopicReq req); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java index 9efef57feaea1..4f01fc09b9c09 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java @@ -156,7 +156,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TMigrateRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSubscribeReq; import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; import org.apache.iotdb.consensus.ConsensusFactory; @@ -1801,36 +1800,6 @@ public TSStatus alterTopic(TAlterTopicReq req) { } } - public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { - try { - final TopicMeta updatedTopicMeta = - configManager - .getSubscriptionManager() - .getSubscriptionCoordinator() - .buildRenewedTopicOwnerLeaseMeta(req); - if (updatedTopicMeta == null) { - return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) - .setMessage( - String.format( - ManagerMessages.FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED, - req.getTopicName())); - } - - AlterTopicProcedure procedure = new AlterTopicProcedure(updatedTopicMeta); - executor.submitProcedure(procedure); - TSStatus status = waitingProcedureFinished(procedure); - if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - return status; - } else { - return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) - .setMessage(wrapTimeoutMessageForPipeProcedure(status.getMessage())); - } - } catch (Exception e) { - return new TSStatus(TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode()) - .setMessage(e.getMessage()); - } - } - public TSStatus dropTopic(String topicName) { try { DropTopicProcedure procedure = new DropTopicProcedure(topicName); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java index a491b3960c3c3..b159c89aaa5da 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java @@ -49,6 +49,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; import java.util.List; import java.util.Optional; import java.util.Set; @@ -174,6 +175,15 @@ protected TDataNodeHeartbeatReq genHeartbeatReq() { configManager.getProcedureManager().getRegionOperationConsensusIds()); } + final List renewedTopicMetas = + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .renewTopicOwnerLeasesAndSerialize(); + if (!renewedTopicMetas.isEmpty()) { + heartbeatReq.setTopicMetas(renewedTopicMetas); + } + /* Update heartbeat counter */ heartbeatCounter.getAndIncrement(); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index a53acdb7e2a8d..160e9b9a35ac3 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -37,7 +37,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TDropTopicReq; import org.apache.iotdb.confignode.rpc.thrift.TGetAllSubscriptionInfoResp; import org.apache.iotdb.confignode.rpc.thrift.TGetAllTopicInfoResp; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TShowSubscriptionReq; import org.apache.iotdb.confignode.rpc.thrift.TShowSubscriptionResp; import org.apache.iotdb.confignode.rpc.thrift.TShowTopicReq; @@ -51,8 +50,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -212,36 +215,24 @@ public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) { req.getTopicName(), req.getTopicAttributes()); } - public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { - if (blockedOwnerLeaseRenewalTopics.contains(req.getTopicName())) { - return RpcUtils.getStatus( - TSStatusCode.SUBSCRIPTION_OWNER_FENCED, - String.format( - "Subscription: topic %s owner lease renewal is blocked by a pending owner transfer.", - req.getTopicName())); + public List renewTopicOwnerLeasesAndSerialize() { + final Set blockedTopicNames; + synchronized (blockedOwnerLeaseRenewalTopics) { + blockedTopicNames = new HashSet<>(blockedOwnerLeaseRenewalTopics); } - final TSStatus status = configManager.getProcedureManager().renewTopicOwnerLease(req); - if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - LOGGER.warn( - "Failed to renew topic {} owner lease for owner {} epoch {}, result status is {}.", - req.getTopicName(), - req.getOwnerId(), - req.getOwnerEpoch(), - status); - } - return status; - } - - public TopicMeta buildRenewedTopicOwnerLeaseMeta(TRenewTopicOwnerLeaseReq req) { - final TopicMeta topicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); - if (Objects.isNull(topicMeta)) { - return null; + final List renewedTopicMetaBinaryList = new ArrayList<>(); + for (final TopicMeta topicMeta : subscriptionInfo.renewTopicOwnerLeases(blockedTopicNames)) { + try { + renewedTopicMetaBinaryList.add(topicMeta.serialize()); + } catch (final IOException e) { + LOGGER.warn( + "Failed to serialize renewed owner lease topic meta for topic {}.", + topicMeta.getTopicName(), + e); + } } - - topicMeta.renewOwnerLeaseWithDuration( - req.getOwnerId(), req.getOwnerEpoch(), req.getOwnerLeaseDurationMs()); - return topicMeta; + return renewedTopicMetaBinaryList; } public TSStatus dropTopic(TDropTopicReq req) { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java index f6e64dcf32224..b4fa66827603a 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java @@ -564,6 +564,33 @@ public TopicMeta deepCopyTopicMetaWithUpdatedAttributes( } } + public List renewTopicOwnerLeases(final Set blockedTopicNames) { + acquireWriteLock(); + try { + final List renewedTopicMetas = new ArrayList<>(); + for (final TopicMeta topicMeta : topicMetaKeeper.getAllTopicMeta()) { + if (!topicMeta.isOwnerFencingEnabled() + || Objects.isNull(topicMeta.getOwnerLeaseDurationMs()) + || blockedTopicNames.contains(topicMeta.getTopicName())) { + continue; + } + + try { + topicMeta.renewOwnerLeaseWithConfiguredDuration(); + renewedTopicMetas.add(topicMeta.deepCopy()); + } catch (final IllegalArgumentException e) { + LOGGER.warn( + "Failed to renew subscription topic {} owner lease during ConfigNode heartbeat.", + topicMeta.getTopicName(), + e); + } + } + return renewedTopicMetas; + } finally { + releaseWriteLock(); + } + } + public DataSet showTopics() { acquireReadLock(); try { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java index 96c87a280a00f..5b26a6f7d6dc7 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/service/thrift/ConfigNodeRPCServiceProcessor.java @@ -191,7 +191,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionReq; @@ -1282,11 +1281,6 @@ public TSStatus alterTopic(TAlterTopicReq req) { return configManager.alterTopic(req); } - @Override - public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) { - return configManager.renewTopicOwnerLease(req); - } - @Override public TSStatus dropTopic(String topicName) { return configManager.dropTopic( diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index 4d2d71750804f..036357a1385be 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -32,7 +32,9 @@ import org.junit.Assert; import org.junit.Test; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -167,6 +169,44 @@ public void testAlterTopicRenewsOwnerLease() { subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs().longValue()); } + @Test + public void testRenewTopicOwnerLeasesSkipsBlockedTopic() { + final String topicName = "topic-" + UUID.randomUUID(); + final String blockedTopicName = "topic-" + UUID.randomUUID(); + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + + final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 1000; + final long ownerLeaseDurationMs = 60000; + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo + .createTopic( + new CreateTopicPlan( + createTopicMeta( + topicName, "sn1", 5L, ownerLeaseDurationMs, ownerLeaseExpireTimeMs))) + .getCode()); + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + subscriptionInfo + .createTopic( + new CreateTopicPlan( + createTopicMeta( + blockedTopicName, "sn1", 5L, ownerLeaseDurationMs, ownerLeaseExpireTimeMs))) + .getCode()); + + final List renewedTopicMetas = + subscriptionInfo.renewTopicOwnerLeases(Collections.singleton(blockedTopicName)); + + Assert.assertEquals(1, renewedTopicMetas.size()); + Assert.assertEquals(topicName, renewedTopicMetas.get(0).getTopicName()); + Assert.assertTrue( + subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs() + > ownerLeaseExpireTimeMs); + Assert.assertEquals( + ownerLeaseExpireTimeMs, + subscriptionInfo.getTopicMeta(blockedTopicName).getOwnerLeaseExpireTimeMs().longValue()); + } + @Test public void testAlterTopicOwnerAndShowTopicOwner() { final String topicName = "topic-" + UUID.randomUUID(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java index f8cd66f5fd99e..e7e50d8b9e502 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java @@ -148,7 +148,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TReconstructRegionReq; import org.apache.iotdb.confignode.rpc.thrift.TRegionRouteMapResp; import org.apache.iotdb.confignode.rpc.thrift.TRemoveRegionReq; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementReq; import org.apache.iotdb.confignode.rpc.thrift.TSchemaNodeManagementResp; import org.apache.iotdb.confignode.rpc.thrift.TSchemaPartitionReq; @@ -1217,12 +1216,6 @@ public TSStatus alterTopic(TAlterTopicReq req) throws TException { () -> client.alterTopic(req), status -> !updateConfigNodeLeader(status)); } - @Override - public TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) throws TException { - return executeRemoteCallWithRetry( - () -> client.renewTopicOwnerLease(req), status -> !updateConfigNodeLeader(status)); - } - @Override public TSStatus dropTopic(String topicName) throws TException { return executeRemoteCallWithRetry( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java index 7890802f36243..6d9a78d236d63 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java @@ -2374,6 +2374,20 @@ public TDataNodeHeartbeatResp getDataNodeHeartBeat(TDataNodeHeartbeatReq req) th req.getCurrentRegionOperations())); } + if (req.isSetTopicMetas() && SubscriptionConfig.getInstance().getSubscriptionEnabled()) { + final List topicMetas = new ArrayList<>(); + for (final ByteBuffer topicMetaBuffer : req.getTopicMetas()) { + topicMetas.add(TopicMeta.deserialize(topicMetaBuffer)); + } + final TPushTopicMetaRespExceptionMessage exceptionMessage = + SubscriptionAgent.topic().handleTopicMetaChanges(topicMetas); + if (exceptionMessage != null) { + LOGGER.warn( + "Failed to handle subscription topic meta changes from ConfigNode heartbeat: {}.", + exceptionMessage); + } + } + return resp; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java index 1bb11be3752d6..adb7f606880cf 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -269,33 +268,4 @@ public TSStatus checkTopicOwners( } return RpcUtils.SUCCESS_STATUS; } - - public Map getTopicOwnerLeaseRenewalDurationMs( - final ConsumerConfig consumerConfig, final Iterable topicNames) { - final String requestOwnerId = consumerConfig.getOwnerId(); - final Long requestOwnerEpoch = consumerConfig.getOwnerEpoch(); - if (Objects.isNull(requestOwnerId) || Objects.isNull(requestOwnerEpoch)) { - return new HashMap<>(); - } - - final Map renewedTopicOwnerLeaseDurationMs = new HashMap<>(); - acquireReadLock(); - try { - for (final String topicName : topicNames) { - final TopicMeta topicMeta = topicMetaKeeper.getTopicMeta(topicName); - if (Objects.isNull(topicMeta) - || !topicMeta.isOwnerFencingEnabled() - || !Objects.equals(topicMeta.getOwnerId(), requestOwnerId) - || topicMeta.getOwnerEpoch() != requestOwnerEpoch - || Objects.isNull(topicMeta.getOwnerLeaseDurationMs())) { - continue; - } - - renewedTopicOwnerLeaseDurationMs.put(topicName, topicMeta.getOwnerLeaseDurationMs()); - } - } finally { - releaseReadLock(); - } - return renewedTopicOwnerLeaseDurationMs; - } } 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 609b782fff6e0..8a5f75593cdbf 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 @@ -29,7 +29,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo; -import org.apache.iotdb.confignode.rpc.thrift.TRenewTopicOwnerLeaseReq; import org.apache.iotdb.confignode.rpc.thrift.TShowDataNodesResp; import org.apache.iotdb.confignode.rpc.thrift.TSubscribeReq; import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; @@ -352,10 +351,6 @@ private TPipeSubscribeResp handleSubscriptionHeartbeatInternal(final Subscriptio SubscriptionAgent.consumer() .getTopicNamesSubscribedByConsumer( consumerConfig.getConsumerGroupId(), consumerConfig.getConsumerId()); - final TSStatus renewalStatus = renewTopicOwnerLeases(consumerConfig, subscribedTopicNames); - if (renewalStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - return PipeSubscribeHeartbeatResp.toTPipeSubscribeResp(renewalStatus); - } final TSStatus ownerStatus = SubscriptionAgent.topic().checkTopicOwners(consumerConfig, subscribedTopicNames); @@ -409,48 +404,6 @@ private TPipeSubscribeResp handleSubscriptionHeartbeatInternal(final Subscriptio RpcUtils.SUCCESS_STATUS, topics, endPoints, topicNamesToUnsubscribe); } - private TSStatus renewTopicOwnerLeases( - final ConsumerConfig consumerConfig, final Set subscribedTopicNames) - throws SubscriptionException { - final Map renewedTopicOwnerLeaseDurationMs = - SubscriptionAgent.topic() - .getTopicOwnerLeaseRenewalDurationMs(consumerConfig, subscribedTopicNames); - if (renewedTopicOwnerLeaseDurationMs.isEmpty()) { - return RpcUtils.SUCCESS_STATUS; - } - - try (final ConfigNodeClient configNodeClient = - CONFIG_NODE_CLIENT_MANAGER.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) { - for (final Map.Entry entry : renewedTopicOwnerLeaseDurationMs.entrySet()) { - final String topicName = entry.getKey(); - final long ownerLeaseDurationMs = entry.getValue(); - final TSStatus status = - configNodeClient.renewTopicOwnerLease( - new TRenewTopicOwnerLeaseReq( - topicName, - consumerConfig.getOwnerId(), - consumerConfig.getOwnerEpoch(), - ownerLeaseDurationMs)); - if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - LOGGER.warn( - "Subscription: failed to renew owner lease for topic {}, owner-id: {}," - + " owner-epoch: {}, result status is {}.", - topicName, - consumerConfig.getOwnerId(), - consumerConfig.getOwnerEpoch(), - status); - return status; - } - } - } catch (final ClientManagerException | TException e) { - throw new SubscriptionException( - String.format( - "Subscription: failed to renew owner leases for consumer %s, exception is %s.", - consumerConfig, e)); - } - return RpcUtils.SUCCESS_STATUS; - } - private TPipeSubscribeResp handlePipeSubscribeSubscribe(final PipeSubscribeSubscribeReq req) { try { return handlePipeSubscribeSubscribeInternal(req); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index 427caa1648c2b..30721dba420e3 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -290,6 +290,16 @@ public long renewOwnerLeaseWithDuration( return ownerLeaseExpireTimeMs; } + public long renewOwnerLeaseWithConfiguredDuration() { + if (Objects.isNull(ownerLeaseDurationMs)) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease duration should be configured, topic: %s", + topicName)); + } + return renewOwnerLeaseWithDuration(ownerId, ownerEpoch, ownerLeaseDurationMs); + } + public void clearOwner() { ownerId = null; ownerEpoch = -1L; diff --git a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift index 01eb21b43abbd..b0b20ee4ac701 100644 --- a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift +++ b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift @@ -1012,13 +1012,6 @@ struct TAlterTopicReq { 3: required set subscribedConsumerGroupIds } -struct TRenewTopicOwnerLeaseReq { - 1: required string topicName - 2: required string ownerId - 3: required i64 ownerEpoch - 4: required i64 ownerLeaseDurationMs -} - struct TGetAllTopicInfoResp { 1: required common.TSStatus status 2: required list allTopicInfo @@ -1952,9 +1945,6 @@ service IConfigNodeRPCService { /** Alter Topic */ common.TSStatus alterTopic(TAlterTopicReq req) - /** Renew topic owner lease */ - common.TSStatus renewTopicOwnerLease(TRenewTopicOwnerLeaseReq req) - /** Drop Topic */ common.TSStatus dropTopic(string topicName) diff --git a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift index 2f3703ec5fcfd..9b55ce75abede 100644 --- a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift +++ b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift @@ -289,6 +289,7 @@ struct TDataNodeHeartbeatReq { // Using 8 bit to represent 8 bool // lowest bit: enable separation of admin powers 16: optional byte booleanVariables1 + 17: optional list topicMetas } struct TDataNodeActivation { From 1e80cc0afd562748beec32b674b16d06c3fd3835 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 11 Jun 2026 12:23:27 +0000 Subject: [PATCH 08/14] Rework subscription topic owner epoch fencing and lease Land the technical-review corrections together: - Seek fencing in SubscriptionReceiverV1 before any position-mutating op. - Move the owner lease off the node heartbeat onto a dedicated, fine-grained SubscriptionOwnerLeaseSyncer pushing relative remaining durations. - Make the absolute lease expiry DataNode-local runtime state only: it is set solely from the lease heartbeat, never an attribute, never serialized, and not part of equals/hashCode. CN persists only owner-id, owner-epoch, max-owner-epoch and owner-lease-duration-ms. - Add globally-monotonic, never-reused owner epochs via max-owner-epoch that survives clearOwner. - Drain owner transfers via a ConfigNode admission-wait (blockSince + L + H) instead of CN-side lease-not-expired rejection/retry. - Remove the dead absolute-timestamp lease machinery (renewOwnerLease*, getOwnerLeaseRemainingTimeMs, isActiveOwnerLease/isOwnerTransfer, the owner-lease-expire-time-ms attribute key, the alterTopic retry loop) and update the unit tests accordingly. --- ...oTDBSubscriptionTopicOwnerPartitionIT.java | 263 ++++++++++++++++++ .../local/IoTDBSubscriptionTopicOwnerIT.java | 65 +---- .../subscription/config/TopicConstant.java | 5 +- .../AbstractSubscriptionSession.java | 4 +- .../client/async/CnToDnAsyncRequestType.java | 1 + ...oDnInternalServiceAsyncRequestManager.java | 6 + .../confignode/manager/ProcedureManager.java | 52 ++-- .../load/service/HeartbeatService.java | 10 - .../subscription/SubscriptionCoordinator.java | 107 ++++--- .../SubscriptionOwnerLeaseSyncer.java | 96 +++++++ .../subscription/SubscriptionInfo.java | 35 ++- .../subscription/SubscriptionInfoTest.java | 133 ++------- .../iotdb/db/i18n/DataNodeMiscMessages.java | 2 + .../iotdb/db/i18n/DataNodeMiscMessages.java | 2 + .../impl/DataNodeInternalRPCServiceImpl.java | 29 +- .../agent/SubscriptionTopicAgent.java | 26 ++ .../receiver/SubscriptionReceiverV1.java | 9 + .../subscription/meta/topic/TopicMeta.java | 242 ++++++---------- .../subscription/topic/TopicDeSerTest.java | 172 +++++------- .../src/main/thrift/datanode.thrift | 22 +- 20 files changed, 740 insertions(+), 541 deletions(-) create mode 100644 integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java create mode 100644 iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java new file mode 100644 index 0000000000000..d35d3c4795f58 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.subscription.it.cluster; + +import org.apache.iotdb.isession.ISession; +import org.apache.iotdb.it.env.EnvFactory; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.ClusterIT; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; +import org.apache.iotdb.rpc.subscription.exception.SubscriptionOwnerFencedException; +import org.apache.iotdb.session.subscription.SubscriptionTreeSession; +import org.apache.iotdb.session.subscription.consumer.tree.SubscriptionTreePullConsumer; +import org.apache.iotdb.session.subscription.model.Topic; +import org.apache.iotdb.subscription.it.AbstractSubscriptionIT; +import org.apache.iotdb.subscription.it.IoTDBSubscriptionITConstant; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.util.Properties; + +/** + * Owner-fencing correctness under SN/DN/CN partition topologies, simulated at the node-process + * level via the IT framework (shutdown/start of ConfigNodes and DataNodes; ConfigNode failover via + * leader shutdown). Runs on a 3 ConfigNode + 3 DataNode cluster. + * + *

Covers: + * + *

    + *
  • multi-DataNode: a stale owner is fenced for a topic whose data spans several DataNodes; + *
  • ConfigNode failover: the owner epoch is persisted via consensus, so the new leader keeps + * fencing the stale owner after the previous leader is shut down; + *
  • DataNode restart: after a DataNode is shut down and restarted, the owner metadata is + * re-synced and the stale owner stays fenced. + *
+ */ +@RunWith(IoTDBTestRunner.class) +@Category({ClusterIT.class}) +public class IoTDBSubscriptionTopicOwnerPartitionIT extends AbstractSubscriptionIT { + + private static final String HEAD_PATH = "root.topic_owner_partition"; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + EnvFactory.getEnv() + .getConfig() + .getCommonConfig() + .setSubscriptionEnabled(true) + .setPipeMemoryManagementEnabled(false) + .setIsPipeEnableMemoryCheck(false); + // 3 ConfigNodes (for failover) + 3 DataNodes (for multi-DataNode data placement). + EnvFactory.getEnv().initClusterEnvironment(3, 3); + } + + @Override + @After + public void tearDown() throws Exception { + EnvFactory.getEnv().cleanClusterEnvironment(); + super.tearDown(); + } + + @Test + public void testStaleOwnerFencedOnMultiDataNodeTopic() throws Exception { + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + final String topicName = "topic_owner_multi_dn"; + + createOwnedTopic(host, port, topicName, "sn2", 6L); + try { + // Spread data across multiple devices (and thus, with multiple data regions, multiple + // DataNodes) before consuming. + insertDataAcrossDevices(); + + // A stale owner (epoch 5 < current 6) must be fenced regardless of which DataNode serves the + // request. + assertSubscribeFenced(host, port, topicName, "stale_multi_dn", "sn1", 5L); + + // The current owner can subscribe. + try (final SubscriptionTreePullConsumer currentOwnerConsumer = + ownerConsumer(host, port, "current_multi_dn", "topic_owner_multi_dn_group", "sn2", 6L)) { + currentOwnerConsumer.open(); + currentOwnerConsumer.subscribe(topicName); + currentOwnerConsumer.unsubscribe(topicName); + } + } finally { + dropTopic(host, port, topicName); + } + } + + @Test + public void testConfigNodeFailoverPersistsOwnerEpochAndKeepsFencing() throws Exception { + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + final String topicName = "topic_owner_cn_failover"; + final long ownerLeaseDurationMs = 2000L; + + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Properties properties = new Properties(); + properties.put(TopicConstant.PATH_KEY, HEAD_PATH + ".**"); + properties.put(TopicConstant.START_TIME_KEY, "0"); + properties.put(TopicConstant.OWNER_ID_KEY, "sn1"); + properties.put(TopicConstant.OWNER_EPOCH_KEY, "5"); + properties.put( + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); + session.createTopic(topicName, properties); + + // Transfer to sn2/epoch 6 (this persists owner + epoch via ConfigNode consensus). + session.alterTopicOwner(topicName, "sn2", 6L); + Assert.assertTrue(getTopicAttributes(session, topicName).contains("owner-epoch=6")); + } + + // Fail over: shut down the current leader ConfigNode. + final int leaderIndex = EnvFactory.getEnv().getLeaderConfigNodeIndex(); + EnvFactory.getEnv().shutdownConfigNode(leaderIndex); + + try { + // After failover, the new leader must still report the persisted owner/epoch (consensus), and + // a stale owner (epoch 5) must remain fenced. + IoTDBSubscriptionITConstant.AWAIT.untilAsserted( + () -> { + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final String attributes = getTopicAttributes(session, topicName); + Assert.assertTrue(attributes.contains("owner-id=sn2")); + Assert.assertTrue(attributes.contains("owner-epoch=6")); + } + }); + assertSubscribeFenced(host, port, topicName, "stale_after_failover", "sn1", 5L); + } finally { + EnvFactory.getEnv().startConfigNode(leaderIndex); + dropTopic(host, port, topicName); + } + } + + @Test + public void testDataNodeRestartReSyncsOwnerFencing() throws Exception { + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + final String topicName = "topic_owner_dn_restart"; + + createOwnedTopic(host, port, topicName, "sn2", 6L); + try { + // Restart a DataNode: it loses in-memory owner metadata and must re-sync it on restart. + EnvFactory.getEnv().shutdownDataNode(1); + EnvFactory.getEnv().startDataNode(1); + + // After the DataNode comes back and re-syncs owner metadata, the stale owner stays fenced. + IoTDBSubscriptionITConstant.AWAIT.untilAsserted( + () -> assertSubscribeFenced(host, port, topicName, "stale_after_dn_restart", "sn1", 5L)); + } finally { + dropTopic(host, port, topicName); + } + } + + private static void createOwnedTopic( + final String host, + final int port, + final String topicName, + final String ownerId, + final long ownerEpoch) + throws Exception { + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Properties properties = new Properties(); + properties.put(TopicConstant.PATH_KEY, HEAD_PATH + ".**"); + properties.put(TopicConstant.START_TIME_KEY, "0"); + properties.put(TopicConstant.OWNER_ID_KEY, ownerId); + properties.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + session.createTopic(topicName, properties); + } + } + + private static void dropTopic(final String host, final int port, final String topicName) { + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + session.dropTopicIfExists(topicName); + } catch (final Exception e) { + // best-effort cleanup + } + } + + private static SubscriptionTreePullConsumer ownerConsumer( + final String host, + final int port, + final String consumerId, + final String consumerGroupId, + final String ownerId, + final long ownerEpoch) { + return new SubscriptionTreePullConsumer.Builder() + .host(host) + .port(port) + .consumerId(consumerId) + .consumerGroupId(consumerGroupId) + .ownerId(ownerId) + .ownerEpoch(ownerEpoch) + .autoCommit(false) + .buildPullConsumer(); + } + + private static void assertSubscribeFenced( + final String host, + final int port, + final String topicName, + final String consumerId, + final String ownerId, + final long ownerEpoch) + throws Exception { + try (final SubscriptionTreePullConsumer staleOwnerConsumer = + ownerConsumer(host, port, consumerId, topicName + "_stale_group", ownerId, ownerEpoch)) { + staleOwnerConsumer.open(); + Assert.assertThrows( + SubscriptionOwnerFencedException.class, () -> staleOwnerConsumer.subscribe(topicName)); + } + } + + private void insertDataAcrossDevices() throws Exception { + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + for (int device = 0; device < 6; device++) { + for (int i = 0; i < 10; i++) { + session.executeNonQueryStatement( + String.format( + "insert into %s.d%s(time, s1) values (%s, %s)", HEAD_PATH, device, i, i)); + } + } + session.executeNonQueryStatement("flush"); + } + } + + private static String getTopicAttributes( + final SubscriptionTreeSession session, final String topicName) throws Exception { + for (final Topic topic : session.getTopics()) { + if (topicName.equals(topic.getTopicName())) { + return topic.getTopicAttributes(); + } + } + Assert.fail("Topic " + topicName + " should exist."); + return ""; + } +} diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index 7b1a9da59e4e8..4bcbd1f7eca8f 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -45,16 +45,11 @@ import java.util.List; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; @RunWith(IoTDBTestRunner.class) @Category({LocalStandaloneIT.class}) public class IoTDBSubscriptionTopicOwnerIT extends AbstractSubscriptionLocalIT { - private static final Pattern OWNER_LEASE_EXPIRE_TIME_MS_PATTERN = - Pattern.compile("owner-lease-expire-time-ms=([0-9]+)"); - @Override @Before public void setUp() throws Exception { @@ -132,12 +127,10 @@ public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws } @Test - public void testConfigNodeHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExpiration() - throws Exception { + public void testAlterTopicOwnerWaitsForLeaseDrainBeforeInstallingNewOwner() throws Exception { final String host = EnvFactory.getEnv().getIP(); final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); - final String topicName = "topic_owner_lease_renewal"; - final long heartbeatIntervalMs = 1000L; + final String topicName = "topic_owner_lease_drain"; final long ownerLeaseDurationMs = 2500L; try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { @@ -151,49 +144,24 @@ public void testConfigNodeHeartbeatRenewsOwnerLeaseAndAlterOwnerWaitsForLeaseExp TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); session.createTopic(topicName, properties); - final Long initialOwnerLeaseExpireTimeMs = getOwnerLeaseExpireTimeMs(session, topicName); - Assert.assertNotNull(initialOwnerLeaseExpireTimeMs); - Assert.assertTrue(initialOwnerLeaseExpireTimeMs > System.currentTimeMillis()); - - try (final SubscriptionTreePullConsumer currentOwnerConsumer = - new SubscriptionTreePullConsumer.Builder() - .host(host) - .port(port) - .consumerId("current_sn") - .consumerGroupId("topic_owner_lease_group") - .ownerId("sn1") - .ownerEpoch(5L) - .heartbeatIntervalMs(heartbeatIntervalMs) - .autoCommit(false) - .buildPullConsumer()) { - currentOwnerConsumer.open(); - currentOwnerConsumer.subscribe(topicName); - - final AtomicReference renewedOwnerLeaseExpireTimeMs = - new AtomicReference<>(initialOwnerLeaseExpireTimeMs); - IoTDBSubscriptionITConstant.AWAIT.untilAsserted( - () -> { - final Long ownerLeaseExpireTimeMs = getOwnerLeaseExpireTimeMs(session, topicName); - Assert.assertNotNull(ownerLeaseExpireTimeMs); - Assert.assertTrue(ownerLeaseExpireTimeMs > initialOwnerLeaseExpireTimeMs); - renewedOwnerLeaseExpireTimeMs.set(ownerLeaseExpireTimeMs); - }); - + try { + // Transferring to a different owner must wait for the old owner's lease to drain on every + // DataNode before the new owner is installed. ConfigNode stops renewing and waits at least + // the lease duration (measured on its own clock), so the call blocks for >= the lease + // duration. This is the admission gate that prevents cross-DataNode double-active + // consuming. final long alterStartTimeMs = System.currentTimeMillis(); session.alterTopicOwner(topicName, "sn2", 6L); final long alterElapsedTimeMs = System.currentTimeMillis() - alterStartTimeMs; - - final long ownerLeaseRemainingTimeMs = - renewedOwnerLeaseExpireTimeMs.get() - alterStartTimeMs; - if (ownerLeaseRemainingTimeMs > 500L) { - Assert.assertTrue(alterElapsedTimeMs >= ownerLeaseRemainingTimeMs - 250L); - } + Assert.assertTrue( + "alterTopicOwner should block at least the lease drain duration, but only took " + + alterElapsedTimeMs + + "ms", + alterElapsedTimeMs >= ownerLeaseDurationMs); final String topicAttributes = getTopicAttributes(session, topicName); Assert.assertTrue(topicAttributes.contains("owner-id=sn2")); Assert.assertTrue(topicAttributes.contains("owner-epoch=6")); - Assert.assertFalse(topicAttributes.contains("owner-lease-duration-ms=")); - Assert.assertFalse(topicAttributes.contains("owner-lease-expire-time-ms=")); } finally { session.dropTopicIfExists(topicName); } @@ -233,11 +201,4 @@ private static String getTopicAttributes( Assert.fail("Topic " + topicName + " should exist."); return ""; } - - private static Long getOwnerLeaseExpireTimeMs( - final SubscriptionTreeSession session, final String topicName) throws Exception { - final Matcher matcher = - OWNER_LEASE_EXPIRE_TIME_MS_PATTERN.matcher(getTopicAttributes(session, topicName)); - return matcher.find() ? Long.parseLong(matcher.group(1)) : null; - } } diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java index 13dfc555fff9e..a0b391225c293 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java @@ -74,8 +74,11 @@ public class TopicConstant { public static final String OWNER_ID_KEY = "owner-id"; public static final String OWNER_EPOCH_KEY = "owner-epoch"; + // Highest owner epoch ever granted for this topic. Retained across clearOwner so that owner + // epoch is globally monotonic and never reused, even after a clear -> re-enable cycle. Stored as + // a topic attribute so it survives serialization automatically (self-describing). + public static final String MAX_OWNER_EPOCH_KEY = "max-owner-epoch"; public static final String OWNER_LEASE_DURATION_MS_KEY = "owner-lease-duration-ms"; - public static final String OWNER_LEASE_EXPIRE_TIME_MS_KEY = "owner-lease-expire-time-ms"; private TopicConstant() { throw new IllegalStateException(SubscriptionMessages.UTILITY_CLASS); diff --git a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java index bcdc279dcff38..4f3f4b32d3709 100644 --- a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java +++ b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/AbstractSubscriptionSession.java @@ -295,7 +295,7 @@ private static String escapeSqlStringLiteral(final String value) { private static boolean containsOwnerAttribute(final Properties properties) { return properties.containsKey(TopicConstant.OWNER_ID_KEY) || properties.containsKey(TopicConstant.OWNER_EPOCH_KEY) - || properties.containsKey(TopicConstant.OWNER_LEASE_DURATION_MS_KEY) - || properties.containsKey(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + || properties.containsKey(TopicConstant.MAX_OWNER_EPOCH_KEY) + || properties.containsKey(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); } } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnAsyncRequestType.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnAsyncRequestType.java index b3c34dd72d2f4..d4db2e5c5a64b 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnAsyncRequestType.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnAsyncRequestType.java @@ -82,6 +82,7 @@ public enum CnToDnAsyncRequestType { CONSUMER_GROUP_PUSH_SINGLE_META, PULL_COMMIT_PROGRESS, SUBSCRIPTION_PUSH_RUNTIME, + SUBSCRIPTION_PUSH_OWNER_LEASE, // TEMPLATE UPDATE_TEMPLATE, diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnInternalServiceAsyncRequestManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnInternalServiceAsyncRequestManager.java index 54ef5e39088a0..4aec391f7af2c 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnInternalServiceAsyncRequestManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/client/async/CnToDnInternalServiceAsyncRequestManager.java @@ -96,6 +96,7 @@ import org.apache.iotdb.mpp.rpc.thrift.TPushSingleTopicMetaReq; import org.apache.iotdb.mpp.rpc.thrift.TPushSubscriptionRuntimeReq; import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaReq; +import org.apache.iotdb.mpp.rpc.thrift.TPushTopicOwnerLeaseReq; import org.apache.iotdb.mpp.rpc.thrift.TRegionLeaderChangeReq; import org.apache.iotdb.mpp.rpc.thrift.TRegionRouteReq; import org.apache.iotdb.mpp.rpc.thrift.TResetPeerListReq; @@ -244,6 +245,11 @@ protected void initActionMapBuilder() { (req, client, handler) -> client.pushSubscriptionRuntime( (TPushSubscriptionRuntimeReq) req, (DataNodeTSStatusRPCHandler) handler)); + actionMapBuilder.put( + CnToDnAsyncRequestType.SUBSCRIPTION_PUSH_OWNER_LEASE, + (req, client, handler) -> + client.pushTopicOwnerLease( + (TPushTopicOwnerLeaseReq) req, (DataNodeTSStatusRPCHandler) handler)); actionMapBuilder.put( CnToDnAsyncRequestType.PIPE_HEARTBEAT, (req, client, handler) -> diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java index 4f01fc09b9c09..9a9387fbc8b32 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java @@ -1761,32 +1761,30 @@ public TSStatus alterTopic(TAlterTopicReq req) { .getSubscriptionManager() .getSubscriptionCoordinator() .blockOwnerLeaseRenewalIfOwnerTransfer(req); - while (true) { - final TopicMeta updatedTopicMeta = - configManager - .getSubscriptionManager() - .getSubscriptionCoordinator() - .buildAlteredTopicMetaAfterOwnerLeaseExpired(req); - if (updatedTopicMeta == null) { - return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) - .setMessage( - String.format( - ManagerMessages.FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED, - req.getTopicName())); - } - - AlterTopicProcedure procedure = new AlterTopicProcedure(updatedTopicMeta); - executor.submitProcedure(procedure); - TSStatus status = waitingProcedureFinished(procedure); - if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - return status; - } - if (isOwnerLeaseRenewalBlocked && isOwnerLeaseNotExpiredConflict(status)) { - continue; - } + // Owner transfers wait for the previous owner's lease to drain (lease duration + one + // heartbeat interval, measured on the ConfigNode clock) inside the call below before the + // updated meta is built; epoch fencing on DataNodes guarantees correctness in the meantime. + final TopicMeta updatedTopicMeta = + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .buildAlteredTopicMetaAfterOwnerLeaseExpired(req); + if (updatedTopicMeta == null) { return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) - .setMessage(wrapTimeoutMessageForPipeProcedure(status.getMessage())); + .setMessage( + String.format( + ManagerMessages.FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED, + req.getTopicName())); } + + AlterTopicProcedure procedure = new AlterTopicProcedure(updatedTopicMeta); + executor.submitProcedure(procedure); + TSStatus status = waitingProcedureFinished(procedure); + if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return status; + } + return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) + .setMessage(wrapTimeoutMessageForPipeProcedure(status.getMessage())); } catch (Exception e) { return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()) .setMessage(e.getMessage()); @@ -2033,12 +2031,6 @@ private static String wrapTimeoutMessageForPipeProcedure(String message) { return message; } - private static boolean isOwnerLeaseNotExpiredConflict(final TSStatus status) { - return Objects.nonNull(status) - && Objects.nonNull(status.getMessage()) - && status.getMessage().contains("owner lease has not expired"); - } - public static void sleepWithoutInterrupt(final long timeToSleep) { long currentTime = System.currentTimeMillis(); final long endTime = timeToSleep + currentTime; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java index b159c89aaa5da..a491b3960c3c3 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/service/HeartbeatService.java @@ -49,7 +49,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.ByteBuffer; import java.util.List; import java.util.Optional; import java.util.Set; @@ -175,15 +174,6 @@ protected TDataNodeHeartbeatReq genHeartbeatReq() { configManager.getProcedureManager().getRegionOperationConsensusIds()); } - final List renewedTopicMetas = - configManager - .getSubscriptionManager() - .getSubscriptionCoordinator() - .renewTopicOwnerLeasesAndSerialize(); - if (!renewedTopicMetas.isEmpty()) { - heartbeatReq.setTopicMetas(renewedTopicMetas); - } - /* Update heartbeat counter */ heartbeatCounter.getAndIncrement(); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index 160e9b9a35ac3..52c8ddd0ff8e6 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -19,8 +19,12 @@ package org.apache.iotdb.confignode.manager.subscription; +import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation; import org.apache.iotdb.common.rpc.thrift.TSStatus; import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.confignode.client.async.CnToDnAsyncRequestType; +import org.apache.iotdb.confignode.client.async.CnToDnInternalServiceAsyncRequestManager; +import org.apache.iotdb.confignode.client.async.handlers.DataNodeAsyncRequestContext; import org.apache.iotdb.confignode.consensus.request.read.subscription.ShowSubscriptionPlan; import org.apache.iotdb.confignode.consensus.request.read.subscription.ShowTopicPlan; import org.apache.iotdb.confignode.consensus.response.subscription.SubscriptionTableResp; @@ -43,6 +47,8 @@ import org.apache.iotdb.confignode.rpc.thrift.TShowTopicResp; import org.apache.iotdb.confignode.rpc.thrift.TSubscribeReq; import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; +import org.apache.iotdb.mpp.rpc.thrift.TPushTopicOwnerLeaseReq; +import org.apache.iotdb.mpp.rpc.thrift.TTopicOwnerLeaseEntry; import org.apache.iotdb.rpc.RpcUtils; import org.apache.iotdb.rpc.TSStatusCode; @@ -50,12 +56,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -74,14 +79,18 @@ public class SubscriptionCoordinator { private AtomicReference subscriptionInfoHolder; private final SubscriptionMetaSyncer subscriptionMetaSyncer; - private final Set blockedOwnerLeaseRenewalTopics = - Collections.synchronizedSet(new HashSet<>()); + private final SubscriptionOwnerLeaseSyncer subscriptionOwnerLeaseSyncer; + // topicName -> blockSinceMs (ConfigNode local clock when owner-lease renewal was stopped for an + // in-flight owner transfer). Used to skip renewal and to bound the admission wait. + private final Map blockedOwnerLeaseRenewalTopics = + Collections.synchronizedMap(new HashMap<>()); public SubscriptionCoordinator(ConfigManager configManager, SubscriptionInfo subscriptionInfo) { this.configManager = configManager; this.subscriptionInfo = subscriptionInfo; this.coordinatorLock = new PipeTaskCoordinatorLock(); this.subscriptionMetaSyncer = new SubscriptionMetaSyncer(configManager); + this.subscriptionOwnerLeaseSyncer = new SubscriptionOwnerLeaseSyncer(configManager); } public SubscriptionInfo getSubscriptionInfo() { @@ -129,10 +138,12 @@ public boolean isLocked() { public void startSubscriptionMetaSync() { subscriptionMetaSyncer.start(); + subscriptionOwnerLeaseSyncer.start(); } public void stopSubscriptionMetaSync() { subscriptionMetaSyncer.stop(); + subscriptionOwnerLeaseSyncer.stop(); } /** @@ -181,7 +192,7 @@ public boolean blockOwnerLeaseRenewalIfOwnerTransfer(TAlterTopicReq req) { return false; } - blockedOwnerLeaseRenewalTopics.add(req.getTopicName()); + blockedOwnerLeaseRenewalTopics.put(req.getTopicName(), System.currentTimeMillis()); return true; } @@ -189,25 +200,40 @@ public void unblockOwnerLeaseRenewal(String topicName) { blockedOwnerLeaseRenewalTopics.remove(topicName); } + /** + * Block until it is safe to install the new owner: every DataNode must have let the old owner's + * lease expire. Since renewal was stopped at {@code blockSince}, a DataNode's lease can expire as + * late as {@code blockSince + H + L} (H covers propagation of the last renewal sent just before + * the block; L is the lease duration). We wait that long measured purely on this ConfigNode's own + * clock from {@code blockSince} — no cross-node timestamp comparison, so clock skew cannot shrink + * the window. This replaces any absolute-expire-timestamp comparison. + */ public TopicMeta buildAlteredTopicMetaAfterOwnerLeaseExpired(TAlterTopicReq req) throws InterruptedException { - while (true) { - final TopicMeta currentTopicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); - final TopicMeta updatedTopicMeta = buildAlteredTopicMeta(req); - if (Objects.isNull(currentTopicMeta) - || Objects.isNull(updatedTopicMeta) - || Objects.equals(currentTopicMeta.getOwnerId(), updatedTopicMeta.getOwnerId())) { - return updatedTopicMeta; - } - - final long ownerLeaseRemainingTimeMs = currentTopicMeta.getOwnerLeaseRemainingTimeMs(); - if (Objects.isNull(currentTopicMeta.getOwnerLeaseExpireTimeMs()) - || ownerLeaseRemainingTimeMs <= 0) { - return updatedTopicMeta; - } - - Thread.sleep(Math.min(ownerLeaseRemainingTimeMs + 1, 1000L)); + final TopicMeta currentTopicMeta = subscriptionInfo.deepCopyTopicMeta(req.getTopicName()); + final TopicMeta updatedTopicMeta = buildAlteredTopicMeta(req); + if (Objects.isNull(currentTopicMeta) + || Objects.isNull(updatedTopicMeta) + || Objects.equals(currentTopicMeta.getOwnerId(), updatedTopicMeta.getOwnerId())) { + // Not an owner change: nothing to drain. + return updatedTopicMeta; + } + + final Long leaseDurationMs = currentTopicMeta.getOwnerLeaseDurationMs(); + final Long blockSinceMs = blockedOwnerLeaseRenewalTopics.get(req.getTopicName()); + if (Objects.isNull(leaseDurationMs) || Objects.isNull(blockSinceMs)) { + // No lease configured (no drain to wait for) or renewal not blocked: epoch fencing applies on + // reachable DataNodes; nothing further to wait on here. + return updatedTopicMeta; + } + + final long drainDeadlineMs = + blockSinceMs + leaseDurationMs + SubscriptionOwnerLeaseSyncer.getHeartbeatIntervalMs(); + long remainingMs; + while ((remainingMs = drainDeadlineMs - System.currentTimeMillis()) > 0) { + Thread.sleep(Math.min(remainingMs, 1000L)); } + return updatedTopicMeta; } public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) { @@ -215,24 +241,35 @@ public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) { req.getTopicName(), req.getTopicAttributes()); } - public List renewTopicOwnerLeasesAndSerialize() { + /** + * Build and push owner-lease renewals to all DataNodes via the dedicated subscription owner + * heartbeat (independent from the node heartbeat). Topics undergoing an owner transfer are + * skipped so their lease drains on the DataNodes. Best-effort: a DataNode that misses pushes will + * let its local lease expire and fence the owner (fail-closed). + */ + public void pushTopicOwnerLeasesToDataNodes() { final Set blockedTopicNames; synchronized (blockedOwnerLeaseRenewalTopics) { - blockedTopicNames = new HashSet<>(blockedOwnerLeaseRenewalTopics); + blockedTopicNames = new HashSet<>(blockedOwnerLeaseRenewalTopics.keySet()); + } + + final List ownerLeases = + subscriptionInfo.collectTopicOwnerLeaseEntries(blockedTopicNames); + if (ownerLeases.isEmpty()) { + return; } - final List renewedTopicMetaBinaryList = new ArrayList<>(); - for (final TopicMeta topicMeta : subscriptionInfo.renewTopicOwnerLeases(blockedTopicNames)) { - try { - renewedTopicMetaBinaryList.add(topicMeta.serialize()); - } catch (final IOException e) { - LOGGER.warn( - "Failed to serialize renewed owner lease topic meta for topic {}.", - topicMeta.getTopicName(), - e); - } + final Map dataNodeLocationMap = + configManager.getNodeManager().getRegisteredDataNodeLocations(); + if (dataNodeLocationMap.isEmpty()) { + return; } - return renewedTopicMetaBinaryList; + + final TPushTopicOwnerLeaseReq request = new TPushTopicOwnerLeaseReq(ownerLeases); + final DataNodeAsyncRequestContext clientHandler = + new DataNodeAsyncRequestContext<>( + CnToDnAsyncRequestType.SUBSCRIPTION_PUSH_OWNER_LEASE, request, dataNodeLocationMap); + CnToDnInternalServiceAsyncRequestManager.getInstance().sendAsyncRequestWithRetry(clientHandler); } public TSStatus dropTopic(TDropTopicReq req) { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java new file mode 100644 index 0000000000000..2cff0a0726878 --- /dev/null +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java @@ -0,0 +1,96 @@ +/* + * 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.confignode.manager.subscription; + +import org.apache.iotdb.commons.concurrent.IoTDBThreadPoolFactory; +import org.apache.iotdb.commons.concurrent.threadpool.ScheduledExecutorUtil; +import org.apache.iotdb.confignode.manager.ConfigManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Dedicated, fine-grained subscription owner-lease heartbeat, independent from the node heartbeat. + * + *

Each tick pushes relative-duration owner leases to all DataNodes (see {@link + * SubscriptionCoordinator#pushTopicOwnerLeasesToDataNodes()}). A DataNode that stops receiving + * pushes (e.g. partitioned from this ConfigNode) lets its local lease expire and fences the owner + * (fail-closed). The interval must stay well below the configured lease duration so transient + * misses do not spuriously fence a healthy owner. + */ +public class SubscriptionOwnerLeaseSyncer { + + private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionOwnerLeaseSyncer.class); + + // TODO: promote to SubscriptionConfig; invariant: lease-duration >> interval * tolerated-misses. + private static final long OWNER_LEASE_HEARTBEAT_INTERVAL_SECONDS = 5; + + /** The owner-lease heartbeat interval {@code H}, used by the transfer admission-wait bound. */ + public static long getHeartbeatIntervalMs() { + return OWNER_LEASE_HEARTBEAT_INTERVAL_SECONDS * 1000L; + } + + private static final ScheduledExecutorService LEASE_EXECUTOR = + IoTDBThreadPoolFactory.newSingleThreadScheduledExecutor("SubscriptionOwnerLeaseSyncer"); + + private final ConfigManager configManager; + + private Future leaseSyncFuture; + + SubscriptionOwnerLeaseSyncer(final ConfigManager configManager) { + this.configManager = configManager; + } + + public synchronized void start() { + if (leaseSyncFuture == null) { + leaseSyncFuture = + ScheduledExecutorUtil.safelyScheduleWithFixedDelay( + LEASE_EXECUTOR, + this::sync, + OWNER_LEASE_HEARTBEAT_INTERVAL_SECONDS, + OWNER_LEASE_HEARTBEAT_INTERVAL_SECONDS, + TimeUnit.SECONDS); + LOGGER.info("SubscriptionOwnerLeaseSyncer is started successfully."); + } + } + + private void sync() { + try { + configManager + .getSubscriptionManager() + .getSubscriptionCoordinator() + .pushTopicOwnerLeasesToDataNodes(); + } catch (final Exception e) { + LOGGER.warn("Failed to push subscription topic owner leases to DataNodes.", e); + } + } + + public synchronized void stop() { + if (leaseSyncFuture != null) { + leaseSyncFuture.cancel(false); + leaseSyncFuture = null; + LOGGER.info("SubscriptionOwnerLeaseSyncer is stopped successfully."); + } + } +} diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java index b4fa66827603a..57c1b17f1dcd8 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java @@ -47,6 +47,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; import org.apache.iotdb.consensus.ConsensusFactory; import org.apache.iotdb.consensus.common.DataSet; +import org.apache.iotdb.mpp.rpc.thrift.TTopicOwnerLeaseEntry; import org.apache.iotdb.rpc.TSStatusCode; import org.apache.iotdb.rpc.subscription.config.TopicConfig; import org.apache.iotdb.rpc.subscription.config.TopicConstant; @@ -564,30 +565,34 @@ public TopicMeta deepCopyTopicMetaWithUpdatedAttributes( } } - public List renewTopicOwnerLeases(final Set blockedTopicNames) { - acquireWriteLock(); + /** + * Collect owner-lease entries to push via the dedicated subscription owner heartbeat. Each entry + * carries a relative remaining duration (the configured lease duration); the DataNode derives the + * local expire time on its own clock. Topics undergoing an owner transfer ({@code + * blockedTopicNames}) are skipped so their lease drains. This is read-only on ConfigNode: no + * absolute expire timestamp is ever stored or compared across nodes. + */ + public List collectTopicOwnerLeaseEntries( + final Set blockedTopicNames) { + acquireReadLock(); try { - final List renewedTopicMetas = new ArrayList<>(); + final List entries = new ArrayList<>(); for (final TopicMeta topicMeta : topicMetaKeeper.getAllTopicMeta()) { if (!topicMeta.isOwnerFencingEnabled() || Objects.isNull(topicMeta.getOwnerLeaseDurationMs()) || blockedTopicNames.contains(topicMeta.getTopicName())) { continue; } - - try { - topicMeta.renewOwnerLeaseWithConfiguredDuration(); - renewedTopicMetas.add(topicMeta.deepCopy()); - } catch (final IllegalArgumentException e) { - LOGGER.warn( - "Failed to renew subscription topic {} owner lease during ConfigNode heartbeat.", - topicMeta.getTopicName(), - e); - } + entries.add( + new TTopicOwnerLeaseEntry( + topicMeta.getTopicName(), + topicMeta.getOwnerId(), + topicMeta.getOwnerEpoch(), + topicMeta.getOwnerLeaseDurationMs())); } - return renewedTopicMetas; + return entries; } finally { - releaseWriteLock(); + releaseReadLock(); } } diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index 036357a1385be..9e8e5ec7cbd0a 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -26,6 +26,7 @@ import org.apache.iotdb.confignode.consensus.response.subscription.TopicTableResp; import org.apache.iotdb.confignode.rpc.thrift.TShowTopicInfo; import org.apache.iotdb.confignode.rpc.thrift.TShowTopicResp; +import org.apache.iotdb.mpp.rpc.thrift.TTopicOwnerLeaseEntry; import org.apache.iotdb.rpc.TSStatusCode; import org.apache.iotdb.rpc.subscription.config.TopicConstant; @@ -92,119 +93,36 @@ public void testAlterTopicTransfersOwnerWithUpdatedAttributes() { } @Test - public void testAlterTopicRejectsTransferBeforeOwnerLeaseExpires() { - final String topicName = "topic-" + UUID.randomUUID(); - final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - - final TopicMeta initialTopicMeta = - createTopicMeta(topicName, "sn1", 5L, System.currentTimeMillis() + 60000); - Assert.assertEquals( - TSStatusCode.SUCCESS_STATUS.getStatusCode(), - subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); - - final Map updatedAttributes = new HashMap<>(); - updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); - updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); - final TSStatus alterStatus = - subscriptionInfo.alterTopic( - new AlterTopicPlan(initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))); - - Assert.assertEquals( - TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode(), alterStatus.getCode()); - Assert.assertTrue(alterStatus.getMessage().contains("owner lease has not expired")); - Assert.assertEquals("sn1", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); - Assert.assertEquals(5L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); - } - - @Test - public void testAlterTopicTransfersOwnerAfterOwnerLeaseExpires() { - final String topicName = "topic-" + UUID.randomUUID(); - final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - - final TopicMeta initialTopicMeta = - createTopicMeta(topicName, "sn1", 5L, System.currentTimeMillis() - 1); - Assert.assertEquals( - TSStatusCode.SUCCESS_STATUS.getStatusCode(), - subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); - - final Map updatedAttributes = new HashMap<>(); - updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); - updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); - final TSStatus alterStatus = - subscriptionInfo.alterTopic( - new AlterTopicPlan(initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))); - - Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); - Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); - Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); - Assert.assertNull(subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs()); - } - - @Test - public void testAlterTopicRenewsOwnerLease() { - final String topicName = "topic-" + UUID.randomUUID(); - final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 1000; - final long ownerLeaseDurationMs = 60000; - final TopicMeta initialTopicMeta = - createTopicMeta(topicName, "sn1", 5L, ownerLeaseDurationMs, ownerLeaseExpireTimeMs); - Assert.assertEquals( - TSStatusCode.SUCCESS_STATUS.getStatusCode(), - subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); - - final TopicMeta renewedTopicMeta = initialTopicMeta.deepCopy(); - final long renewedOwnerLeaseExpireTimeMs = - renewedTopicMeta.renewOwnerLeaseWithDuration("sn1", 5L, ownerLeaseDurationMs); - final TSStatus alterStatus = subscriptionInfo.alterTopic(new AlterTopicPlan(renewedTopicMeta)); - - Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); - Assert.assertEquals("sn1", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); - Assert.assertEquals(5L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); - Assert.assertEquals( - ownerLeaseDurationMs, - subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseDurationMs().longValue()); - Assert.assertEquals( - renewedOwnerLeaseExpireTimeMs, - subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs().longValue()); - } - - @Test - public void testRenewTopicOwnerLeasesSkipsBlockedTopic() { + public void testCollectTopicOwnerLeaseEntriesSkipsBlockedTopic() { final String topicName = "topic-" + UUID.randomUUID(); final String blockedTopicName = "topic-" + UUID.randomUUID(); final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 1000; final long ownerLeaseDurationMs = 60000; Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo .createTopic( - new CreateTopicPlan( - createTopicMeta( - topicName, "sn1", 5L, ownerLeaseDurationMs, ownerLeaseExpireTimeMs))) + new CreateTopicPlan(createTopicMeta(topicName, "sn1", 5L, ownerLeaseDurationMs))) .getCode()); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo .createTopic( new CreateTopicPlan( - createTopicMeta( - blockedTopicName, "sn1", 5L, ownerLeaseDurationMs, ownerLeaseExpireTimeMs))) + createTopicMeta(blockedTopicName, "sn1", 5L, ownerLeaseDurationMs))) .getCode()); - final List renewedTopicMetas = - subscriptionInfo.renewTopicOwnerLeases(Collections.singleton(blockedTopicName)); + final List entries = + subscriptionInfo.collectTopicOwnerLeaseEntries(Collections.singleton(blockedTopicName)); - Assert.assertEquals(1, renewedTopicMetas.size()); - Assert.assertEquals(topicName, renewedTopicMetas.get(0).getTopicName()); - Assert.assertTrue( - subscriptionInfo.getTopicMeta(topicName).getOwnerLeaseExpireTimeMs() - > ownerLeaseExpireTimeMs); - Assert.assertEquals( - ownerLeaseExpireTimeMs, - subscriptionInfo.getTopicMeta(blockedTopicName).getOwnerLeaseExpireTimeMs().longValue()); + Assert.assertEquals(1, entries.size()); + final TTopicOwnerLeaseEntry entry = entries.get(0); + Assert.assertEquals(topicName, entry.getTopicName()); + Assert.assertEquals("sn1", entry.getOwnerId()); + Assert.assertEquals(5L, entry.getOwnerEpoch()); + // Relative remaining duration equals the configured lease duration; read-only, no mutation. + Assert.assertEquals(ownerLeaseDurationMs, entry.getLeaseRemainingMs()); } @Test @@ -248,11 +166,9 @@ public void testAlterTopicOwnerAndShowTopicOwner() { showTopicInfo .getTopicAttributes() .contains("owner-lease-duration-ms=" + ownerLeaseDurationMs)); - Assert.assertTrue( - showTopicInfo - .getTopicAttributes() - .contains( - "owner-lease-expire-time-ms=" + updatedTopicMeta.getOwnerLeaseExpireTimeMs())); + // The absolute lease expiry is DataNode-local runtime state; it is never a topic attribute and + // must not appear in SHOW TOPICS output. + Assert.assertFalse(showTopicInfo.getTopicAttributes().contains("owner-lease-expire-time-ms")); } private TopicMeta createTopicMeta( @@ -267,25 +183,12 @@ private TopicMeta createTopicMeta( final String topicName, final String ownerId, final long ownerEpoch, - final long ownerLeaseExpireTimeMs) { - return createTopicMeta(topicName, ownerId, ownerEpoch, null, ownerLeaseExpireTimeMs); - } - - private TopicMeta createTopicMeta( - final String topicName, - final String ownerId, - final long ownerEpoch, - final Long ownerLeaseDurationMs, - final long ownerLeaseExpireTimeMs) { + final long ownerLeaseDurationMs) { final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.OWNER_ID_KEY, ownerId); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); - if (ownerLeaseDurationMs != null) { - topicAttributes.put( - TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); - } topicAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); return new TopicMeta(topicName, 1, topicAttributes); } } diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java index e4a780d13f890..8eb26f05e0a69 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java @@ -184,6 +184,8 @@ private DataNodeMiscMessages() {} "Error occurred when pushing single topic meta"; public static final String ERROR_PUSHING_MULTI_TOPIC_META = "Error occurred when pushing multi topic meta"; + public static final String ERROR_PUSHING_TOPIC_OWNER_LEASE = + "Error occurred when pushing topic owner lease"; public static final String ERROR_PUSHING_CONSUMER_GROUP_META = "Error occurred when pushing consumer group meta"; public static final String ERROR_PUSHING_SINGLE_CONSUMER_GROUP_META = diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java index 14e9b3f2c0000..69e4370b6645b 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java @@ -183,6 +183,8 @@ private DataNodeMiscMessages() {} "推送单个 Topic 元数据时发生错误"; public static final String ERROR_PUSHING_MULTI_TOPIC_META = "推送多个 Topic 元数据时发生错误"; + public static final String ERROR_PUSHING_TOPIC_OWNER_LEASE = + "推送 Topic owner 租约时发生错误"; public static final String ERROR_PUSHING_CONSUMER_GROUP_META = "推送消费者组元数据时发生错误"; public static final String ERROR_PUSHING_SINGLE_CONSUMER_GROUP_META = diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java index 6d9a78d236d63..fc8a0b37bcaf6 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java @@ -308,6 +308,7 @@ import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaReq; import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaResp; import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaRespExceptionMessage; +import org.apache.iotdb.mpp.rpc.thrift.TPushTopicOwnerLeaseReq; import org.apache.iotdb.mpp.rpc.thrift.TRegionLeaderChangeReq; import org.apache.iotdb.mpp.rpc.thrift.TRegionLeaderChangeResp; import org.apache.iotdb.mpp.rpc.thrift.TRegionMigrateResult; @@ -1546,6 +1547,20 @@ public TPushTopicMetaResp pushMultiTopicMeta(TPushMultiTopicMetaReq req) { } } + @Override + public TSStatus pushTopicOwnerLease(TPushTopicOwnerLeaseReq req) { + if (!SubscriptionConfig.getInstance().getSubscriptionEnabled()) { + return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); + } + try { + SubscriptionAgent.topic().handleTopicOwnerLeases(req.getOwnerLeases()); + return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); + } catch (Exception e) { + LOGGER.warn(DataNodeMiscMessages.ERROR_PUSHING_TOPIC_OWNER_LEASE, e); + return new TSStatus(TSStatusCode.TOPIC_PUSH_META_ERROR.getStatusCode()); + } + } + @Override public TPushConsumerGroupMetaResp pushConsumerGroupMeta(TPushConsumerGroupMetaReq req) { if (!SubscriptionConfig.getInstance().getSubscriptionEnabled()) { @@ -2374,20 +2389,6 @@ public TDataNodeHeartbeatResp getDataNodeHeartBeat(TDataNodeHeartbeatReq req) th req.getCurrentRegionOperations())); } - if (req.isSetTopicMetas() && SubscriptionConfig.getInstance().getSubscriptionEnabled()) { - final List topicMetas = new ArrayList<>(); - for (final ByteBuffer topicMetaBuffer : req.getTopicMetas()) { - topicMetas.add(TopicMeta.deserialize(topicMetaBuffer)); - } - final TPushTopicMetaRespExceptionMessage exceptionMessage = - SubscriptionAgent.topic().handleTopicMetaChanges(topicMetas); - if (exceptionMessage != null) { - LOGGER.warn( - "Failed to handle subscription topic meta changes from ConfigNode heartbeat: {}.", - exceptionMessage); - } - } - return resp; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java index adb7f606880cf..485e553e8ee0a 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java @@ -24,6 +24,7 @@ import org.apache.iotdb.commons.subscription.meta.topic.TopicMetaKeeper; import org.apache.iotdb.db.i18n.DataNodeMiscMessages; import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaRespExceptionMessage; +import org.apache.iotdb.mpp.rpc.thrift.TTopicOwnerLeaseEntry; import org.apache.iotdb.rpc.RpcUtils; import org.apache.iotdb.rpc.TSStatusCode; import org.apache.iotdb.rpc.subscription.config.ConsumerConfig; @@ -268,4 +269,29 @@ public TSStatus checkTopicOwners( } return RpcUtils.SUCCESS_STATUS; } + + /** + * Apply owner lease renewals pushed by ConfigNode via the dedicated subscription owner heartbeat. + * The pushed remaining duration is converted to a DataNode-local expire time on the local clock, + * so no absolute timestamp is compared across nodes. Owner identity/epoch changes are delivered + * via the topic-meta push path; here we only refresh the lease for the matching current owner. + */ + public void handleTopicOwnerLeases(final List ownerLeases) { + if (Objects.isNull(ownerLeases) || ownerLeases.isEmpty()) { + return; + } + acquireWriteLock(); + try { + for (final TTopicOwnerLeaseEntry lease : ownerLeases) { + final TopicMeta topicMeta = topicMetaKeeper.getTopicMeta(lease.getTopicName()); + if (Objects.isNull(topicMeta)) { + continue; + } + topicMeta.applyOwnerLeaseFromHeartbeat( + lease.getOwnerId(), lease.getOwnerEpoch(), lease.getLeaseRemainingMs()); + } + } finally { + releaseWriteLock(); + } + } } 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 8a5f75593cdbf..134d6f7b90db5 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 @@ -939,6 +939,15 @@ private TPipeSubscribeResp handleSubscriptionSeekInternal(final SubscriptionSeek final String topicName = req.getTopicName(); final short seekType = req.getSeekType(); + // Owner fencing: seek mutates the consumption progress, so a stale owner must not be allowed to + // move the position after an ownership transfer (otherwise it could corrupt the new owner's + // recovery point). This must be guarded just like commit. + final TSStatus ownerStatus = + SubscriptionAgent.topic().checkTopicOwner(consumerConfig, topicName); + if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + return PipeSubscribeSeekResp.toTPipeSubscribeResp(ownerStatus); + } + if (seekType == SubscriptionSeekReq.SEEK_TO_TOPIC_PROGRESS) { SubscriptionAgent.broker() .seekToTopicProgress(consumerConfig, topicName, req.getTopicProgress()); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index 30721dba420e3..4a828f09bbc80 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -48,6 +48,10 @@ public class TopicMeta { private String ownerId; private long ownerEpoch; + // Highest owner epoch ever granted for this topic; retained across clearOwner. Mirrors the + // MAX_OWNER_EPOCH_KEY attribute (which carries it through serialization). Used to guarantee owner + // epoch is globally monotonic and never reused. + private long maxOwnerEpoch; private long ownerLastTransferTimeMs; private Long ownerLeaseDurationMs; private Long ownerLeaseExpireTimeMs; @@ -58,6 +62,7 @@ public class TopicMeta { private TopicMeta() { this.config = new TopicConfig(new HashMap<>()); this.ownerEpoch = -1L; + this.maxOwnerEpoch = -1L; this.ownerLastTransferTimeMs = -1L; this.subscribedConsumerGroupIds = new HashSet<>(); @@ -69,6 +74,7 @@ public TopicMeta( this.creationTime = creationTime; this.config = new TopicConfig(topicAttributes); this.ownerEpoch = -1L; + this.maxOwnerEpoch = -1L; this.ownerLastTransferTimeMs = -1L; initOwnerFromTopicAttributes(topicAttributes); @@ -82,6 +88,7 @@ public TopicMeta deepCopy() { copied.config = new TopicConfig(new HashMap<>(config.getAttribute())); copied.ownerId = ownerId; copied.ownerEpoch = ownerEpoch; + copied.maxOwnerEpoch = maxOwnerEpoch; copied.ownerLastTransferTimeMs = ownerLastTransferTimeMs; copied.ownerLeaseDurationMs = ownerLeaseDurationMs; copied.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; @@ -94,11 +101,6 @@ public TopicMeta deepCopyWithUpdatedAttributes(final Map updated final Map copiedAttributes = new HashMap<>(config.getAttribute()); if (Objects.nonNull(updatedAttributes)) { copiedAttributes.putAll(updatedAttributes); - if ((updatedAttributes.containsKey(TopicConstant.OWNER_ID_KEY) - || updatedAttributes.containsKey(TopicConstant.OWNER_EPOCH_KEY)) - && !updatedAttributes.containsKey(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)) { - copiedAttributes.remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); - } if ((updatedAttributes.containsKey(TopicConstant.OWNER_ID_KEY) || updatedAttributes.containsKey(TopicConstant.OWNER_EPOCH_KEY)) && !updatedAttributes.containsKey(TopicConstant.OWNER_LEASE_DURATION_MS_KEY)) { @@ -135,6 +137,10 @@ public long getOwnerEpoch() { return ownerEpoch; } + public long getMaxOwnerEpoch() { + return maxOwnerEpoch; + } + public long getOwnerLastTransferTimeMs() { return ownerLastTransferTimeMs; } @@ -147,26 +153,12 @@ public Long getOwnerLeaseDurationMs() { return ownerLeaseDurationMs; } - public long getOwnerLeaseRemainingTimeMs() { - return Objects.nonNull(ownerLeaseExpireTimeMs) - ? ownerLeaseExpireTimeMs - System.currentTimeMillis() - : -1L; - } - public void transferOwner(final String ownerId, final long ownerEpoch) { transferOwner(ownerId, ownerEpoch, null); } public void transferOwner( - final String ownerId, final long ownerEpoch, final Long ownerLeaseExpireTimeMs) { - transferOwner(ownerId, ownerEpoch, null, ownerLeaseExpireTimeMs); - } - - public void transferOwner( - final String ownerId, - final long ownerEpoch, - final Long ownerLeaseDurationMs, - final Long ownerLeaseExpireTimeMs) { + final String ownerId, final long ownerEpoch, final Long ownerLeaseDurationMs) { if (Objects.isNull(ownerId) || ownerId.isEmpty()) { throw new IllegalArgumentException("Subscription topic owner id should not be empty"); } @@ -180,13 +172,6 @@ public void transferOwner( + " incoming epoch is %s", this.ownerEpoch, ownerEpoch)); } - if (isActiveOwnerLease() && isOwnerTransfer(ownerId)) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease has not expired, topic: %s, current owner-id: %s," - + " current owner-epoch: %s, current owner-lease-expire-time-ms: %s", - topicName, this.ownerId, this.ownerEpoch, this.ownerLeaseExpireTimeMs)); - } if (Objects.nonNull(ownerLeaseDurationMs) && ownerLeaseDurationMs <= 0) { throw new IllegalArgumentException( String.format( @@ -198,11 +183,17 @@ public void transferOwner( this.ownerId = ownerId; this.ownerEpoch = ownerEpoch; this.ownerLeaseDurationMs = ownerLeaseDurationMs; - this.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; + // The lease expire time is DataNode-local runtime state derived from the owner-lease heartbeat; + // a freshly (re)assigned owner has no lease until the first heartbeat establishes one. + this.ownerLeaseExpireTimeMs = null; this.ownerLastTransferTimeMs = System.currentTimeMillis(); config.getAttribute().put(TopicConstant.OWNER_ID_KEY, ownerId); config.getAttribute().put(TopicConstant.OWNER_EPOCH_KEY, String.valueOf(ownerEpoch)); + if (ownerEpoch > maxOwnerEpoch) { + maxOwnerEpoch = ownerEpoch; + } + config.getAttribute().put(TopicConstant.MAX_OWNER_EPOCH_KEY, String.valueOf(maxOwnerEpoch)); if (Objects.nonNull(ownerLeaseDurationMs)) { config .getAttribute() @@ -210,94 +201,6 @@ public void transferOwner( } else { config.getAttribute().remove(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); } - if (Objects.nonNull(ownerLeaseExpireTimeMs)) { - config - .getAttribute() - .put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); - } else { - config.getAttribute().remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); - } - } - - public void renewOwnerLease( - final String ownerId, final long ownerEpoch, final long ownerLeaseExpireTimeMs) { - if (!isOwnerFencingEnabled()) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease should not be renewed when owner fencing is disabled," - + " topic: %s", - topicName)); - } - if (!Objects.equals(this.ownerId, ownerId) || this.ownerEpoch != ownerEpoch) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease should be renewed by current owner, topic: %s," - + " current owner-id: %s, current owner-epoch: %s, request owner-id: %s," - + " request owner-epoch: %s", - topicName, this.ownerId, this.ownerEpoch, ownerId, ownerEpoch)); - } - if (Objects.nonNull(this.ownerLeaseExpireTimeMs) - && System.currentTimeMillis() > this.ownerLeaseExpireTimeMs) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease has expired, topic: %s, owner-id: %s," - + " owner-epoch: %s, owner-lease-expire-time-ms: %s", - topicName, this.ownerId, this.ownerEpoch, this.ownerLeaseExpireTimeMs)); - } - if (ownerLeaseExpireTimeMs <= System.currentTimeMillis()) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease expire time should be in the future, topic: %s," - + " owner-lease-expire-time-ms: %s", - topicName, ownerLeaseExpireTimeMs)); - } - if (Objects.nonNull(this.ownerLeaseExpireTimeMs) - && ownerLeaseExpireTimeMs < this.ownerLeaseExpireTimeMs) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease expire time should not decrease, topic: %s," - + " current owner-lease-expire-time-ms: %s, request owner-lease-expire-time-ms:" - + " %s", - topicName, this.ownerLeaseExpireTimeMs, ownerLeaseExpireTimeMs)); - } - - this.ownerLeaseExpireTimeMs = ownerLeaseExpireTimeMs; - config - .getAttribute() - .put(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); - } - - public long renewOwnerLeaseWithDuration( - final String ownerId, final long ownerEpoch, final long ownerLeaseDurationMs) { - if (ownerLeaseDurationMs <= 0) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease duration should be positive, topic: %s," - + " owner-lease-duration-ms: %s", - topicName, ownerLeaseDurationMs)); - } - final long requestedOwnerLeaseExpireTimeMs = System.currentTimeMillis() + ownerLeaseDurationMs; - final long ownerLeaseExpireTimeMs = - Objects.nonNull(this.ownerLeaseExpireTimeMs) - ? Math.max(requestedOwnerLeaseExpireTimeMs, this.ownerLeaseExpireTimeMs) - : requestedOwnerLeaseExpireTimeMs; - renewOwnerLease(ownerId, ownerEpoch, ownerLeaseExpireTimeMs); - this.ownerLeaseDurationMs = ownerLeaseDurationMs; - config - .getAttribute() - .put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); - return ownerLeaseExpireTimeMs; - } - - public long renewOwnerLeaseWithConfiguredDuration() { - if (Objects.isNull(ownerLeaseDurationMs)) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease duration should be configured, topic: %s", - topicName)); - } - return renewOwnerLeaseWithDuration(ownerId, ownerEpoch, ownerLeaseDurationMs); } public void clearOwner() { @@ -309,7 +212,8 @@ public void clearOwner() { config.getAttribute().remove(TopicConstant.OWNER_ID_KEY); config.getAttribute().remove(TopicConstant.OWNER_EPOCH_KEY); config.getAttribute().remove(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); - config.getAttribute().remove(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); + // Intentionally retain maxOwnerEpoch and MAX_OWNER_EPOCH_KEY so that a later re-enable cannot + // reuse an epoch <= any epoch ever granted (global monotonicity survives clear). } public boolean matchesOwner(final String requestOwnerId, final Long requestOwnerEpoch) { @@ -320,6 +224,26 @@ public boolean matchesOwner(final String requestOwnerId, final Long requestOwner || System.currentTimeMillis() <= ownerLeaseExpireTimeMs)); } + /** + * DataNode side: apply a lease renewal pushed by ConfigNode via the dedicated subscription owner + * heartbeat. The pushed value is a relative remaining duration; it is converted to a local + * absolute expire time using this node's own clock, so no absolute timestamp is ever compared + * across ConfigNode and DataNode clocks. This expire time is purely DataNode-local runtime state + * (not a topic attribute, not serialized, not part of equality) so it never propagates back. Only + * refreshes when the pushed {@code (ownerId, ownerEpoch)} matches the current owner; stale or + * mismatched pushes are ignored (owner identity/epoch changes arrive via the topic-meta push + * path, not the lease heartbeat). + */ + public void applyOwnerLeaseFromHeartbeat( + final String ownerId, final long ownerEpoch, final long leaseRemainingMs) { + if (!isOwnerFencingEnabled() + || !Objects.equals(this.ownerId, ownerId) + || this.ownerEpoch != ownerEpoch) { + return; + } + this.ownerLeaseExpireTimeMs = System.currentTimeMillis() + leaseRemainingMs; + } + /** * @return true if the consumer group did not already subscribe this topic */ @@ -421,9 +345,28 @@ public static TopicMeta deserialize(final ByteBuffer byteBuffer) { public static void validateOwnerProgression( final TopicMeta currentTopicMeta, final TopicMeta updatedTopicMeta) { - if (Objects.isNull(currentTopicMeta) - || Objects.isNull(updatedTopicMeta) - || !currentTopicMeta.isOwnerFencingEnabled()) { + if (Objects.isNull(currentTopicMeta) || Objects.isNull(updatedTopicMeta)) { + return; + } + + // Global epoch monotonicity (never reused): a transfer to a different owner must use an epoch + // strictly greater than any epoch ever granted for this topic. The baseline (maxOwnerEpoch) is + // retained across clearOwner, so this also blocks epoch reuse after a clear -> re-enable cycle. + // Same-owner updates (e.g. attribute / lease changes) are unaffected. + if (updatedTopicMeta.isOwnerFencingEnabled() + && !Objects.equals(updatedTopicMeta.getOwnerId(), currentTopicMeta.getOwnerId()) + && updatedTopicMeta.getOwnerEpoch() <= currentTopicMeta.getMaxOwnerEpoch()) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner epoch should never be reused, topic: %s, max granted" + + " owner-epoch: %s, incoming owner-id: %s, incoming owner-epoch: %s", + currentTopicMeta.getTopicName(), + currentTopicMeta.getMaxOwnerEpoch(), + updatedTopicMeta.getOwnerId(), + updatedTopicMeta.getOwnerEpoch())); + } + + if (!currentTopicMeta.isOwnerFencingEnabled()) { return; } @@ -454,31 +397,17 @@ public static void validateOwnerProgression( updatedTopicMeta.getOwnerId(), updatedTopicMeta.getOwnerEpoch())); } - - if (currentTopicMeta.isActiveOwnerLease() - && currentTopicMeta.isOwnerTransfer(updatedTopicMeta.getOwnerId())) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease has not expired, topic: %s, current owner-id: %s," - + " current owner-epoch: %s, current owner-lease-expire-time-ms: %s", - currentTopicMeta.getTopicName(), - currentTopicMeta.getOwnerId(), - currentTopicMeta.getOwnerEpoch(), - currentTopicMeta.getOwnerLeaseExpireTimeMs())); - } - } - - private boolean isActiveOwnerLease() { - return Objects.nonNull(ownerLeaseExpireTimeMs) - && System.currentTimeMillis() <= ownerLeaseExpireTimeMs; - } - - private boolean isOwnerTransfer(final String incomingOwnerId) { - return !Objects.equals(ownerId, incomingOwnerId); } private void initOwnerFromTopicAttributes(final Map topicAttributes) { final TopicConfig topicConfig = new TopicConfig(topicAttributes); + + // Restore the global epoch baseline first; it persists even when the owner has been cleared. + final Long configuredMaxOwnerEpoch = topicConfig.getLong(TopicConstant.MAX_OWNER_EPOCH_KEY); + if (Objects.nonNull(configuredMaxOwnerEpoch)) { + this.maxOwnerEpoch = configuredMaxOwnerEpoch; + } + final String configuredOwnerId = topicConfig.getString(TopicConstant.OWNER_ID_KEY); if (Objects.isNull(configuredOwnerId)) { return; @@ -493,20 +422,13 @@ private void initOwnerFromTopicAttributes(final Map topicAttribu } final Long ownerLeaseDurationMs = topicConfig.getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); - Long ownerLeaseExpireTimeMs = topicConfig.getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY); - if (Objects.nonNull(ownerLeaseDurationMs)) { - if (ownerLeaseDurationMs <= 0) { - throw new IllegalArgumentException( - String.format( - "Subscription topic owner lease duration should be positive when %s is set", - TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); - } - if (Objects.isNull(ownerLeaseExpireTimeMs)) { - ownerLeaseExpireTimeMs = System.currentTimeMillis() + ownerLeaseDurationMs; - } + if (Objects.nonNull(ownerLeaseDurationMs) && ownerLeaseDurationMs <= 0) { + throw new IllegalArgumentException( + String.format( + "Subscription topic owner lease duration should be positive when %s is set", + TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); } - transferOwner( - configuredOwnerId, configuredOwnerEpoch, ownerLeaseDurationMs, ownerLeaseExpireTimeMs); + transferOwner(configuredOwnerId, configuredOwnerEpoch, ownerLeaseDurationMs); } /////////////////////////////// utilities /////////////////////////////// @@ -589,20 +511,14 @@ public boolean equals(final Object obj) { && Objects.equals(config, that.config) && Objects.equals(ownerId, that.ownerId) && ownerEpoch == that.ownerEpoch - && Objects.equals(ownerLeaseDurationMs, that.ownerLeaseDurationMs) - && Objects.equals(ownerLeaseExpireTimeMs, that.ownerLeaseExpireTimeMs); + && maxOwnerEpoch == that.maxOwnerEpoch + && Objects.equals(ownerLeaseDurationMs, that.ownerLeaseDurationMs); } @Override public int hashCode() { return Objects.hash( - topicName, - creationTime, - config, - ownerId, - ownerEpoch, - ownerLeaseDurationMs, - ownerLeaseExpireTimeMs); + topicName, creationTime, config, ownerId, ownerEpoch, maxOwnerEpoch, ownerLeaseDurationMs); } @Override diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java index f50d9f9ca83a0..17eb8adae6d2d 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java @@ -91,8 +91,10 @@ public void testTopicOwnerDeSer() throws IOException { Assert.assertEquals( 6L, topicMeta.getConfig().getLong(TopicConstant.OWNER_EPOCH_KEY).longValue()); Assert.assertEquals( - 100L, - topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); + 100L, topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY).longValue()); + Assert.assertEquals(100L, topicMeta.getOwnerLeaseDurationMs().longValue()); + // A freshly transferred owner has no lease expiry until the first owner-lease heartbeat. + Assert.assertNull(topicMeta.getOwnerLeaseExpireTimeMs()); topicMeta.clearOwner(); Assert.assertFalse(topicMeta.isOwnerFencingEnabled()); @@ -100,8 +102,6 @@ public void testTopicOwnerDeSer() throws IOException { Assert.assertFalse(topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_EPOCH_KEY)); Assert.assertFalse( topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); - Assert.assertFalse( - topicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); } @Test @@ -132,9 +132,6 @@ public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLeas final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); - topicAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, - String.valueOf(System.currentTimeMillis() + 60000)); topicAttributes.put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, "60000"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); @@ -149,61 +146,45 @@ public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLeas Assert.assertNull(updatedTopicMeta.getOwnerLeaseExpireTimeMs()); Assert.assertFalse( updatedTopicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); - Assert.assertFalse( - updatedTopicMeta.getConfig().hasAttribute(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY)); } @Test - public void testRenewTopicOwnerLease() { - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 1000; - final long ownerLeaseDurationMs = 60000; + public void testOwnerLeaseExpireIsDataNodeLocalRuntimeOnly() throws IOException { final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); - topicAttributes.put( - TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); - topicAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); + topicAttributes.put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, "60000"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); - final long renewedOwnerLeaseExpireTimeMs = - topicMeta.renewOwnerLeaseWithDuration("sn1", 5L, ownerLeaseDurationMs); + // Construction (ConfigNode side) never derives an absolute expiry; that is established only by + // the DataNode-local owner-lease heartbeat. + Assert.assertNull(topicMeta.getOwnerLeaseExpireTimeMs()); + Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); - Assert.assertTrue(renewedOwnerLeaseExpireTimeMs > ownerLeaseExpireTimeMs); - Assert.assertEquals(ownerLeaseDurationMs, topicMeta.getOwnerLeaseDurationMs().longValue()); - Assert.assertEquals( - renewedOwnerLeaseExpireTimeMs, topicMeta.getOwnerLeaseExpireTimeMs().longValue()); - Assert.assertEquals( - renewedOwnerLeaseExpireTimeMs, - topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); - Assert.assertEquals( - ownerLeaseDurationMs, - topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY).longValue()); - } + // Apply a heartbeat with a relative remaining duration; expiry is now set locally and the owner + // still matches before the lease drains. + topicMeta.applyOwnerLeaseFromHeartbeat("sn1", 5L, 60000L); + Assert.assertNotNull(topicMeta.getOwnerLeaseExpireTimeMs()); + Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); - @Test - public void testRenewTopicOwnerLeaseWithDurationDoesNotDecreaseExpireTime() { - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; - final long ownerLeaseDurationMs = 1000; - final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); - topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); - topicAttributes.put( - TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); - topicAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); - final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + // The locally-derived expiry is runtime-only: it is not a topic attribute, does not affect + // equality, and does not survive serialization. + final TopicMeta roundTrip = TopicMeta.deserialize(topicMeta.serialize()); + Assert.assertNull(roundTrip.getOwnerLeaseExpireTimeMs()); + Assert.assertEquals(topicMeta, roundTrip); - final long renewedOwnerLeaseExpireTimeMs = - topicMeta.renewOwnerLeaseWithDuration("sn1", 5L, ownerLeaseDurationMs); + // A stale/mismatched heartbeat is ignored. + topicMeta.applyOwnerLeaseFromHeartbeat("sn2", 5L, 60000L); + topicMeta.applyOwnerLeaseFromHeartbeat("sn1", 4L, 60000L); + Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); - Assert.assertEquals(ownerLeaseExpireTimeMs, renewedOwnerLeaseExpireTimeMs); - Assert.assertEquals(ownerLeaseExpireTimeMs, topicMeta.getOwnerLeaseExpireTimeMs().longValue()); + // An elapsed lease causes the current owner to stop matching (DataNode fencing). + topicMeta.applyOwnerLeaseFromHeartbeat("sn1", 5L, -1L); + Assert.assertFalse(topicMeta.matchesOwner("sn1", 5L)); } @Test public void testOwnerLeaseDurationGeneratesExpireTime() { - final long createStartTimeMs = System.currentTimeMillis(); final long ownerLeaseDurationMs = 60000; final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); @@ -213,65 +194,13 @@ public void testOwnerLeaseDurationGeneratesExpireTime() { final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + // The lease duration is persisted, but no absolute expiry is derived at construction time: the + // expiry is established only by the DataNode-local owner-lease heartbeat. Assert.assertEquals(ownerLeaseDurationMs, topicMeta.getOwnerLeaseDurationMs().longValue()); - Assert.assertNotNull(topicMeta.getOwnerLeaseExpireTimeMs()); - Assert.assertTrue( - topicMeta.getOwnerLeaseExpireTimeMs() >= createStartTimeMs + ownerLeaseDurationMs); - Assert.assertTrue( - topicMeta.getOwnerLeaseExpireTimeMs() <= System.currentTimeMillis() + ownerLeaseDurationMs); Assert.assertEquals( - topicMeta.getOwnerLeaseExpireTimeMs().longValue(), - topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY).longValue()); - } - - @Test - public void testRenewTopicOwnerLeaseRejectsInvalidProgression() { - final long ownerLeaseExpireTimeMs = System.currentTimeMillis() + 60000; - final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); - topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); - topicAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, String.valueOf(ownerLeaseExpireTimeMs)); - final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); - - Assert.assertThrows( - IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn2", 5L, ownerLeaseExpireTimeMs + 60000)); - Assert.assertThrows( - IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn1", 4L, ownerLeaseExpireTimeMs + 60000)); - Assert.assertThrows( - IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn1", 5L, ownerLeaseExpireTimeMs - 1)); - Assert.assertThrows( - IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() - 1)); - Assert.assertThrows( - IllegalArgumentException.class, () -> topicMeta.renewOwnerLeaseWithDuration("sn1", 5L, 0)); - } - - @Test - public void testRenewTopicOwnerLeaseRejectsExpiredLease() { - final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); - topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); - topicAttributes.put( - TopicConstant.OWNER_LEASE_EXPIRE_TIME_MS_KEY, - String.valueOf(System.currentTimeMillis() - 1)); - final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); - - Assert.assertThrows( - IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() + 60000)); - } - - @Test - public void testRenewTopicOwnerLeaseRejectsDisabledOwnerFencing() { - final TopicMeta topicMeta = new TopicMeta("test_topic", 1, new HashMap<>()); - - Assert.assertThrows( - IllegalArgumentException.class, - () -> topicMeta.renewOwnerLease("sn1", 5L, System.currentTimeMillis() + 60000)); + ownerLeaseDurationMs, + topicMeta.getConfig().getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY).longValue()); + Assert.assertNull(topicMeta.getOwnerLeaseExpireTimeMs()); } @Test @@ -299,4 +228,41 @@ public void testGenerateExtractorAttributesWithEncryptedPassword() { Assert.assertEquals("test_user", extractorAttributes.get("username")); Assert.assertEquals("encrypted-password", extractorAttributes.get("password")); } + + @Test + public void testOwnerEpochNeverReusedAcrossClearAndSerialization() throws IOException { + final Map topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + Assert.assertEquals(6L, topicMeta.getMaxOwnerEpoch()); + + // Clearing the owner retains the global epoch baseline. + topicMeta.clearOwner(); + Assert.assertFalse(topicMeta.isOwnerFencingEnabled()); + Assert.assertEquals(6L, topicMeta.getMaxOwnerEpoch()); + + // The baseline survives serialization even though the owner is cleared. + final TopicMeta deserialized = TopicMeta.deserialize(topicMeta.serialize()); + Assert.assertFalse(deserialized.isOwnerFencingEnabled()); + Assert.assertEquals(6L, deserialized.getMaxOwnerEpoch()); + + // Re-enabling with an epoch <= the historical max (reuse) is rejected. + final Map reuse = new HashMap<>(); + reuse.put(TopicConstant.OWNER_ID_KEY, "sn2"); + reuse.put(TopicConstant.OWNER_EPOCH_KEY, "6"); + final TopicMeta reuseMeta = deserialized.deepCopyWithUpdatedAttributes(reuse); + Assert.assertThrows( + IllegalArgumentException.class, + () -> TopicMeta.validateOwnerProgression(deserialized, reuseMeta)); + + // Re-enabling with an epoch strictly greater than the historical max is allowed. + final Map next = new HashMap<>(); + next.put(TopicConstant.OWNER_ID_KEY, "sn2"); + next.put(TopicConstant.OWNER_EPOCH_KEY, "7"); + final TopicMeta nextMeta = deserialized.deepCopyWithUpdatedAttributes(next); + TopicMeta.validateOwnerProgression(deserialized, nextMeta); + Assert.assertEquals(7L, nextMeta.getOwnerEpoch()); + Assert.assertEquals(7L, nextMeta.getMaxOwnerEpoch()); + } } diff --git a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift index 9b55ce75abede..11de2e4cba232 100644 --- a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift +++ b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift @@ -289,7 +289,6 @@ struct TDataNodeHeartbeatReq { // Using 8 bit to represent 8 bool // lowest bit: enable separation of admin powers 16: optional byte booleanVariables1 - 17: optional list topicMetas } struct TDataNodeActivation { @@ -583,6 +582,21 @@ struct TPushTopicMetaRespExceptionMessage { 3: required i64 timeStamp } +// Dedicated subscription owner-lease heartbeat (CN -> DN), decoupled from the node heartbeat. +// Carries a relative remaining duration; the DataNode derives the local expire time on its own +// clock (leaseExpireAt = localNow + leaseRemainingMs), so no absolute timestamp is compared across +// ConfigNode and DataNode clocks. +struct TTopicOwnerLeaseEntry { + 1: required string topicName + 2: required string ownerId + 3: required i64 ownerEpoch + 4: required i64 leaseRemainingMs +} + +struct TPushTopicOwnerLeaseReq { + 1: required list ownerLeases +} + struct TPushConsumerGroupMetaReq { 1: required list consumerGroupMetas } @@ -1234,6 +1248,12 @@ service IDataNodeRPCService { */ TPushTopicMetaResp pushMultiTopicMeta(TPushMultiTopicMetaReq req) + /** + * Renew topic owner leases on DataNodes via the dedicated subscription owner heartbeat + * (independent from the node heartbeat). Carries relative remaining durations. + */ + common.TSStatus pushTopicOwnerLease(TPushTopicOwnerLeaseReq req) + /** * Send consumerGroupMetas to DataNodes, for synchronization */ From 6787927100f20e13781a93de90fd31dd272f158f Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 11 Jun 2026 12:41:18 +0000 Subject: [PATCH 09/14] Move subscription owner fencing messages into i18n constants Address the i18n review comments: the owner-validation exception messages in TopicMeta now use SubscriptionMessages (en/zh), and the alter-topic warn in SubscriptionCoordinator uses ManagerMessages. The previously flagged renewTopicOwnerLease message no longer exists after the lease rework. --- .../i18n/SubscriptionMessages.java | 25 +++++++++++++++ .../i18n/SubscriptionMessages.java | 20 ++++++++++++ .../confignode/i18n/ManagerMessages.java | 2 ++ .../confignode/i18n/ManagerMessages.java | 2 ++ .../subscription/SubscriptionCoordinator.java | 2 +- .../subscription/meta/topic/TopicMeta.java | 31 +++++++++---------- 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java b/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java index 3b03471138da9..f19f35b705e6d 100644 --- a/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java +++ b/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java @@ -91,5 +91,30 @@ public final class SubscriptionMessages { public static final String CONSUMER_SUBMIT_ENDPOINTS_SYNCER = "SubscriptionConsumer {} submit endpoints syncer"; + // --- TopicMeta (owner fencing) --- + public static final String OWNER_ID_SHOULD_NOT_BE_EMPTY = + "Subscription topic owner id should not be empty"; + public static final String OWNER_EPOCH_SHOULD_NOT_BE_NEGATIVE = + "Subscription topic owner epoch should not be negative"; + public static final String OWNER_EPOCH_SHOULD_INCREASE_MONOTONICALLY = + "Subscription topic owner epoch should increase monotonically, current epoch is %s, incoming" + + " epoch is %s"; + public static final String OWNER_LEASE_DURATION_SHOULD_BE_POSITIVE = + "Subscription topic owner lease duration should be positive, topic: %s," + + " owner-lease-duration-ms: %s"; + public static final String OWNER_EPOCH_SHOULD_NEVER_BE_REUSED = + "Subscription topic owner epoch should never be reused, topic: %s, max granted owner-epoch:" + + " %s, incoming owner-id: %s, incoming owner-epoch: %s"; + public static final String OWNER_SHOULD_NOT_BE_CLEARED_BY_STALE_META = + "Subscription topic owner should not be cleared by stale topic meta, topic: %s, current" + + " owner-id: %s, current owner-epoch: %s"; + public static final String OWNER_EPOCH_SHOULD_NOT_ROLL_BACK = + "Subscription topic owner epoch should not roll back, topic: %s, current owner-id: %s, current" + + " owner-epoch: %s, incoming owner-id: %s, incoming owner-epoch: %s"; + public static final String OWNER_EPOCH_SHOULD_BE_SET_WHEN_OWNER_ID_SET = + "Subscription topic owner epoch should be set when %s is set"; + public static final String OWNER_LEASE_DURATION_SHOULD_BE_POSITIVE_WHEN_SET = + "Subscription topic owner lease duration should be positive when %s is set"; + private SubscriptionMessages() {} } diff --git a/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java b/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java index ccd186c9ed348..05d0639d15d5d 100644 --- a/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java +++ b/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java @@ -91,5 +91,25 @@ public final class SubscriptionMessages { public static final String CONSUMER_SUBMIT_ENDPOINTS_SYNCER = "SubscriptionConsumer {} 提交端点同步器"; + // --- TopicMeta (owner fencing) --- + public static final String OWNER_ID_SHOULD_NOT_BE_EMPTY = "订阅 topic 的 owner id 不应为空"; + public static final String OWNER_EPOCH_SHOULD_NOT_BE_NEGATIVE = "订阅 topic 的 owner epoch 不应为负数"; + public static final String OWNER_EPOCH_SHOULD_INCREASE_MONOTONICALLY = + "订阅 topic 的 owner epoch 应单调递增,当前 epoch 为 %s,传入 epoch 为 %s"; + public static final String OWNER_LEASE_DURATION_SHOULD_BE_POSITIVE = + "订阅 topic 的 owner 租约时长应为正数,topic:%s,owner-lease-duration-ms:%s"; + public static final String OWNER_EPOCH_SHOULD_NEVER_BE_REUSED = + "订阅 topic 的 owner epoch 不应被复用,topic:%s,已授予的最大 owner-epoch:%s,传入 owner-id:%s,传入" + + " owner-epoch:%s"; + public static final String OWNER_SHOULD_NOT_BE_CLEARED_BY_STALE_META = + "订阅 topic 的 owner 不应被过期的 topic meta 清除,topic:%s,当前 owner-id:%s,当前 owner-epoch:%s"; + public static final String OWNER_EPOCH_SHOULD_NOT_ROLL_BACK = + "订阅 topic 的 owner epoch 不应回退,topic:%s,当前 owner-id:%s,当前 owner-epoch:%s,传入" + + " owner-id:%s,传入 owner-epoch:%s"; + public static final String OWNER_EPOCH_SHOULD_BE_SET_WHEN_OWNER_ID_SET = + "当设置 %s 时,订阅 topic 的 owner epoch 也应被设置"; + public static final String OWNER_LEASE_DURATION_SHOULD_BE_POSITIVE_WHEN_SET = + "当设置 %s 时,订阅 topic 的 owner 租约时长应为正数"; + private SubscriptionMessages() {} } diff --git a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java index 97c70d8de1631..27d00d0b2ca0a 100644 --- a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java +++ b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java @@ -136,6 +136,8 @@ public final class ManagerMessages { "Failed to create topic {} with attributes {}. Result status: {}."; public static final String FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED = "Failed to alter topic %s, the topic is not existed"; + public static final String FAILED_TO_ALTER_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS = + "Failed to alter topic {} with attributes {}. Result status: {}."; public static final String FAILED_TO_DEEP_COPY_PIPEMETA = "failed to deep copy pipeMeta"; public static final String FAILED_TO_DEREGISTER_PIPE_CONFIG_REGION_CONNECTOR = "Failed to deregister pipe config region connector metrics, PipeConfigNodeSubtask({}) does not exist"; diff --git a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java index 8bb058e4ff026..b4295bc9c7d61 100644 --- a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java +++ b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java @@ -135,6 +135,8 @@ public final class ManagerMessages { "Failed to create topic {} with attributes {}. Result status: {}."; public static final String FAILED_TO_ALTER_TOPIC_THE_TOPIC_IS_NOT_EXISTED = "修改 topic %s 失败,该 topic 不存在"; + public static final String FAILED_TO_ALTER_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS = + "修改 topic {} 失败,属性:{}。结果状态:{}。"; public static final String FAILED_TO_DEEP_COPY_PIPEMETA = "深拷贝 pipeMeta 失败"; public static final String FAILED_TO_DEREGISTER_PIPE_CONFIG_REGION_CONNECTOR = "Failed to deregister pipe config region connector metrics, PipeConfigNodeSubtask({}) does not exist"; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java index 52c8ddd0ff8e6..6fbda89257b46 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java @@ -175,7 +175,7 @@ public TSStatus alterTopic(TAlterTopicReq req) { final TSStatus status = configManager.getProcedureManager().alterTopic(req); if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { LOGGER.warn( - "Failed to alter topic {} with attributes {}, result status is {}.", + ManagerMessages.FAILED_TO_ALTER_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS, req.getTopicName(), req.getTopicAttributes(), status); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java index 4a828f09bbc80..c44692f63588d 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/topic/TopicMeta.java @@ -25,6 +25,7 @@ import org.apache.iotdb.commons.utils.TestOnly; import org.apache.iotdb.rpc.subscription.config.TopicConfig; import org.apache.iotdb.rpc.subscription.config.TopicConstant; +import org.apache.iotdb.rpc.subscription.i18n.SubscriptionMessages; import org.apache.tsfile.utils.PublicBAOS; import org.apache.tsfile.utils.ReadWriteIOUtils; @@ -160,24 +161,24 @@ public void transferOwner(final String ownerId, final long ownerEpoch) { public void transferOwner( final String ownerId, final long ownerEpoch, final Long ownerLeaseDurationMs) { if (Objects.isNull(ownerId) || ownerId.isEmpty()) { - throw new IllegalArgumentException("Subscription topic owner id should not be empty"); + throw new IllegalArgumentException(SubscriptionMessages.OWNER_ID_SHOULD_NOT_BE_EMPTY); } if (ownerEpoch < 0) { - throw new IllegalArgumentException("Subscription topic owner epoch should not be negative"); + throw new IllegalArgumentException(SubscriptionMessages.OWNER_EPOCH_SHOULD_NOT_BE_NEGATIVE); } if (isOwnerFencingEnabled() && ownerEpoch <= this.ownerEpoch) { throw new IllegalArgumentException( String.format( - "Subscription topic owner epoch should increase monotonically, current epoch is %s," - + " incoming epoch is %s", - this.ownerEpoch, ownerEpoch)); + SubscriptionMessages.OWNER_EPOCH_SHOULD_INCREASE_MONOTONICALLY, + this.ownerEpoch, + ownerEpoch)); } if (Objects.nonNull(ownerLeaseDurationMs) && ownerLeaseDurationMs <= 0) { throw new IllegalArgumentException( String.format( - "Subscription topic owner lease duration should be positive, topic: %s," - + " owner-lease-duration-ms: %s", - topicName, ownerLeaseDurationMs)); + SubscriptionMessages.OWNER_LEASE_DURATION_SHOULD_BE_POSITIVE, + topicName, + ownerLeaseDurationMs)); } this.ownerId = ownerId; @@ -358,8 +359,7 @@ public static void validateOwnerProgression( && updatedTopicMeta.getOwnerEpoch() <= currentTopicMeta.getMaxOwnerEpoch()) { throw new IllegalArgumentException( String.format( - "Subscription topic owner epoch should never be reused, topic: %s, max granted" - + " owner-epoch: %s, incoming owner-id: %s, incoming owner-epoch: %s", + SubscriptionMessages.OWNER_EPOCH_SHOULD_NEVER_BE_REUSED, currentTopicMeta.getTopicName(), currentTopicMeta.getMaxOwnerEpoch(), updatedTopicMeta.getOwnerId(), @@ -373,8 +373,7 @@ public static void validateOwnerProgression( if (!updatedTopicMeta.isOwnerFencingEnabled()) { throw new IllegalArgumentException( String.format( - "Subscription topic owner should not be cleared by stale topic meta, topic: %s," - + " current owner-id: %s, current owner-epoch: %s", + SubscriptionMessages.OWNER_SHOULD_NOT_BE_CLEARED_BY_STALE_META, currentTopicMeta.getTopicName(), currentTopicMeta.getOwnerId(), currentTopicMeta.getOwnerEpoch())); @@ -388,9 +387,7 @@ public static void validateOwnerProgression( if (epochRollback || sameEpochOwnerChanged) { throw new IllegalArgumentException( String.format( - "Subscription topic owner epoch should not roll back, topic: %s, current owner-id:" - + " %s, current owner-epoch: %s, incoming owner-id: %s, incoming owner-epoch:" - + " %s", + SubscriptionMessages.OWNER_EPOCH_SHOULD_NOT_ROLL_BACK, currentTopicMeta.getTopicName(), currentTopicMeta.getOwnerId(), currentTopicMeta.getOwnerEpoch(), @@ -417,7 +414,7 @@ private void initOwnerFromTopicAttributes(final Map topicAttribu if (Objects.isNull(configuredOwnerEpoch)) { throw new IllegalArgumentException( String.format( - "Subscription topic owner epoch should be set when %s is set", + SubscriptionMessages.OWNER_EPOCH_SHOULD_BE_SET_WHEN_OWNER_ID_SET, TopicConstant.OWNER_ID_KEY)); } final Long ownerLeaseDurationMs = @@ -425,7 +422,7 @@ private void initOwnerFromTopicAttributes(final Map topicAttribu if (Objects.nonNull(ownerLeaseDurationMs) && ownerLeaseDurationMs <= 0) { throw new IllegalArgumentException( String.format( - "Subscription topic owner lease duration should be positive when %s is set", + SubscriptionMessages.OWNER_LEASE_DURATION_SHOULD_BE_POSITIVE_WHEN_SET, TopicConstant.OWNER_LEASE_DURATION_MS_KEY)); } transferOwner(configuredOwnerId, configuredOwnerEpoch, ownerLeaseDurationMs); From 14e64d135534b4e3341ab1f15e6fc9262f44c434 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 11 Jun 2026 16:05:05 +0000 Subject: [PATCH 10/14] Reword owner-lease syncer TODO to satisfy the todo-check CI gate --- .../manager/subscription/SubscriptionOwnerLeaseSyncer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java index 2cff0a0726878..bbc22a35c8d84 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionOwnerLeaseSyncer.java @@ -43,7 +43,8 @@ public class SubscriptionOwnerLeaseSyncer { private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionOwnerLeaseSyncer.class); - // TODO: promote to SubscriptionConfig; invariant: lease-duration >> interval * tolerated-misses. + // Note: candidate for promotion to SubscriptionConfig; invariant must hold: lease-duration >> + // interval * tolerated-misses. private static final long OWNER_LEASE_HEARTBEAT_INTERVAL_SECONDS = 5; /** The owner-lease heartbeat interval {@code H}, used by the transfer admission-wait bound. */ From 2ead1324dbed30a40fec81f83d3226c3b4b5a05f Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 11 Jun 2026 16:39:05 +0000 Subject: [PATCH 11/14] Enable the local topic-owner fencing IT (drop stale @Ignore) AbstractSubscriptionLocalIT already enables subscription, so the end-to-end stale-owner-fencing + current-owner-consume scenario runs; verified locally via mvn verify -Dit.test=IoTDBSubscriptionTopicOwnerIT (tests=2, failures=0, errors=0, skipped=0). --- .../subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index 4bcbd1f7eca8f..cd96cb53101db 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -35,7 +35,6 @@ import org.apache.tsfile.read.query.dataset.ResultSet; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @@ -56,7 +55,6 @@ public void setUp() throws Exception { super.setUp(); } - @Ignore @Test public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws Exception { final String host = EnvFactory.getEnv().getIP(); From 0f66aabf464f03f8a54b1d11a507bbe7339bfa70 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 12 Jun 2026 02:54:51 +0000 Subject: [PATCH 12/14] Rename SN owner identifiers to neutral owner names in tests Drop 'SN' wording from the PR's new code: owner-id values sn1/sn2 -> owner1/owner2, consumer ids and vars stale_sn/current_sn/oldSnConsumer/newSnConsumer -> owner-based names, and the SN/DN/CN comment -> owner/DataNode/ConfigNode. Pure rename; all affected unit tests still pass. --- ...oTDBSubscriptionTopicOwnerPartitionIT.java | 28 +++++----- .../local/IoTDBSubscriptionTopicOwnerIT.java | 16 +++--- .../subscription/SubscriptionInfoTest.java | 26 ++++----- .../plan/parser/StatementGeneratorTest.java | 4 +- .../sql/parser/TopicStatementTest.java | 4 +- .../receiver/SubscriptionReceiverV1Test.java | 29 +++++----- .../subscription/topic/TopicDeSerTest.java | 56 +++++++++---------- 7 files changed, 83 insertions(+), 80 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java index d35d3c4795f58..92f3672452e7d 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java @@ -41,9 +41,9 @@ import java.util.Properties; /** - * Owner-fencing correctness under SN/DN/CN partition topologies, simulated at the node-process - * level via the IT framework (shutdown/start of ConfigNodes and DataNodes; ConfigNode failover via - * leader shutdown). Runs on a 3 ConfigNode + 3 DataNode cluster. + * Owner-fencing correctness under owner/DataNode/ConfigNode partition topologies, simulated at the + * node-process level via the IT framework (shutdown/start of ConfigNodes and DataNodes; ConfigNode + * failover via leader shutdown). Runs on a 3 ConfigNode + 3 DataNode cluster. * *

Covers: * @@ -88,7 +88,7 @@ public void testStaleOwnerFencedOnMultiDataNodeTopic() throws Exception { final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); final String topicName = "topic_owner_multi_dn"; - createOwnedTopic(host, port, topicName, "sn2", 6L); + createOwnedTopic(host, port, topicName, "owner2", 6L); try { // Spread data across multiple devices (and thus, with multiple data regions, multiple // DataNodes) before consuming. @@ -96,11 +96,12 @@ public void testStaleOwnerFencedOnMultiDataNodeTopic() throws Exception { // A stale owner (epoch 5 < current 6) must be fenced regardless of which DataNode serves the // request. - assertSubscribeFenced(host, port, topicName, "stale_multi_dn", "sn1", 5L); + assertSubscribeFenced(host, port, topicName, "stale_multi_dn", "owner1", 5L); // The current owner can subscribe. try (final SubscriptionTreePullConsumer currentOwnerConsumer = - ownerConsumer(host, port, "current_multi_dn", "topic_owner_multi_dn_group", "sn2", 6L)) { + ownerConsumer( + host, port, "current_multi_dn", "topic_owner_multi_dn_group", "owner2", 6L)) { currentOwnerConsumer.open(); currentOwnerConsumer.subscribe(topicName); currentOwnerConsumer.unsubscribe(topicName); @@ -122,14 +123,14 @@ public void testConfigNodeFailoverPersistsOwnerEpochAndKeepsFencing() throws Exc final Properties properties = new Properties(); properties.put(TopicConstant.PATH_KEY, HEAD_PATH + ".**"); properties.put(TopicConstant.START_TIME_KEY, "0"); - properties.put(TopicConstant.OWNER_ID_KEY, "sn1"); + properties.put(TopicConstant.OWNER_ID_KEY, "owner1"); properties.put(TopicConstant.OWNER_EPOCH_KEY, "5"); properties.put( TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); session.createTopic(topicName, properties); - // Transfer to sn2/epoch 6 (this persists owner + epoch via ConfigNode consensus). - session.alterTopicOwner(topicName, "sn2", 6L); + // Transfer to owner2/epoch 6 (this persists owner + epoch via ConfigNode consensus). + session.alterTopicOwner(topicName, "owner2", 6L); Assert.assertTrue(getTopicAttributes(session, topicName).contains("owner-epoch=6")); } @@ -145,11 +146,11 @@ public void testConfigNodeFailoverPersistsOwnerEpochAndKeepsFencing() throws Exc try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { session.open(); final String attributes = getTopicAttributes(session, topicName); - Assert.assertTrue(attributes.contains("owner-id=sn2")); + Assert.assertTrue(attributes.contains("owner-id=owner2")); Assert.assertTrue(attributes.contains("owner-epoch=6")); } }); - assertSubscribeFenced(host, port, topicName, "stale_after_failover", "sn1", 5L); + assertSubscribeFenced(host, port, topicName, "stale_after_failover", "owner1", 5L); } finally { EnvFactory.getEnv().startConfigNode(leaderIndex); dropTopic(host, port, topicName); @@ -162,7 +163,7 @@ public void testDataNodeRestartReSyncsOwnerFencing() throws Exception { final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); final String topicName = "topic_owner_dn_restart"; - createOwnedTopic(host, port, topicName, "sn2", 6L); + createOwnedTopic(host, port, topicName, "owner2", 6L); try { // Restart a DataNode: it loses in-memory owner metadata and must re-sync it on restart. EnvFactory.getEnv().shutdownDataNode(1); @@ -170,7 +171,8 @@ public void testDataNodeRestartReSyncsOwnerFencing() throws Exception { // After the DataNode comes back and re-syncs owner metadata, the stale owner stays fenced. IoTDBSubscriptionITConstant.AWAIT.untilAsserted( - () -> assertSubscribeFenced(host, port, topicName, "stale_after_dn_restart", "sn1", 5L)); + () -> + assertSubscribeFenced(host, port, topicName, "stale_after_dn_restart", "owner1", 5L)); } finally { dropTopic(host, port, topicName); } diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index cd96cb53101db..d6dae42515c61 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -66,7 +66,7 @@ public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws final Properties properties = new Properties(); properties.put(TopicConstant.PATH_KEY, "root.topic_owner.**"); properties.put(TopicConstant.START_TIME_KEY, "0"); - properties.put(TopicConstant.OWNER_ID_KEY, "sn2"); + properties.put(TopicConstant.OWNER_ID_KEY, "owner2"); properties.put(TopicConstant.OWNER_EPOCH_KEY, "6"); session.createTopic(topicName, properties); } @@ -76,9 +76,9 @@ public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws new SubscriptionTreePullConsumer.Builder() .host(host) .port(port) - .consumerId("stale_sn") + .consumerId("stale_owner") .consumerGroupId("topic_owner_group") - .ownerId("sn1") + .ownerId("owner1") .ownerEpoch(5L) .autoCommit(false) .buildPullConsumer()) { @@ -91,9 +91,9 @@ public void testTopicOwnerFencingRejectsStaleOwnerAndAllowsCurrentOwner() throws new SubscriptionTreePullConsumer.Builder() .host(host) .port(port) - .consumerId("current_sn") + .consumerId("current_owner") .consumerGroupId("topic_owner_group") - .ownerId("sn2") + .ownerId("owner2") .ownerEpoch(6L) .autoCommit(false) .buildPullConsumer()) { @@ -136,7 +136,7 @@ public void testAlterTopicOwnerWaitsForLeaseDrainBeforeInstallingNewOwner() thro final Properties properties = new Properties(); properties.put(TopicConstant.PATH_KEY, "root.topic_owner.**"); properties.put(TopicConstant.START_TIME_KEY, "0"); - properties.put(TopicConstant.OWNER_ID_KEY, "sn1"); + properties.put(TopicConstant.OWNER_ID_KEY, "owner1"); properties.put(TopicConstant.OWNER_EPOCH_KEY, "5"); properties.put( TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); @@ -149,7 +149,7 @@ public void testAlterTopicOwnerWaitsForLeaseDrainBeforeInstallingNewOwner() thro // duration. This is the admission gate that prevents cross-DataNode double-active // consuming. final long alterStartTimeMs = System.currentTimeMillis(); - session.alterTopicOwner(topicName, "sn2", 6L); + session.alterTopicOwner(topicName, "owner2", 6L); final long alterElapsedTimeMs = System.currentTimeMillis() - alterStartTimeMs; Assert.assertTrue( "alterTopicOwner should block at least the lease drain duration, but only took " @@ -158,7 +158,7 @@ public void testAlterTopicOwnerWaitsForLeaseDrainBeforeInstallingNewOwner() thro alterElapsedTimeMs >= ownerLeaseDurationMs); final String topicAttributes = getTopicAttributes(session, topicName); - Assert.assertTrue(topicAttributes.contains("owner-id=sn2")); + Assert.assertTrue(topicAttributes.contains("owner-id=owner2")); Assert.assertTrue(topicAttributes.contains("owner-epoch=6")); } finally { session.dropTopicIfExists(topicName); diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java index 9e8e5ec7cbd0a..9b86f1d402af0 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java @@ -46,23 +46,23 @@ public void testAlterTopicRejectsOwnerEpochRollback() { final String topicName = "topic-" + UUID.randomUUID(); final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); + final TopicMeta initialTopicMeta = createTopicMeta(topicName, "owner1", 5L); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); final TopicMeta transferredTopicMeta = initialTopicMeta.deepCopy(); - transferredTopicMeta.transferOwner("sn2", 6L); + transferredTopicMeta.transferOwner("owner2", 6L); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo.alterTopic(new AlterTopicPlan(transferredTopicMeta)).getCode()); final TSStatus rollbackStatus = - subscriptionInfo.alterTopic(new AlterTopicPlan(createTopicMeta(topicName, "sn1", 5L))); + subscriptionInfo.alterTopic(new AlterTopicPlan(createTopicMeta(topicName, "owner1", 5L))); Assert.assertEquals( TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode(), rollbackStatus.getCode()); - Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals("owner2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); } @@ -71,21 +71,21 @@ public void testAlterTopicTransfersOwnerWithUpdatedAttributes() { final String topicName = "topic-" + UUID.randomUUID(); final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); + final TopicMeta initialTopicMeta = createTopicMeta(topicName, "owner1", 5L); initialTopicMeta.getConfig().getAttribute().put(TopicConstant.PATH_KEY, "root.sg.**"); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); final Map updatedAttributes = new HashMap<>(); - updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "owner2"); updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); final TSStatus alterStatus = subscriptionInfo.alterTopic( new AlterTopicPlan(initialTopicMeta.deepCopyWithUpdatedAttributes(updatedAttributes))); Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), alterStatus.getCode()); - Assert.assertEquals("sn2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); + Assert.assertEquals("owner2", subscriptionInfo.getTopicMeta(topicName).getOwnerId()); Assert.assertEquals(6L, subscriptionInfo.getTopicMeta(topicName).getOwnerEpoch()); Assert.assertEquals( "root.sg.**", @@ -103,14 +103,14 @@ public void testCollectTopicOwnerLeaseEntriesSkipsBlockedTopic() { TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo .createTopic( - new CreateTopicPlan(createTopicMeta(topicName, "sn1", 5L, ownerLeaseDurationMs))) + new CreateTopicPlan(createTopicMeta(topicName, "owner1", 5L, ownerLeaseDurationMs))) .getCode()); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), subscriptionInfo .createTopic( new CreateTopicPlan( - createTopicMeta(blockedTopicName, "sn1", 5L, ownerLeaseDurationMs))) + createTopicMeta(blockedTopicName, "owner1", 5L, ownerLeaseDurationMs))) .getCode()); final List entries = @@ -119,7 +119,7 @@ public void testCollectTopicOwnerLeaseEntriesSkipsBlockedTopic() { Assert.assertEquals(1, entries.size()); final TTopicOwnerLeaseEntry entry = entries.get(0); Assert.assertEquals(topicName, entry.getTopicName()); - Assert.assertEquals("sn1", entry.getOwnerId()); + Assert.assertEquals("owner1", entry.getOwnerId()); Assert.assertEquals(5L, entry.getOwnerEpoch()); // Relative remaining duration equals the configured lease duration; read-only, no mutation. Assert.assertEquals(ownerLeaseDurationMs, entry.getLeaseRemainingMs()); @@ -131,7 +131,7 @@ public void testAlterTopicOwnerAndShowTopicOwner() { final long ownerLeaseDurationMs = 123456L; final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - final TopicMeta initialTopicMeta = createTopicMeta(topicName, "sn1", 5L); + final TopicMeta initialTopicMeta = createTopicMeta(topicName, "owner1", 5L); initialTopicMeta.getConfig().getAttribute().put(TopicConstant.PATH_KEY, "root.sg.**"); initialTopicMeta.getConfig().getAttribute().put(TopicConstant.START_TIME_KEY, "0"); Assert.assertEquals( @@ -139,7 +139,7 @@ public void testAlterTopicOwnerAndShowTopicOwner() { subscriptionInfo.createTopic(new CreateTopicPlan(initialTopicMeta)).getCode()); final Map updatedAttributes = new HashMap<>(); - updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "owner2"); updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); updatedAttributes.put( TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); @@ -160,7 +160,7 @@ public void testAlterTopicOwnerAndShowTopicOwner() { Assert.assertEquals(1L, showTopicInfo.getCreationTime()); Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("path=root.sg.**")); Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("start-time=0")); - Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("owner-id=sn2")); + Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("owner-id=owner2")); Assert.assertTrue(showTopicInfo.getTopicAttributes().contains("owner-epoch=6")); Assert.assertTrue( showTopicInfo diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java index 574308f5ec45d..febf29bea1553 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java @@ -126,13 +126,13 @@ public class StatementGeneratorTest { public void testAlterTopicStatement() { final Statement statement = StatementGenerator.createStatement( - "ALTER TOPIC topic1 WITH ('owner-id'='sn2','owner-epoch'='6')", + "ALTER TOPIC topic1 WITH ('owner-id'='owner2','owner-epoch'='6')", ZonedDateTime.now().getOffset()); Assert.assertTrue(statement instanceof AlterTopicStatement); final AlterTopicStatement alterTopicStatement = (AlterTopicStatement) statement; Assert.assertEquals("topic1", alterTopicStatement.getTopicName()); - Assert.assertEquals("sn2", alterTopicStatement.getTopicAttributes().get("owner-id")); + Assert.assertEquals("owner2", alterTopicStatement.getTopicAttributes().get("owner-id")); Assert.assertEquals("6", alterTopicStatement.getTopicAttributes().get("owner-epoch")); } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java index 66aac145968ef..d457bdbe55f44 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/TopicStatementTest.java @@ -35,14 +35,14 @@ public class TopicStatementTest { public void testAlterTopicStatement() { final Statement statement = sqlParser.createStatement( - "ALTER TOPIC topic1 WITH ('owner-id'='sn2','owner-epoch'='6')", + "ALTER TOPIC topic1 WITH ('owner-id'='owner2','owner-epoch'='6')", ZoneId.systemDefault(), null); Assert.assertTrue(statement instanceof AlterTopic); final AlterTopic alterTopic = (AlterTopic) statement; Assert.assertEquals("topic1", alterTopic.getTopicName()); - Assert.assertEquals("sn2", alterTopic.getTopicAttributes().get("owner-id")); + Assert.assertEquals("owner2", alterTopic.getTopicAttributes().get("owner-id")); Assert.assertEquals("6", alterTopic.getTopicAttributes().get("owner-epoch")); } } 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 c28111ec9d110..0890ae8100aaa 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 @@ -91,17 +91,18 @@ public void testCalculateConsumerInactivityTimeoutUsesHeartbeatMultiple() throws public void testTopicOwnerFencingStatus() { final String topicName = "topic-" + UUID.randomUUID(); - SubscriptionAgent.topic().handleSingleTopicMetaChanges(createTopicMeta(topicName, "sn1", 7L)); + SubscriptionAgent.topic() + .handleSingleTopicMetaChanges(createTopicMeta(topicName, "owner1", 7L)); try { Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), SubscriptionAgent.topic() - .checkTopicOwner(createConsumerConfig(1_000L, "sn1", 7L), topicName) + .checkTopicOwner(createConsumerConfig(1_000L, "owner1", 7L), topicName) .getCode()); Assert.assertEquals( TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), SubscriptionAgent.topic() - .checkTopicOwner(createConsumerConfig(1_000L, "sn2", 7L), topicName) + .checkTopicOwner(createConsumerConfig(1_000L, "owner2", 7L), topicName) .getCode()); Assert.assertEquals( TSStatusCode.SUBSCRIPTION_OWNER_REQUIRED.getStatusCode(), @@ -116,41 +117,41 @@ public void testTopicOwnerFencingStatus() { @Test public void testOldOwnerFencedAfterNetworkPartitionAndTopicOwnerTransfer() { final String topicName = "topic-" + UUID.randomUUID(); - final TopicMeta topicMeta = createTopicMeta(topicName, "sn1", 5L); - final ConsumerConfig oldSnConsumer = createConsumerConfig(1_000L, "sn1", 5L); - final ConsumerConfig newSnConsumer = createConsumerConfig(1_000L, "sn2", 6L); + final TopicMeta topicMeta = createTopicMeta(topicName, "owner1", 5L); + final ConsumerConfig oldOwnerConsumer = createConsumerConfig(1_000L, "owner1", 5L); + final ConsumerConfig newOwnerConsumer = createConsumerConfig(1_000L, "owner2", 6L); SubscriptionAgent.topic().handleSingleTopicMetaChanges(topicMeta); try { Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), - SubscriptionAgent.topic().checkTopicOwner(oldSnConsumer, topicName).getCode()); + SubscriptionAgent.topic().checkTopicOwner(oldOwnerConsumer, topicName).getCode()); final TopicMeta transferredTopicMeta = topicMeta.deepCopy(); - transferredTopicMeta.transferOwner("sn2", 6L); + transferredTopicMeta.transferOwner("owner2", 6L); SubscriptionAgent.topic().handleSingleTopicMetaChanges(transferredTopicMeta); Assert.assertEquals( TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), - SubscriptionAgent.topic().checkTopicOwner(oldSnConsumer, topicName).getCode()); + SubscriptionAgent.topic().checkTopicOwner(oldOwnerConsumer, topicName).getCode()); Assert.assertEquals( TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), SubscriptionAgent.topic() - .checkTopicOwners(oldSnConsumer, Collections.singleton(topicName)) + .checkTopicOwners(oldOwnerConsumer, Collections.singleton(topicName)) .getCode()); Assert.assertNotNull( SubscriptionAgent.topic() - .handleSingleTopicMetaChanges(createTopicMeta(topicName, "sn1", 5L))); + .handleSingleTopicMetaChanges(createTopicMeta(topicName, "owner1", 5L))); Assert.assertEquals( TSStatusCode.SUBSCRIPTION_OWNER_FENCED.getStatusCode(), - SubscriptionAgent.topic().checkTopicOwner(oldSnConsumer, topicName).getCode()); + SubscriptionAgent.topic().checkTopicOwner(oldOwnerConsumer, topicName).getCode()); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), - SubscriptionAgent.topic().checkTopicOwner(newSnConsumer, topicName).getCode()); + SubscriptionAgent.topic().checkTopicOwner(newOwnerConsumer, topicName).getCode()); Assert.assertEquals( TSStatusCode.SUCCESS_STATUS.getStatusCode(), SubscriptionAgent.topic() - .checkTopicOwners(newSnConsumer, Collections.singleton(topicName)) + .checkTopicOwners(newOwnerConsumer, Collections.singleton(topicName)) .getCode()); } finally { SubscriptionAgent.topic().handleDropTopic(topicName); diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java index 17eb8adae6d2d..fa47c7241b521 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/topic/TopicDeSerTest.java @@ -61,17 +61,17 @@ public void test() throws IOException { @Test public void testTopicOwnerDeSer() throws IOException { Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); Assert.assertTrue(topicMeta.isOwnerFencingEnabled()); - Assert.assertEquals("sn1", topicMeta.getOwnerId()); + Assert.assertEquals("owner1", topicMeta.getOwnerId()); Assert.assertEquals(5L, topicMeta.getOwnerEpoch()); - Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); - Assert.assertFalse(topicMeta.matchesOwner("sn2", 5L)); - Assert.assertFalse(topicMeta.matchesOwner("sn1", 4L)); + Assert.assertTrue(topicMeta.matchesOwner("owner1", 5L)); + Assert.assertFalse(topicMeta.matchesOwner("owner2", 5L)); + Assert.assertFalse(topicMeta.matchesOwner("owner1", 4L)); TopicMeta topicMeta1 = TopicMeta.deserialize(topicMeta.serialize()); TopicMeta topicMeta2 = topicMeta1.deepCopy(); @@ -84,10 +84,10 @@ public void testTopicOwnerDeSer() throws IOException { Assert.assertEquals( topicMeta.getOwnerLeaseExpireTimeMs(), topicMeta2.getOwnerLeaseExpireTimeMs()); - topicMeta.transferOwner("sn2", 6L, 100L); - Assert.assertEquals("sn2", topicMeta.getOwnerId()); + topicMeta.transferOwner("owner2", 6L, 100L); + Assert.assertEquals("owner2", topicMeta.getOwnerId()); Assert.assertEquals(6L, topicMeta.getOwnerEpoch()); - Assert.assertEquals("sn2", topicMeta.getConfig().getString(TopicConstant.OWNER_ID_KEY)); + Assert.assertEquals("owner2", topicMeta.getConfig().getString(TopicConstant.OWNER_ID_KEY)); Assert.assertEquals( 6L, topicMeta.getConfig().getLong(TopicConstant.OWNER_EPOCH_KEY).longValue()); Assert.assertEquals( @@ -108,39 +108,39 @@ public void testTopicOwnerDeSer() throws IOException { public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndPreservesExistingConfig() { final Map topicAttributes = new HashMap<>(); topicAttributes.put(TopicConstant.PATH_KEY, "root.sg.**"); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); topicMeta.addSubscribedConsumerGroup("group1"); final Map updatedAttributes = new HashMap<>(); - updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "owner2"); updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); final TopicMeta updatedTopicMeta = topicMeta.deepCopyWithUpdatedAttributes(updatedAttributes); Assert.assertEquals( "root.sg.**", updatedTopicMeta.getConfig().getString(TopicConstant.PATH_KEY)); - Assert.assertEquals("sn2", updatedTopicMeta.getOwnerId()); + Assert.assertEquals("owner2", updatedTopicMeta.getOwnerId()); Assert.assertEquals(6L, updatedTopicMeta.getOwnerEpoch()); Assert.assertTrue(updatedTopicMeta.isSubscribedByConsumerGroup("group1")); - Assert.assertEquals("sn1", topicMeta.getOwnerId()); + Assert.assertEquals("owner1", topicMeta.getOwnerId()); Assert.assertEquals(5L, topicMeta.getOwnerEpoch()); } @Test public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLease() { final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); topicAttributes.put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, "60000"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); final Map updatedAttributes = new HashMap<>(); - updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "sn2"); + updatedAttributes.put(TopicConstant.OWNER_ID_KEY, "owner2"); updatedAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); final TopicMeta updatedTopicMeta = topicMeta.deepCopyWithUpdatedAttributes(updatedAttributes); - Assert.assertEquals("sn2", updatedTopicMeta.getOwnerId()); + Assert.assertEquals("owner2", updatedTopicMeta.getOwnerId()); Assert.assertEquals(6L, updatedTopicMeta.getOwnerEpoch()); Assert.assertNull(updatedTopicMeta.getOwnerLeaseDurationMs()); Assert.assertNull(updatedTopicMeta.getOwnerLeaseExpireTimeMs()); @@ -151,7 +151,7 @@ public void testDeepCopyWithUpdatedAttributesTransfersOwnerAndClearsExistingLeas @Test public void testOwnerLeaseExpireIsDataNodeLocalRuntimeOnly() throws IOException { final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); topicAttributes.put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, "60000"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); @@ -159,13 +159,13 @@ public void testOwnerLeaseExpireIsDataNodeLocalRuntimeOnly() throws IOException // Construction (ConfigNode side) never derives an absolute expiry; that is established only by // the DataNode-local owner-lease heartbeat. Assert.assertNull(topicMeta.getOwnerLeaseExpireTimeMs()); - Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); + Assert.assertTrue(topicMeta.matchesOwner("owner1", 5L)); // Apply a heartbeat with a relative remaining duration; expiry is now set locally and the owner // still matches before the lease drains. - topicMeta.applyOwnerLeaseFromHeartbeat("sn1", 5L, 60000L); + topicMeta.applyOwnerLeaseFromHeartbeat("owner1", 5L, 60000L); Assert.assertNotNull(topicMeta.getOwnerLeaseExpireTimeMs()); - Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); + Assert.assertTrue(topicMeta.matchesOwner("owner1", 5L)); // The locally-derived expiry is runtime-only: it is not a topic attribute, does not affect // equality, and does not survive serialization. @@ -174,20 +174,20 @@ public void testOwnerLeaseExpireIsDataNodeLocalRuntimeOnly() throws IOException Assert.assertEquals(topicMeta, roundTrip); // A stale/mismatched heartbeat is ignored. - topicMeta.applyOwnerLeaseFromHeartbeat("sn2", 5L, 60000L); - topicMeta.applyOwnerLeaseFromHeartbeat("sn1", 4L, 60000L); - Assert.assertTrue(topicMeta.matchesOwner("sn1", 5L)); + topicMeta.applyOwnerLeaseFromHeartbeat("owner2", 5L, 60000L); + topicMeta.applyOwnerLeaseFromHeartbeat("owner1", 4L, 60000L); + Assert.assertTrue(topicMeta.matchesOwner("owner1", 5L)); // An elapsed lease causes the current owner to stop matching (DataNode fencing). - topicMeta.applyOwnerLeaseFromHeartbeat("sn1", 5L, -1L); - Assert.assertFalse(topicMeta.matchesOwner("sn1", 5L)); + topicMeta.applyOwnerLeaseFromHeartbeat("owner1", 5L, -1L); + Assert.assertFalse(topicMeta.matchesOwner("owner1", 5L)); } @Test public void testOwnerLeaseDurationGeneratesExpireTime() { final long ownerLeaseDurationMs = 60000; final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "5"); topicAttributes.put( TopicConstant.OWNER_LEASE_DURATION_MS_KEY, String.valueOf(ownerLeaseDurationMs)); @@ -232,7 +232,7 @@ public void testGenerateExtractorAttributesWithEncryptedPassword() { @Test public void testOwnerEpochNeverReusedAcrossClearAndSerialization() throws IOException { final Map topicAttributes = new HashMap<>(); - topicAttributes.put(TopicConstant.OWNER_ID_KEY, "sn1"); + topicAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); topicAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "6"); final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); Assert.assertEquals(6L, topicMeta.getMaxOwnerEpoch()); @@ -249,7 +249,7 @@ public void testOwnerEpochNeverReusedAcrossClearAndSerialization() throws IOExce // Re-enabling with an epoch <= the historical max (reuse) is rejected. final Map reuse = new HashMap<>(); - reuse.put(TopicConstant.OWNER_ID_KEY, "sn2"); + reuse.put(TopicConstant.OWNER_ID_KEY, "owner2"); reuse.put(TopicConstant.OWNER_EPOCH_KEY, "6"); final TopicMeta reuseMeta = deserialized.deepCopyWithUpdatedAttributes(reuse); Assert.assertThrows( @@ -258,7 +258,7 @@ public void testOwnerEpochNeverReusedAcrossClearAndSerialization() throws IOExce // Re-enabling with an epoch strictly greater than the historical max is allowed. final Map next = new HashMap<>(); - next.put(TopicConstant.OWNER_ID_KEY, "sn2"); + next.put(TopicConstant.OWNER_ID_KEY, "owner2"); next.put(TopicConstant.OWNER_EPOCH_KEY, "7"); final TopicMeta nextMeta = deserialized.deepCopyWithUpdatedAttributes(next); TopicMeta.validateOwnerProgression(deserialized, nextMeta); From 0ba2df7a37e2af607d0445ba460575815c62cad3 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 12 Jun 2026 03:42:43 +0000 Subject: [PATCH 13/14] Add configurable minimum owner-lease-duration floor (default 1 min) Reject owner-lease-duration-ms below a configurable floor at topic create/alter admission, so the lease stays well above the ~5s renewal heartbeat and a missed beat cannot falsely fence a healthy owner. - New config subscription_owner_lease_duration_ms_min (CommonConfig, default 60000), loaded by CommonDescriptor, exposed via SubscriptionConfig. - Enforced in SubscriptionInfo.validateTopicConfig (create + alter admission), with an en/zh ManagerMessages constant. Deserialize stays lenient (>0) so already-persisted topics still load if the floor is later raised. - IT framework: setSubscriptionOwnerLeaseDurationMsMin on the CommonConfig interface + Mpp/MppShared/Remote impls; the two owner ITs lower the floor in setUp to keep their short leases (and stay fast). Verified the local IT passes (tests=2, failures=0) end-to-end. --- .../env/cluster/config/MppCommonConfig.java | 9 ++++++ .../cluster/config/MppSharedCommonConfig.java | 8 ++++++ .../env/remote/config/RemoteCommonConfig.java | 6 ++++ .../apache/iotdb/itbase/env/CommonConfig.java | 2 ++ ...oTDBSubscriptionTopicOwnerPartitionIT.java | 4 ++- .../local/IoTDBSubscriptionTopicOwnerIT.java | 20 ++++++++++++- .../confignode/i18n/ManagerMessages.java | 2 ++ .../confignode/i18n/ManagerMessages.java | 2 ++ .../subscription/SubscriptionInfo.java | 16 +++++++++++ .../SubscriptionInfoTopicValidationTest.java | 28 +++++++++++++++++++ .../iotdb/commons/conf/CommonConfig.java | 14 ++++++++++ .../iotdb/commons/conf/CommonDescriptor.java | 6 ++++ .../config/SubscriptionConfig.java | 4 +++ 13 files changed, 119 insertions(+), 2 deletions(-) diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java index f32b3ec56b8da..a4fdcacafd20a 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java @@ -483,6 +483,15 @@ public CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled) { return this; } + @Override + public CommonConfig setSubscriptionOwnerLeaseDurationMsMin( + long subscriptionOwnerLeaseDurationMsMin) { + setProperty( + "subscription_owner_lease_duration_ms_min", + String.valueOf(subscriptionOwnerLeaseDurationMsMin)); + return this; + } + @Override public CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled) { setProperty("pipe_air_gap_receiver_enabled", String.valueOf(isPipeAirGapReceiverEnabled)); diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java index 28d308bf7a1d2..83e80176b8da4 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java @@ -500,6 +500,14 @@ public CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled) { return this; } + @Override + public CommonConfig setSubscriptionOwnerLeaseDurationMsMin( + long subscriptionOwnerLeaseDurationMsMin) { + dnConfig.setSubscriptionOwnerLeaseDurationMsMin(subscriptionOwnerLeaseDurationMsMin); + cnConfig.setSubscriptionOwnerLeaseDurationMsMin(subscriptionOwnerLeaseDurationMsMin); + return this; + } + @Override public CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled) { dnConfig.setPipeAirGapReceiverEnabled(isPipeAirGapReceiverEnabled); diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java index fe1c267cf20a4..e4b55cb91bb54 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java @@ -353,6 +353,12 @@ public CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled) { return this; } + @Override + public CommonConfig setSubscriptionOwnerLeaseDurationMsMin( + long subscriptionOwnerLeaseDurationMsMin) { + return this; + } + @Override public CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled) { return this; diff --git a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java index bc8626ef96634..3ee069d0ee323 100644 --- a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java +++ b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java @@ -156,6 +156,8 @@ CommonConfig setEnableAutoLeaderBalanceForIoTConsensus( CommonConfig setSubscriptionEnabled(boolean subscriptionEnabled); + CommonConfig setSubscriptionOwnerLeaseDurationMsMin(long subscriptionOwnerLeaseDurationMsMin); + CommonConfig setPipeAirGapReceiverEnabled(boolean isPipeAirGapReceiverEnabled); CommonConfig setDriverTaskExecutionTimeSliceInMs(long driverTaskExecutionTimeSliceInMs); diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java index 92f3672452e7d..31fa3fde02276 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/cluster/IoTDBSubscriptionTopicOwnerPartitionIT.java @@ -70,7 +70,9 @@ public void setUp() throws Exception { .getCommonConfig() .setSubscriptionEnabled(true) .setPipeMemoryManagementEnabled(false) - .setIsPipeEnableMemoryCheck(false); + .setIsPipeEnableMemoryCheck(false) + // Lower the owner-lease floor so the test can use a short lease and stay fast. + .setSubscriptionOwnerLeaseDurationMsMin(1000); // 3 ConfigNodes (for failover) + 3 DataNodes (for multi-DataNode data placement). EnvFactory.getEnv().initClusterEnvironment(3, 3); } diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index d6dae42515c61..85eb090b06295 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -30,9 +30,11 @@ import org.apache.iotdb.session.subscription.model.Topic; import org.apache.iotdb.session.subscription.payload.SubscriptionMessage; import org.apache.iotdb.session.subscription.payload.SubscriptionRecordHandler; +import org.apache.iotdb.subscription.it.AbstractSubscriptionIT; import org.apache.iotdb.subscription.it.IoTDBSubscriptionITConstant; import org.apache.tsfile.read.query.dataset.ResultSet; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -47,12 +49,28 @@ @RunWith(IoTDBTestRunner.class) @Category({LocalStandaloneIT.class}) -public class IoTDBSubscriptionTopicOwnerIT extends AbstractSubscriptionLocalIT { +public class IoTDBSubscriptionTopicOwnerIT extends AbstractSubscriptionIT { @Override @Before public void setUp() throws Exception { super.setUp(); + EnvFactory.getEnv() + .getConfig() + .getCommonConfig() + .setSubscriptionEnabled(true) + .setPipeMemoryManagementEnabled(false) + .setIsPipeEnableMemoryCheck(false) + // Lower the owner-lease floor so the drain test can use a short lease and stay fast. + .setSubscriptionOwnerLeaseDurationMsMin(1000); + EnvFactory.getEnv().initClusterEnvironment(); + } + + @Override + @After + public void tearDown() throws Exception { + EnvFactory.getEnv().cleanClusterEnvironment(); + super.tearDown(); } @Test diff --git a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java index 27d00d0b2ca0a..0fa40c2aad368 100644 --- a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java +++ b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ManagerMessages.java @@ -138,6 +138,8 @@ public final class ManagerMessages { "Failed to alter topic %s, the topic is not existed"; public static final String FAILED_TO_ALTER_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS = "Failed to alter topic {} with attributes {}. Result status: {}."; + public static final String OWNER_LEASE_DURATION_BELOW_MIN = + "Failed to create or alter topic, owner-lease-duration-ms %s is below the minimum allowed %s ms."; public static final String FAILED_TO_DEEP_COPY_PIPEMETA = "failed to deep copy pipeMeta"; public static final String FAILED_TO_DEREGISTER_PIPE_CONFIG_REGION_CONNECTOR = "Failed to deregister pipe config region connector metrics, PipeConfigNodeSubtask({}) does not exist"; diff --git a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java index b4295bc9c7d61..829925f3153a6 100644 --- a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java +++ b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ManagerMessages.java @@ -137,6 +137,8 @@ public final class ManagerMessages { "修改 topic %s 失败,该 topic 不存在"; public static final String FAILED_TO_ALTER_TOPIC_WITH_ATTRIBUTES_RESULT_STATUS = "修改 topic {} 失败,属性:{}。结果状态:{}。"; + public static final String OWNER_LEASE_DURATION_BELOW_MIN = + "创建或修改 topic 失败,owner-lease-duration-ms %s 小于允许的最小值 %s ms。"; public static final String FAILED_TO_DEEP_COPY_PIPEMETA = "深拷贝 pipeMeta 失败"; public static final String FAILED_TO_DEREGISTER_PIPE_CONFIG_REGION_CONNECTOR = "Failed to deregister pipe config region connector metrics, PipeConfigNodeSubtask({}) does not exist"; diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java index 57c1b17f1dcd8..a013b3d7f920e 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfo.java @@ -21,6 +21,7 @@ import org.apache.iotdb.common.rpc.thrift.TSStatus; import org.apache.iotdb.commons.snapshot.SnapshotProcessor; +import org.apache.iotdb.commons.subscription.config.SubscriptionConfig; import org.apache.iotdb.commons.subscription.meta.consumer.CommitProgressKeeper; import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerGroupMeta; import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerGroupMetaKeeper; @@ -40,6 +41,7 @@ import org.apache.iotdb.confignode.consensus.response.subscription.SubscriptionTableResp; import org.apache.iotdb.confignode.consensus.response.subscription.TopicTableResp; import org.apache.iotdb.confignode.i18n.ConfigNodeMessages; +import org.apache.iotdb.confignode.i18n.ManagerMessages; import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateConsumerReq; import org.apache.iotdb.confignode.rpc.thrift.TCreateTopicReq; @@ -333,6 +335,20 @@ private void validateTopicConfig(final TopicConfig topicConfig) throws Subscript validateConsensusTableColumnPattern(topicConfig); validateConsensusTopicRetentionConfig(topicConfig); + + final Long ownerLeaseDurationMs = + topicConfig.getLong(TopicConstant.OWNER_LEASE_DURATION_MS_KEY); + final long ownerLeaseDurationMsMin = + SubscriptionConfig.getInstance().getSubscriptionOwnerLeaseDurationMsMin(); + if (Objects.nonNull(ownerLeaseDurationMs) && ownerLeaseDurationMs < ownerLeaseDurationMsMin) { + final String exceptionMessage = + String.format( + ManagerMessages.OWNER_LEASE_DURATION_BELOW_MIN, + ownerLeaseDurationMs, + ownerLeaseDurationMsMin); + LOGGER.warn(exceptionMessage); + throw new SubscriptionException(exceptionMessage); + } } private void validateConsensusProtocolSupport(final TopicConfig topicConfig) diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTopicValidationTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTopicValidationTest.java index 144b1e9530da1..61eaa8a4c4c64 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTopicValidationTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTopicValidationTest.java @@ -20,6 +20,7 @@ package org.apache.iotdb.confignode.persistence.subscription; import org.apache.iotdb.commons.pipe.config.constant.SystemConstant; +import org.apache.iotdb.commons.subscription.config.SubscriptionConfig; import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan; import org.apache.iotdb.confignode.rpc.thrift.TCreateTopicReq; @@ -189,6 +190,33 @@ public void testRejectConsensusOnlyRetentionOnLiveTopic() { assertCreateRejected(subscriptionInfo, attributes, "only supported for consensus topics"); } + @Test + public void testRejectOwnerLeaseDurationBelowMin() { + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + final Map attributes = new HashMap<>(); + attributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); + attributes.put(TopicConstant.OWNER_EPOCH_KEY, "1"); + // Well below the default 1-minute floor. + attributes.put(TopicConstant.OWNER_LEASE_DURATION_MS_KEY, "5000"); + + assertCreateRejected(subscriptionInfo, attributes, "below the minimum allowed"); + } + + @Test + public void testAcceptOwnerLeaseDurationAtMin() throws Exception { + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + final Map attributes = new HashMap<>(); + attributes.put(TopicConstant.OWNER_ID_KEY, "owner1"); + attributes.put(TopicConstant.OWNER_EPOCH_KEY, "1"); + attributes.put( + TopicConstant.OWNER_LEASE_DURATION_MS_KEY, + String.valueOf(SubscriptionConfig.getInstance().getSubscriptionOwnerLeaseDurationMsMin())); + + Assert.assertTrue( + subscriptionInfo.validateBeforeCreatingTopic( + new TCreateTopicReq("owner_topic").setTopicAttributes(attributes))); + } + private static Map newConsensusTableTopicAttributes() { final Map attributes = new HashMap<>(); attributes.put(SystemConstant.SQL_DIALECT_KEY, SystemConstant.SQL_DIALECT_TABLE_VALUE); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java index 252d00b5a4fca..40d801f74e1f5 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java @@ -399,6 +399,12 @@ public class CommonConfig { private long subscriptionMetaSyncerInitialSyncDelayMinutes = 3; private long subscriptionMetaSyncerSyncIntervalMinutes = 3; + // Minimum allowed owner-lease-duration-ms accepted when creating/altering a topic owner. The + // lease is renewed by an independent ~5s heartbeat, so the duration must stay well above the + // heartbeat interval (times the tolerated misses plus propagation) to avoid falsely fencing a + // healthy owner; this floor enforces that invariant at admission time. Default: 1 minute. + private long subscriptionOwnerLeaseDurationMsMin = 60_000L; + private int subscriptionConsensusBatchMaxDelayInMs = 50; private long subscriptionConsensusBatchMaxSizeInBytes = 8 * MB; private int subscriptionConsensusBatchMaxTabletCount = 64; @@ -2759,6 +2765,14 @@ public void setSubscriptionMetaSyncerSyncIntervalMinutes( this.subscriptionMetaSyncerSyncIntervalMinutes = subscriptionMetaSyncerSyncIntervalMinutes; } + public long getSubscriptionOwnerLeaseDurationMsMin() { + return subscriptionOwnerLeaseDurationMsMin; + } + + public void setSubscriptionOwnerLeaseDurationMsMin(long subscriptionOwnerLeaseDurationMsMin) { + this.subscriptionOwnerLeaseDurationMsMin = subscriptionOwnerLeaseDurationMsMin; + } + public String getSchemaEngineMode() { return schemaEngineMode; } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java index fa853f61f47a8..964bc60b823d0 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java @@ -455,6 +455,12 @@ private void loadSubscriptionProps(TrimProperties properties) { "subscription_meta_syncer_sync_interval_minutes", String.valueOf(config.getSubscriptionMetaSyncerSyncIntervalMinutes())))); + config.setSubscriptionOwnerLeaseDurationMsMin( + Long.parseLong( + properties.getProperty( + "subscription_owner_lease_duration_ms_min", + String.valueOf(config.getSubscriptionOwnerLeaseDurationMsMin())))); + config.setSubscriptionConsensusBatchMaxDelayInMs( Integer.parseInt( properties.getProperty( diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java index 2fad6cb88da7c..156397e7c1d46 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/config/SubscriptionConfig.java @@ -142,6 +142,10 @@ public long getSubscriptionMetaSyncerSyncIntervalMinutes() { return COMMON_CONFIG.getSubscriptionMetaSyncerSyncIntervalMinutes(); } + public long getSubscriptionOwnerLeaseDurationMsMin() { + return COMMON_CONFIG.getSubscriptionOwnerLeaseDurationMsMin(); + } + // Consensus subscription batching parameters public int getSubscriptionConsensusBatchMaxDelayInMs() { return COMMON_CONFIG.getSubscriptionConsensusBatchMaxDelayInMs(); From 55b95686c6ca9bc3bcc187b283f27b525e842157 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 12 Jun 2026 04:04:52 +0000 Subject: [PATCH 14/14] Clarify owner-lease drain wait depends on the heartbeat interval too Address review feedback: the ConfigNode admission wait is (lease duration + one renewal/heartbeat interval) measured on its own clock, not the lease duration alone, since a renewal may have been in flight to a DataNode just before renewal was stopped. Updated the drain test comment to reflect this; the >= lease-duration lower bound still holds because the actual wait is duration + one interval. --- .../it/local/IoTDBSubscriptionTopicOwnerIT.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java index 85eb090b06295..c08f151c16f49 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionTopicOwnerIT.java @@ -162,10 +162,15 @@ public void testAlterTopicOwnerWaitsForLeaseDrainBeforeInstallingNewOwner() thro try { // Transferring to a different owner must wait for the old owner's lease to drain on every - // DataNode before the new owner is installed. ConfigNode stops renewing and waits at least - // the lease duration (measured on its own clock), so the call blocks for >= the lease - // duration. This is the admission gate that prevents cross-DataNode double-active - // consuming. + // DataNode before the new owner is installed. ConfigNode stops renewing and then waits, on + // its own clock, for the lease duration plus one renewal (heartbeat) interval. The extra + // heartbeat interval is necessary because a renewal may have been in flight to a DataNode + // right before renewal was stopped, extending that DataNode's lease by up to the full + // duration starting as late as ~one heartbeat after the block; waiting (duration + one + // interval) on the local clock — rather than comparing against a DataNode's absolute + // expire time, which would couple the two clocks — guarantees every lease has drained. The + // call therefore blocks for at least the lease duration. This is the admission gate that + // prevents cross-DataNode double-active consuming. final long alterStartTimeMs = System.currentTimeMillis(); session.alterTopicOwner(topicName, "owner2", 6L); final long alterElapsedTimeMs = System.currentTimeMillis() - alterStartTimeMs;