This is an automated email from the ASF dual-hosted git repository. Caideyipi pushed a commit to branch feature/initial-retain-progress-after-unsubscribe in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit f3d9020835b0145d013ba0e76967389e40064ea7 Author: Caideyipi <[email protected]> AuthorDate: Mon Oct 26 17:58:54 2026 +0800 Support progress retention for initial subscription topics --- .../it/local/IoTDBSubscriptionBasicIT.java | 134 +++++++++++ .../iotdb/confignode/i18n/ConfigNodeMessages.java | 4 + .../iotdb/confignode/i18n/ConfigNodeMessages.java | 4 + .../persistence/subscription/SubscriptionInfo.java | 103 ++++++-- .../consumer/AlterConsumerGroupProcedure.java | 3 + .../subscription/CreateSubscriptionProcedure.java | 25 +- .../subscription/DropSubscriptionProcedure.java | 5 + .../subscription/topic/AlterTopicProcedure.java | 240 +++++++++++++++--- .../subscription/topic/DropTopicProcedure.java | 267 +++++++++++++++++++-- .../procedure/store/ProcedureFactory.java | 21 +- .../confignode/procedure/store/ProcedureType.java | 3 + .../SubscriptionInfoTopicValidationTest.java | 37 ++- .../consumer/DropConsumerProcedureTest.java | 61 +++++ .../CreateSubscriptionProcedureTest.java | 47 ++++ .../DropSubscriptionProcedureTest.java | 34 +++ .../topic/AlterTopicProcedureTest.java | 7 + .../subscription/topic/DropTopicProcedureTest.java | 244 +++++++++++++++++++ .../agent/SubscriptionBrokerAgent.java | 8 + .../agent/SubscriptionConsumerAgent.java | 55 ++++- .../db/subscription/broker/SubscriptionBroker.java | 17 +- .../agent/SubscriptionConsumerAgentTest.java | 89 +++++++ .../meta/consumer/CommitProgressKeeper.java | 14 ++ .../meta/consumer/ConsumerGroupMeta.java | 53 +++- .../consumer/ConsumerGroupDeSerTest.java | 35 +++ 24 files changed, 1415 insertions(+), 95 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionBasicIT.java b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionBasicIT.java index bed9721f3aa..18e0fb296be 100644 --- a/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionBasicIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/subscription/it/local/IoTDBSubscriptionBasicIT.java @@ -54,6 +54,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; @@ -63,6 +65,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.LockSupport; import java.util.stream.Collectors; +import java.util.stream.LongStream; import static org.apache.iotdb.subscription.it.IoTDBSubscriptionITConstant.AWAIT; import static org.junit.Assert.fail; @@ -430,6 +433,137 @@ public class IoTDBSubscriptionBasicIT extends AbstractSubscriptionLocalIT { } } + @Test + public void testInitialTopicRetainsProgressAfterUnsubscribe() throws Exception { + final String topicName = "topic_initial_retained_progress"; + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + session.executeNonQueryStatement("create database root.retain_progress"); + for (int i = 0; i < 20; ++i) { + session.executeNonQueryStatement( + String.format("insert into root.retain_progress.d1(time, s1) values (%s, 1)", i)); + } + session.executeNonQueryStatement("flush"); + } + + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Properties properties = new Properties(); + properties.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + properties.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, true); + session.createTopic(topicName, properties); + } + + try (final SubscriptionTreePullConsumer consumer = + new SubscriptionTreePullConsumer.Builder() + .host(host) + .port(port) + .consumerId("consumer_retained_progress") + .consumerGroupId("group_retained_progress") + .autoCommit(false) + .buildPullConsumer()) { + consumer.open(); + consumer.subscribe(topicName); + + Assert.assertEquals( + LongStream.range(0, 20).boxed().collect(Collectors.toList()), + pollTimestamps(consumer, 20, true)); + + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + session.executeNonQueryStatement( + "insert into root.retain_progress.d1(time, s1) values (20, 1)"); + session.executeNonQueryStatement("flush"); + } + + // Leave this delivery uncommitted. The server must requeue it when the last consumer leaves. + Assert.assertEquals(Collections.singletonList(20L), pollTimestamps(consumer, 1, false)); + + consumer.unsubscribe(topicName); + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + session.executeNonQueryStatement( + "insert into root.retain_progress.d1(time, s1) values (21, 1)"); + session.executeNonQueryStatement("flush"); + } + + consumer.subscribe(topicName); + Assert.assertEquals(Arrays.asList(20L, 21L), pollTimestamps(consumer, 2, true)); + consumer.unsubscribe(topicName); + } + } + + @Test + public void testInitialTopicDoesNotRetainProgressByDefault() throws Exception { + final String topicName = "topic_initial_unretained_progress"; + final String host = EnvFactory.getEnv().getIP(); + final int port = Integer.parseInt(EnvFactory.getEnv().getPort()); + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + session.executeNonQueryStatement("create database root.unretained_progress"); + for (int i = 0; i < 3; ++i) { + session.executeNonQueryStatement( + String.format("insert into root.unretained_progress.d1(time, s1) values (%s, 1)", i)); + } + session.executeNonQueryStatement("flush"); + } + + try (final SubscriptionTreeSession session = new SubscriptionTreeSession(host, port)) { + session.open(); + final Properties properties = new Properties(); + properties.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + session.createTopic(topicName, properties); + } + + try (final SubscriptionTreePullConsumer consumer = + new SubscriptionTreePullConsumer.Builder() + .host(host) + .port(port) + .consumerId("consumer_unretained_progress") + .consumerGroupId("group_unretained_progress") + .autoCommit(false) + .buildPullConsumer()) { + consumer.open(); + consumer.subscribe(topicName); + Assert.assertEquals(Arrays.asList(0L, 1L, 2L), pollTimestamps(consumer, 3, true)); + consumer.unsubscribe(topicName); + + try (final ISession session = EnvFactory.getEnv().getSessionConnection()) { + session.executeNonQueryStatement( + "insert into root.unretained_progress.d1(time, s1) values (3, 1)"); + session.executeNonQueryStatement("flush"); + } + + consumer.subscribe(topicName); + Assert.assertEquals(Arrays.asList(0L, 1L, 2L, 3L), pollTimestamps(consumer, 4, true)); + consumer.unsubscribe(topicName); + } + } + + private static List<Long> pollTimestamps( + final SubscriptionTreePullConsumer consumer, final int expectedRows, final boolean commit) { + final List<Long> timestamps = new ArrayList<>(); + AWAIT.untilAsserted( + () -> { + final List<SubscriptionMessage> messages = consumer.poll(Duration.ofSeconds(1)); + for (final SubscriptionMessage message : messages) { + for (final ResultSet dataSet : message.getResultSets()) { + try (final ResultSet result = dataSet) { + final SubscriptionRecordHandler.SubscriptionResultSet records = + (SubscriptionRecordHandler.SubscriptionResultSet) result; + while (records.hasNext()) { + timestamps.add(records.nextRecord().getTimestamp()); + } + } + } + } + if (commit) { + consumer.commitSync(messages); + } + Assert.assertEquals(expectedRows, timestamps.size()); + }); + Collections.sort(timestamps); + return timestamps; + } + @Ignore @Test public void testTsFileDeduplication() { diff --git a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java index cfde56c6419..72d6493104f 100644 --- a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java +++ b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java @@ -20,6 +20,10 @@ package org.apache.iotdb.confignode.i18n; public final class ConfigNodeMessages { + public static final String + EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ARG_IS_NOT_SUPPORTED_FOR_SNAPSHOT_TOPICS_894FDD40 = + "Failed to create or alter topic, %s is not supported for snapshot topics"; + public static final String EXCEPTION_CONFIGNODE_SIMPLECONSENSUS_WRITES_ARE_DISABLED_AFTER_A_WAL_CLEANUP_FAILURE_RESTART_IS_REQUIRED_D52F0107 = "ConfigNode SimpleConsensus writes are disabled after a WAL cleanup failure; restart is required."; public static final String EXCEPTION_FAILED_TO_CLOSE_THE_CONFIGNODE_SIMPLECONSENSUS_LOG_WRITER_BC0F417C = diff --git a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java index ddf8be67cef..865fd6ac795 100644 --- a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java +++ b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java @@ -20,6 +20,10 @@ package org.apache.iotdb.confignode.i18n; public final class ConfigNodeMessages { + public static final String + EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ARG_IS_NOT_SUPPORTED_FOR_SNAPSHOT_TOPICS_894FDD40 = + "创建或修改主题失败,快照模式不支持 %s"; + public static final String EXCEPTION_CONFIGNODE_SIMPLECONSENSUS_WRITES_ARE_DISABLED_AFTER_A_WAL_CLEANUP_FAILURE_RESTART_IS_REQUIRED_D52F0107 = "ConfigNode SimpleConsensus 因 WAL 清理失败已禁用写入,需要重启。"; public static final String EXCEPTION_FAILED_TO_CLOSE_THE_CONFIGNODE_SIMPLECONSENSUS_LOG_WRITER_BC0F417C = 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 79a735ded7f..2ce4781a7e6 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 @@ -411,6 +411,7 @@ public class SubscriptionInfo implements SnapshotProcessor { validateColumnFilter(topicConfig); validateTagFilter(topicConfig); validateIncrementalTopicRetentionConfig(topicConfig); + validateRetainProgressAfterUnsubscribe(topicConfig); validateRecycleUncommittedEventInterval(topicConfig); final Long ownerLeaseDurationMs = @@ -654,9 +655,7 @@ public class SubscriptionInfo implements SnapshotProcessor { private void validateIncrementalTopicRetentionConfig(final TopicConfig topicConfig) throws SubscriptionException { if (!hasTopicAttributeIgnoreCase(topicConfig, TopicConstant.RETENTION_BYTES_KEY) - && !hasTopicAttributeIgnoreCase(topicConfig, TopicConstant.RETENTION_MS_KEY) - && !hasTopicAttributeIgnoreCase( - topicConfig, TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY)) { + && !hasTopicAttributeIgnoreCase(topicConfig, TopicConstant.RETENTION_MS_KEY)) { return; } @@ -665,17 +664,13 @@ public class SubscriptionInfo implements SnapshotProcessor { String.format( ConfigNodeMessages .EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ATTRIBUTES_ARG_ARE_ONLY_SUPPORTED_FOR_INCREMENTAL_TOPICS_55CB0BF2, - List.of( - TopicConstant.RETENTION_BYTES_KEY, - TopicConstant.RETENTION_MS_KEY, - TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY)); + List.of(TopicConstant.RETENTION_BYTES_KEY, TopicConstant.RETENTION_MS_KEY)); LOGGER.warn(exceptionMessage); throw new SubscriptionException(exceptionMessage); } validateRetentionValue(topicConfig, TopicConstant.RETENTION_BYTES_KEY); validateRetentionValue(topicConfig, TopicConstant.RETENTION_MS_KEY); - validateRetainProgressAfterUnsubscribe(topicConfig); } private void validateRetainProgressAfterUnsubscribe(final TopicConfig topicConfig) @@ -684,6 +679,15 @@ public class SubscriptionInfo implements SnapshotProcessor { topicConfig, TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY)) { return; } + if (topicConfig.isSnapshotMode()) { + final String exceptionMessage = + String.format( + ConfigNodeMessages + .EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ARG_IS_NOT_SUPPORTED_FOR_SNAPSHOT_TOPICS_894FDD40, + TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY); + LOGGER.warn(exceptionMessage); + throw new SubscriptionException(exceptionMessage); + } final String value = getTopicAttributeIgnoreCase( topicConfig, TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY); @@ -958,12 +962,21 @@ public class SubscriptionInfo implements SnapshotProcessor { if (Objects.nonNull(oldTopicMeta) && oldTopicMeta.getConfig().isProgressRetainedAfterUnsubscribe() - && !plan.getTopicMeta().getConfig().isProgressRetainedAfterUnsubscribe()) { + && (!plan.getTopicMeta().getConfig().isProgressRetainedAfterUnsubscribe() + || (oldTopicMeta.getConfig().isInitialMode() + && !plan.getTopicMeta().getConfig().isInitialMode()))) { for (final ConsumerGroupMeta consumerGroupMeta : consumerGroupMetaKeeper.getAllConsumerGroupMeta()) { if (consumerGroupMeta.visibleUnder(isTableModel) && !consumerGroupMeta.isTopicSubscribedByConsumerGroup( plan.getTopicMeta().getTopicName())) { + if (oldTopicMeta.getConfig().isInitialMode() + && consumerGroupMeta + .getRetainedInitialTopicNames() + .contains(plan.getTopicMeta().getTopicName())) { + // The AlterTopicProcedure drops the retained Pipe before clearing this marker. + continue; + } commitProgressKeeper.removeTopicProgress( consumerGroupMeta.getConsumerGroupId(), plan.getTopicMeta().getTopicName()); removeEmptyConsumerGroupWithoutProgress(consumerGroupMeta); @@ -1004,25 +1017,52 @@ public class SubscriptionInfo implements SnapshotProcessor { public TSStatus dropTopic(DropTopicPlan plan) { acquireWriteLock(); try { + final TopicMeta topicMeta = + plan.isTableModelSet() + ? topicMetaKeeper.getTopicMeta(plan.getTopicName(), plan.isTableModel()) + : topicMetaKeeper.getTopicMeta(plan.getTopicName()); + final boolean isTableModel = + plan.isTableModelSet() + ? plan.isTableModel() + : Objects.nonNull(topicMeta) && topicMeta.visibleUnderTableModel(); + final boolean isModelScoped = plan.isTableModelSet() || Objects.nonNull(topicMeta); for (final ConsumerGroupMeta consumerGroupMeta : consumerGroupMetaKeeper.getAllConsumerGroupMeta()) { - if (!plan.isTableModelSet() || consumerGroupMeta.visibleUnder(plan.isTableModel())) { + if (!isModelScoped || consumerGroupMeta.visibleUnder(isTableModel)) { + if (isModelScoped + && consumerGroupMeta.getRetainedInitialTopicNames().contains(plan.getTopicName())) { + // The DropTopicProcedure drops the retained Pipe before clearing this marker. + continue; + } commitProgressKeeper.removeTopicProgress( consumerGroupMeta.getConsumerGroupId(), plan.getTopicName()); removeEmptyConsumerGroupWithoutProgress(consumerGroupMeta); } } - if (plan.isTableModelSet()) { - topicMetaKeeper.removeTopicMeta(plan.getTopicName(), plan.isTableModel()); - } else { - topicMetaKeeper.removeTopicMeta(plan.getTopicName()); - } + topicMetaKeeper.removeTopicMeta(plan.getTopicName(), isTableModel); return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); } finally { releaseWriteLock(); } } + public List<ConsumerGroupMeta> getConsumerGroupsRetainingInitialTopic( + final String topicName, final boolean isTableModel) { + acquireReadLock(); + try { + final List<ConsumerGroupMeta> consumerGroupMetas = new ArrayList<>(); + for (final ConsumerGroupMeta meta : consumerGroupMetaKeeper.getAllConsumerGroupMeta()) { + if (meta.visibleUnder(isTableModel) + && meta.getRetainedInitialTopicNames().contains(topicName)) { + consumerGroupMetas.add(meta.deepCopy()); + } + } + return consumerGroupMetas; + } finally { + releaseReadLock(); + } + } + public TSStatus handleTopicMetaChanges(TopicHandleMetaChangePlan plan) { acquireWriteLock(); try { @@ -1185,6 +1225,12 @@ public class SubscriptionInfo implements SnapshotProcessor { ? consumerGroupMetaKeeper.getConsumerGroupMeta(consumerGroupId) : null; if (Objects.nonNull(currentConsumerGroupMeta)) { + final Set<String> removedRetainedTopics = + new HashSet<>(currentConsumerGroupMeta.getRetainedInitialTopicNames()); + removedRetainedTopics.removeAll(consumerGroupMeta.getRetainedInitialTopicNames()); + removedRetainedTopics.removeAll(consumerGroupMeta.getSubscribedTopicNames()); + removedRetainedTopics.forEach( + topicName -> commitProgressKeeper.removeTopicProgress(consumerGroupId, topicName)); ConsumerGroupMeta.getTopicsUnsubByGroup(currentConsumerGroupMeta, consumerGroupMeta) .forEach( topicName -> { @@ -1196,7 +1242,7 @@ public class SubscriptionInfo implements SnapshotProcessor { commitProgressKeeper.removeTopicProgress(consumerGroupId, topicName); } }); - if (consumerGroupMeta.isEmpty()) { + if (consumerGroupMeta.isEmpty() && !currentConsumerGroupMeta.isEmpty()) { final Set<String> retainedTopicNames = new HashSet<>(); for (final TopicMeta topicMeta : topicMetaKeeper.getAllTopicMeta()) { if (topicMeta.visibleUnderTableModel() == currentConsumerGroupMeta.visibleUnder(true) @@ -1210,6 +1256,7 @@ public class SubscriptionInfo implements SnapshotProcessor { } consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId); if (!consumerGroupMeta.isEmpty() + || !consumerGroupMeta.getRetainedInitialTopicNames().isEmpty() || commitProgressKeeper.hasConsumerGroupProgress(consumerGroupId)) { consumerGroupMetaKeeper.addConsumerGroupMeta(consumerGroupId, consumerGroupMeta); } @@ -1222,11 +1269,35 @@ public class SubscriptionInfo implements SnapshotProcessor { private void removeEmptyConsumerGroupWithoutProgress(final ConsumerGroupMeta consumerGroupMeta) { if (consumerGroupMeta.isEmpty() + && consumerGroupMeta.getRetainedInitialTopicNames().isEmpty() && !commitProgressKeeper.hasConsumerGroupProgress(consumerGroupMeta.getConsumerGroupId())) { consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupMeta.getConsumerGroupId()); } } + public void retainInitialSubscriptionProgress( + final ConsumerGroupMeta existingMeta, final ConsumerGroupMeta updatedMeta) { + if (Objects.isNull(existingMeta)) { + return; + } + acquireReadLock(); + try { + for (final String topicName : + ConsumerGroupMeta.getTopicsUnsubByGroup(existingMeta, updatedMeta)) { + final TopicMeta topicMeta = + topicMetaKeeper.getTopicMeta(topicName, existingMeta.visibleUnder(true)); + if (Objects.nonNull(topicMeta) + && topicMeta.getConfig().isInitialMode() + && topicMeta.getConfig().isProgressRetainedAfterUnsubscribe()) { + updatedMeta.retainInitialTopic(topicName); + } + } + updatedMeta.getSubscribedTopicNames().forEach(updatedMeta::removeRetainedInitialTopic); + } finally { + releaseReadLock(); + } + } + public TSStatus handleConsumerGroupMetaChanges(ConsumerGroupHandleMetaChangePlan plan) { acquireWriteLock(); try { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/AlterConsumerGroupProcedure.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/AlterConsumerGroupProcedure.java index faf9cce8a68..0f6e5924e57 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/AlterConsumerGroupProcedure.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/AlterConsumerGroupProcedure.java @@ -81,6 +81,9 @@ public class AlterConsumerGroupProcedure extends AbstractOperateSubscriptionProc LOGGER.info(ProcedureMessages.ALTERCONSUMERGROUPPROCEDURE_EXECUTEFROMVALIDATE_TRY_TO_VALIDATE); validateAndGetOldAndNewMeta(env); + subscriptionInfo + .get() + .retainInitialSubscriptionProgress(existingConsumerGroupMeta, updatedConsumerGroupMeta); return true; } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedure.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedure.java index 04e29edbb94..f8a19838144 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedure.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedure.java @@ -135,10 +135,15 @@ public class CreateSubscriptionProcedure extends AbstractOperateSubscriptionAndP final String pipeName = PipeStaticMeta.generateSubscriptionPipeName(topicName, consumerGroupId); - if (!subscriptionInfo.get().isTopicSubscribedByConsumerGroup(topicName, consumerGroupId) - // even if there existed subscription meta, if there is no corresponding pipe meta, it - // will try to create the pipe - || !pipeTaskInfo.get().isPipeExisted(pipeName, topicMeta.visibleUnderTableModel())) { + final boolean pipeExists = + pipeTaskInfo.get().isPipeExisted(pipeName, topicMeta.visibleUnderTableModel()); + final boolean topicAlreadySubscribed = + consumerGroupMeta.getSubscribedTopicNames().contains(topicName); + final boolean topicProgressRetained = + consumerGroupMeta.getRetainedInitialTopicNames().contains(topicName); + // A retained initial subscription keeps the original Pipe and its sink queue alive. Reuse it + // only while the group is subscribed or has an explicit retained-progress marker. + if (!pipeExists || (!topicAlreadySubscribed && !topicProgressRetained)) { createPipeProcedures.add( new CreatePipeProcedureV2( new TCreatePipeReq() @@ -227,12 +232,12 @@ public class CreateSubscriptionProcedure extends AbstractOperateSubscriptionAndP consensusTopicNames); } - // Push pipe meta to data nodes (only for non-consensus pipe-based topics) - if (!createPipeProcedures.isEmpty()) { - final List<PipeStaticMeta> pipeStaticMetas = - createPipeProcedures.stream() - .map(CreatePipeProcedureV2::getPipeStaticMeta) - .collect(Collectors.toList()); + // Push pipe meta to data nodes (only for newly created non-consensus pipes). + final List<PipeStaticMeta> pipeStaticMetas = + createPipeProcedures.stream() + .map(CreatePipeProcedureV2::getPipeStaticMeta) + .collect(Collectors.toList()); + if (!pipeStaticMetas.isEmpty()) { final String exceptionMessage = AbstractOperatePipeProcedureV2.parsePushPipeMetaExceptionForPipe( null, pushMultiPipeMetaToDataNodes(pipeStaticMetas, env)); diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedure.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedure.java index b4c16985aef..ad6004302ea 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedure.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedure.java @@ -121,6 +121,11 @@ public class DropSubscriptionProcedure extends AbstractOperateSubscriptionAndPip continue; } + if (topicMeta.getConfig().isInitialMode() + && topicMeta.getConfig().isProgressRetainedAfterUnsubscribe()) { + continue; + } + // Topic will be subscribed by no consumers in this group dropPipeProcedures.add( new DropPipeProcedureV2( diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java index fb7ff2f6332..f034e6b9e05 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java @@ -20,14 +20,26 @@ package org.apache.iotdb.confignode.procedure.impl.subscription.topic; import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta; +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.topic.TopicMeta; +import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlan; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.CreatePipePlanV2; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.DropPipePlanV2; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.OperateMultiplePipesPlanV2; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.AlterConsumerGroupPlan; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.runtime.CommitProgressHandleMetaChangePlan; import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.AlterTopicPlan; import org.apache.iotdb.confignode.i18n.ConfigNodeMessages; import org.apache.iotdb.confignode.i18n.ProcedureMessages; import org.apache.iotdb.confignode.persistence.subscription.SubscriptionInfo; import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv; -import org.apache.iotdb.confignode.procedure.impl.subscription.AbstractOperateSubscriptionProcedure; +import org.apache.iotdb.confignode.procedure.impl.pipe.AbstractOperatePipeProcedureV2; +import org.apache.iotdb.confignode.procedure.impl.pipe.task.DropPipeProcedureV2; import org.apache.iotdb.confignode.procedure.impl.subscription.SubscriptionOperation; +import org.apache.iotdb.confignode.procedure.impl.subscription.subscription.AbstractOperateSubscriptionAndPipeProcedure; import org.apache.iotdb.confignode.procedure.store.ProcedureType; import org.apache.iotdb.consensus.exception.ConsensusException; import org.apache.iotdb.rpc.RpcUtils; @@ -41,13 +53,14 @@ import org.slf4j.LoggerFactory; import java.io.DataOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; -public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { +public class AlterTopicProcedure extends AbstractOperateSubscriptionAndPipeProcedure { private static final Logger LOGGER = LoggerFactory.getLogger(AlterTopicProcedure.class); @@ -60,6 +73,11 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { // initial TopicMeta snapshot was built. private Map<String, String> updatedTopicAttributes; + private final List<ConsumerGroupMeta> retainedConsumerGroupMetas = new ArrayList<>(); + private final List<DropPipeProcedureV2> retainedPipeProcedures = new ArrayList<>(); + private Map<String, ByteBuffer> retainedProgress = new HashMap<>(); + private boolean serializeRetainedProgressState; + public AlterTopicProcedure() { super(); } @@ -71,6 +89,13 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { } } + public AlterTopicProcedure( + final boolean shouldMergeUpdatedTopicAttributes, + final boolean serializeRetainedProgressState) { + this(shouldMergeUpdatedTopicAttributes); + this.serializeRetainedProgressState = serializeRetainedProgressState; + } + public AlterTopicProcedure(TopicMeta updatedTopicMeta) { super(); this.updatedTopicMeta = updatedTopicMeta; @@ -115,6 +140,12 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { return Objects.nonNull(updatedTopicAttributes); } + public boolean shouldSerializeRetainedProgressState() { + return serializeRetainedProgressState + || !retainedConsumerGroupMetas.isEmpty() + || !retainedPipeProcedures.isEmpty(); + } + @Override protected SubscriptionOperation getOperation() { return SubscriptionOperation.ALTER_TOPIC; @@ -124,6 +155,10 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { public boolean executeFromValidate(ConfigNodeProcedureEnv env) throws SubscriptionException { LOGGER.info(ProcedureMessages.ALTERTOPICPROCEDURE_EXECUTEFROMVALIDATE); + retainedConsumerGroupMetas.clear(); + retainedPipeProcedures.clear(); + retainedProgress.clear(); + existedTopicMeta = subscriptionInfo .get() @@ -136,23 +171,75 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { subscriptionInfo.get().validateBeforeAlteringTopic(updatedTopicMeta); + if (Objects.nonNull(existedTopicMeta) + && existedTopicMeta.getConfig().isInitialMode() + && existedTopicMeta.getConfig().isProgressRetainedAfterUnsubscribe() + && !updatedTopicMeta.getConfig().isProgressRetainedAfterUnsubscribe()) { + prepareRetainedProgressCleanup(env); + } + return true; } + private void prepareRetainedProgressCleanup(final ConfigNodeProcedureEnv env) { + final String topicName = updatedTopicMeta.getTopicName(); + final boolean isTableModel = updatedTopicMeta.visibleUnderTableModel(); + for (final ConsumerGroupMeta consumerGroupMeta : + subscriptionInfo.get().getConsumerGroupsRetainingInitialTopic(topicName, isTableModel)) { + retainedConsumerGroupMetas.add(consumerGroupMeta); + retainedProgress.putAll( + subscriptionInfo + .get() + .getCommitProgressKeeper() + .getTopicProgress(consumerGroupMeta.getConsumerGroupId(), topicName)); + final String pipeName = + PipeStaticMeta.generateSubscriptionPipeName( + topicName, consumerGroupMeta.getConsumerGroupId()); + if (pipeTaskInfo.get().isPipeExisted(pipeName, isTableModel)) { + final DropPipeProcedureV2 dropPipeProcedure = + new DropPipeProcedureV2(pipeName, isTableModel, pipeTaskInfo); + dropPipeProcedure.executeFromValidateTask(env); + dropPipeProcedure.executeFromCalculateInfoForTask(env); + retainedPipeProcedures.add(dropPipeProcedure); + } + } + } + @Override public void executeFromOperateOnConfigNodes(ConfigNodeProcedureEnv env) throws SubscriptionException { LOGGER.info( ProcedureMessages.ALTERTOPICPROCEDURE_EXECUTEFROMOPERATEONCONFIGNODES_TRY_TO_ALTER_TOPIC); - TSStatus response; + if (!retainedPipeProcedures.isEmpty()) { + final List<ConfigPhysicalPlan> dropPipePlans = + retainedPipeProcedures.stream() + .map( + procedure -> + new DropPipePlanV2( + procedure.getPipeName(), updatedTopicMeta.visibleUnderTableModel())) + .collect(java.util.stream.Collectors.toList()); + writeConsensusPlan(env, new OperateMultiplePipesPlanV2(dropPipePlans)); + } + for (final ConsumerGroupMeta consumerGroupMeta : getUpdatedRetainedConsumerGroupMetas()) { + writeConsensusPlan(env, new AlterConsumerGroupPlan(consumerGroupMeta)); + } + writeAlterTopicPlan(env, updatedTopicMeta); + } + + private void writeAlterTopicPlan(final ConfigNodeProcedureEnv env, final TopicMeta topicMeta) + throws SubscriptionException { + writeConsensusPlan(env, new AlterTopicPlan(topicMeta)); + } + + private void writeConsensusPlan(final ConfigNodeProcedureEnv env, final ConfigPhysicalPlan plan) + throws SubscriptionException { + final TSStatus response; try { - response = - env.getConfigManager().getConsensusManager().write(new AlterTopicPlan(updatedTopicMeta)); + response = env.getConfigManager().getConsensusManager().write(plan); } catch (ConsensusException e) { LOGGER.warn(ConfigNodeMessages.FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE, e); - response = - new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()).setMessage(e.getMessage()); + throw new SubscriptionException(e.getMessage()); } if (response.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { throw new SubscriptionException( @@ -164,6 +251,16 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { } } + private List<ConsumerGroupMeta> getUpdatedRetainedConsumerGroupMetas() { + final List<ConsumerGroupMeta> updatedMetas = new ArrayList<>(); + for (final ConsumerGroupMeta existingMeta : retainedConsumerGroupMetas) { + final ConsumerGroupMeta updatedMeta = existingMeta.deepCopy(); + updatedMeta.removeRetainedInitialTopic(updatedTopicMeta.getTopicName()); + updatedMetas.add(updatedMeta); + } + return updatedMetas; + } + @Override public void executeFromOperateOnDataNodes(ConfigNodeProcedureEnv env) throws SubscriptionException, IOException { @@ -171,6 +268,24 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { ProcedureMessages.ALTERTOPICPROCEDURE_EXECUTEFROMOPERATEONDATANODES, updatedTopicMeta.getTopicName()); + if (!retainedPipeProcedures.isEmpty()) { + final String exceptionMessage = + AbstractOperatePipeProcedureV2.parsePushPipeMetaExceptionForPipe( + null, dropMultiPipeOnDataNodes(retainedPipeProcedures, env)); + if (!exceptionMessage.isEmpty()) { + throw new SubscriptionException(exceptionMessage); + } + } + + for (final ConsumerGroupMeta consumerGroupMeta : getUpdatedRetainedConsumerGroupMetas()) { + final List<TSStatus> consumerGroupStatuses = + env.pushSingleConsumerGroupOnDataNode(consumerGroupMeta.serialize()); + if (RpcUtils.squashResponseStatusList(consumerGroupStatuses).getCode() + != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new SubscriptionException(consumerGroupStatuses.toString()); + } + } + final List<TSStatus> statuses = env.pushSingleTopicOnDataNode(updatedTopicMeta.serialize()); if (RpcUtils.squashResponseStatusList(statuses).getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { @@ -198,23 +313,35 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { ProcedureMessages.ALTERTOPICPROCEDURE_ROLLBACKFROMOPERATEONCONFIGNODES, updatedTopicMeta.getTopicName()); - TSStatus response; - try { - response = - env.getConfigManager().getConsensusManager().write(new AlterTopicPlan(existedTopicMeta)); - } catch (ConsensusException e) { - LOGGER.warn(ConfigNodeMessages.FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE, e); - response = - new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode()).setMessage(e.getMessage()); + writeAlterTopicPlan(env, existedTopicMeta); + if (!retainedPipeProcedures.isEmpty()) { + for (final DropPipeProcedureV2 dropPipeProcedure : retainedPipeProcedures) { + final PipeMeta pipeMeta = dropPipeProcedure.getPipeMetaToDrop(); + if (!pipeTaskInfo + .get() + .isPipeExisted( + pipeMeta.getStaticMeta().getPipeName(), + pipeMeta.getStaticMeta().visibleUnderTableModel())) { + writeConsensusPlan( + env, new CreatePipePlanV2(pipeMeta.getStaticMeta(), pipeMeta.getRuntimeMeta())); + } + } } - if (response.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - throw new SubscriptionException( - String.format( - ProcedureMessages.FAILED_TO_ROLLBACK_FROM_ALTERING_TOPIC_ON_CONFIG_NODES_BECAUSE, - updatedTopicMeta, - existedTopicMeta, - response)); + for (final ConsumerGroupMeta consumerGroupMeta : retainedConsumerGroupMetas) { + writeConsensusPlan(env, new AlterConsumerGroupPlan(consumerGroupMeta)); } + restoreRetainedProgress(env); + } + + private void restoreRetainedProgress(final ConfigNodeProcedureEnv env) + throws SubscriptionException { + if (retainedProgress.isEmpty()) { + return; + } + final Map<String, ByteBuffer> progress = + subscriptionInfo.get().getCommitProgressKeeper().getAllRegionProgress(); + progress.putAll(retainedProgress); + writeConsensusPlan(env, new CommitProgressHandleMetaChangePlan(progress)); } @Override @@ -224,6 +351,15 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { ProcedureMessages.ALTERTOPICPROCEDURE_ROLLBACKFROMOPERATEONDATANODES, updatedTopicMeta.getTopicName()); + for (final ConsumerGroupMeta consumerGroupMeta : retainedConsumerGroupMetas) { + final List<TSStatus> consumerGroupStatuses = + env.pushSingleConsumerGroupOnDataNode(consumerGroupMeta.serialize()); + if (RpcUtils.squashResponseStatusList(consumerGroupStatuses).getCode() + != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new SubscriptionException(consumerGroupStatuses.toString()); + } + } + final List<TSStatus> statuses = env.pushSingleTopicOnDataNode(existedTopicMeta.serialize()); if (RpcUtils.squashResponseStatusList(statuses).getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { @@ -235,15 +371,19 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { existedTopicMeta, statuses)); } + if (!retainedPipeProcedures.isEmpty()) { + final String exceptionMessage = + AbstractOperatePipeProcedureV2.parsePushPipeMetaExceptionForPipe( + null, AbstractOperatePipeProcedureV2.pushPipeMetaToDataNodes(env, pipeTaskInfo)); + if (!exceptionMessage.isEmpty()) { + throw new SubscriptionException(exceptionMessage); + } + } } @Override public void serialize(DataOutputStream stream) throws IOException { - stream.writeShort( - (shouldMergeUpdatedTopicAttributes() - ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE - : ProcedureType.ALTER_TOPIC_PROCEDURE) - .getTypeCode()); + stream.writeShort(getProcedureType().getTypeCode()); super.serialize(stream); ReadWriteIOUtils.write(updatedTopicMeta != null, stream); @@ -259,6 +399,28 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { if (shouldMergeUpdatedTopicAttributes()) { ReadWriteIOUtils.write(updatedTopicAttributes, stream); } + if (shouldSerializeRetainedProgressState()) { + ReadWriteIOUtils.write(retainedConsumerGroupMetas.size(), stream); + for (final ConsumerGroupMeta consumerGroupMeta : retainedConsumerGroupMetas) { + consumerGroupMeta.serialize(stream); + } + ReadWriteIOUtils.write(retainedPipeProcedures.size(), stream); + for (final DropPipeProcedureV2 pipeProcedure : retainedPipeProcedures) { + pipeProcedure.serialize(stream); + } + CommitProgressKeeper.serializeRegionProgressMapToStream(retainedProgress, stream); + } + } + + private ProcedureType getProcedureType() { + if (shouldSerializeRetainedProgressState()) { + return shouldMergeUpdatedTopicAttributes() + ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_AND_RETAINED_PROGRESS_PROCEDURE + : ProcedureType.ALTER_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE; + } + return shouldMergeUpdatedTopicAttributes() + ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE + : ProcedureType.ALTER_TOPIC_PROCEDURE; } @Override @@ -276,6 +438,20 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { if (shouldMergeUpdatedTopicAttributes()) { updatedTopicAttributes = ReadWriteIOUtils.readMap(byteBuffer); } + if (shouldSerializeRetainedProgressState()) { + int size = ReadWriteIOUtils.readInt(byteBuffer); + for (int i = 0; i < size; i++) { + retainedConsumerGroupMetas.add(ConsumerGroupMeta.deserialize(byteBuffer)); + } + size = ReadWriteIOUtils.readInt(byteBuffer); + for (int i = 0; i < size; i++) { + ReadWriteIOUtils.readShort(byteBuffer); + final DropPipeProcedureV2 pipeProcedure = new DropPipeProcedureV2(); + pipeProcedure.deserialize(byteBuffer); + retainedPipeProcedures.add(pipeProcedure); + } + retainedProgress = CommitProgressKeeper.deserializeRegionProgressFromBuffer(byteBuffer); + } } @Override @@ -292,7 +468,10 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { && getCycles() == that.getCycles() && Objects.equals(updatedTopicMeta, that.updatedTopicMeta) && Objects.equals(existedTopicMeta, that.existedTopicMeta) - && Objects.equals(updatedTopicAttributes, that.updatedTopicAttributes); + && Objects.equals(updatedTopicAttributes, that.updatedTopicAttributes) + && Objects.equals(retainedConsumerGroupMetas, that.retainedConsumerGroupMetas) + && Objects.equals(retainedPipeProcedures, that.retainedPipeProcedures) + && Objects.equals(retainedProgress, that.retainedProgress); } @Override @@ -303,6 +482,9 @@ public class AlterTopicProcedure extends AbstractOperateSubscriptionProcedure { getCycles(), updatedTopicMeta, existedTopicMeta, - updatedTopicAttributes); + updatedTopicAttributes, + retainedConsumerGroupMetas, + retainedPipeProcedures, + retainedProgress); } } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedure.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedure.java index 8a95811fce5..bee5210b1ce 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedure.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedure.java @@ -20,12 +20,26 @@ package org.apache.iotdb.confignode.procedure.impl.subscription.topic; import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta; +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.topic.TopicMeta; +import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlan; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.CreatePipePlanV2; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.DropPipePlanV2; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.OperateMultiplePipesPlanV2; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.AlterConsumerGroupPlan; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.runtime.CommitProgressHandleMetaChangePlan; +import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan; import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.DropTopicPlan; import org.apache.iotdb.confignode.i18n.ConfigNodeMessages; import org.apache.iotdb.confignode.i18n.ProcedureMessages; import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv; -import org.apache.iotdb.confignode.procedure.impl.subscription.AbstractOperateSubscriptionProcedure; +import org.apache.iotdb.confignode.procedure.impl.pipe.AbstractOperatePipeProcedureV2; +import org.apache.iotdb.confignode.procedure.impl.pipe.task.DropPipeProcedureV2; import org.apache.iotdb.confignode.procedure.impl.subscription.SubscriptionOperation; +import org.apache.iotdb.confignode.procedure.impl.subscription.subscription.AbstractOperateSubscriptionAndPipeProcedure; import org.apache.iotdb.confignode.procedure.store.ProcedureType; import org.apache.iotdb.consensus.exception.ConsensusException; import org.apache.iotdb.rpc.RpcUtils; @@ -39,16 +53,24 @@ import org.slf4j.LoggerFactory; import java.io.DataOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; -public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { +public class DropTopicProcedure extends AbstractOperateSubscriptionAndPipeProcedure { private static final Logger LOGGER = LoggerFactory.getLogger(DropTopicProcedure.class); private String topicName; private boolean isTableModel; private boolean isTableModelSet; + private TopicMeta existedTopicMeta; + private final List<ConsumerGroupMeta> retainedConsumerGroupMetas = new ArrayList<>(); + private final List<DropPipeProcedureV2> retainedPipeProcedures = new ArrayList<>(); + private Map<String, ByteBuffer> retainedProgress = new HashMap<>(); + private boolean serializeRetainedProgressState; public DropTopicProcedure() { super(); @@ -65,6 +87,17 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { this.isTableModelSet = true; } + public DropTopicProcedure(final boolean serializeRetainedProgressState) { + super(); + this.serializeRetainedProgressState = serializeRetainedProgressState; + } + + public boolean shouldSerializeRetainedProgressState() { + return serializeRetainedProgressState + || !retainedConsumerGroupMetas.isEmpty() + || !retainedPipeProcedures.isEmpty(); + } + @Override protected SubscriptionOperation getOperation() { return SubscriptionOperation.DROP_TOPIC; @@ -74,11 +107,44 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { protected boolean executeFromValidate(ConfigNodeProcedureEnv env) throws SubscriptionException { LOGGER.info(ProcedureMessages.DROPTOPICPROCEDURE_EXECUTEFROMVALIDATE, topicName); + retainedConsumerGroupMetas.clear(); + retainedPipeProcedures.clear(); + retainedProgress.clear(); + if (isTableModelSet) { subscriptionInfo.get().validateBeforeDroppingTopic(topicName, isTableModel); } else { subscriptionInfo.get().validateBeforeDroppingTopic(topicName); } + + existedTopicMeta = + isTableModelSet + ? subscriptionInfo.get().deepCopyTopicMeta(topicName, isTableModel) + : subscriptionInfo.get().deepCopyTopicMeta(topicName); + if (Objects.nonNull(existedTopicMeta) + && existedTopicMeta.getConfig().isInitialMode() + && existedTopicMeta.getConfig().isProgressRetainedAfterUnsubscribe()) { + final boolean model = existedTopicMeta.visibleUnderTableModel(); + for (final ConsumerGroupMeta consumerGroupMeta : + subscriptionInfo.get().getConsumerGroupsRetainingInitialTopic(topicName, model)) { + retainedConsumerGroupMetas.add(consumerGroupMeta); + retainedProgress.putAll( + subscriptionInfo + .get() + .getCommitProgressKeeper() + .getTopicProgress(consumerGroupMeta.getConsumerGroupId(), topicName)); + final String pipeName = + PipeStaticMeta.generateSubscriptionPipeName( + topicName, consumerGroupMeta.getConsumerGroupId()); + if (pipeTaskInfo.get().isPipeExisted(pipeName, model)) { + final DropPipeProcedureV2 dropPipeProcedure = + new DropPipeProcedureV2(pipeName, model, pipeTaskInfo); + dropPipeProcedure.executeFromValidateTask(env); + dropPipeProcedure.executeFromCalculateInfoForTask(env); + retainedPipeProcedures.add(dropPipeProcedure); + } + } + } return true; } @@ -87,15 +153,29 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { throws SubscriptionException { LOGGER.info(ProcedureMessages.DROPTOPICPROCEDURE_EXECUTEFROMOPERATEONCONFIGNODES, topicName); + final boolean model = getTopicModel(); + writeConsensusPlan( + env, + existedTopicMeta == null && !isTableModelSet + ? new DropTopicPlan(topicName) + : new DropTopicPlan(topicName, model)); + if (!retainedPipeProcedures.isEmpty()) { + final List<ConfigPhysicalPlan> dropPipePlans = + retainedPipeProcedures.stream() + .map(proc -> new DropPipePlanV2(proc.getPipeName(), model)) + .collect(java.util.stream.Collectors.toList()); + writeConsensusPlan(env, new OperateMultiplePipesPlanV2(dropPipePlans)); + } + for (final ConsumerGroupMeta consumerGroupMeta : getUpdatedRetainedConsumerGroupMetas()) { + writeConsensusPlan(env, new AlterConsumerGroupPlan(consumerGroupMeta)); + } + } + + private void writeConsensusPlan(final ConfigNodeProcedureEnv env, final ConfigPhysicalPlan plan) + throws SubscriptionException { TSStatus response; try { - response = - env.getConfigManager() - .getConsensusManager() - .write( - isTableModelSet - ? new DropTopicPlan(topicName, isTableModel) - : new DropTopicPlan(topicName)); + response = env.getConfigManager().getConsensusManager().write(plan); } catch (ConsensusException e) { LOGGER.warn(ConfigNodeMessages.FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE, e); response = @@ -108,14 +188,46 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { } } + private List<ConsumerGroupMeta> getUpdatedRetainedConsumerGroupMetas() { + final List<ConsumerGroupMeta> updatedMetas = new ArrayList<>(); + for (final ConsumerGroupMeta existingMeta : retainedConsumerGroupMetas) { + final ConsumerGroupMeta updatedMeta = existingMeta.deepCopy(); + updatedMeta.removeRetainedInitialTopic(topicName); + updatedMetas.add(updatedMeta); + } + return updatedMetas; + } + + private boolean getTopicModel() { + return existedTopicMeta == null ? isTableModel : existedTopicMeta.visibleUnderTableModel(); + } + @Override - protected void executeFromOperateOnDataNodes(ConfigNodeProcedureEnv env) { + protected void executeFromOperateOnDataNodes(ConfigNodeProcedureEnv env) + throws SubscriptionException, IOException { LOGGER.info(ProcedureMessages.DROPTOPICPROCEDURE_EXECUTEFROMOPERATEONDATANODES, topicName); + if (!retainedPipeProcedures.isEmpty()) { + final String exceptionMessage = + AbstractOperatePipeProcedureV2.parsePushPipeMetaExceptionForPipe( + null, dropMultiPipeOnDataNodes(retainedPipeProcedures, env)); + if (!exceptionMessage.isEmpty()) { + throw new SubscriptionException(exceptionMessage); + } + } + for (final ConsumerGroupMeta consumerGroupMeta : getUpdatedRetainedConsumerGroupMetas()) { + final List<TSStatus> consumerGroupStatuses = + env.pushSingleConsumerGroupOnDataNode(consumerGroupMeta.serialize()); + if (RpcUtils.squashResponseStatusList(consumerGroupStatuses).getCode() + != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new SubscriptionException(consumerGroupStatuses.toString()); + } + } + final List<TSStatus> statuses = - isTableModelSet - ? env.dropSingleTopicOnDataNode(topicName, isTableModel) - : env.dropSingleTopicOnDataNode(topicName); + existedTopicMeta == null && !isTableModelSet + ? env.dropSingleTopicOnDataNode(topicName) + : env.dropSingleTopicOnDataNode(topicName, getTopicModel()); if (RpcUtils.squashResponseStatusList(statuses).getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { // throw exception instead of logging warn, do not rely on metadata synchronization @@ -131,22 +243,98 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { } @Override - protected void rollbackFromOperateOnConfigNodes(ConfigNodeProcedureEnv env) { + protected void rollbackFromOperateOnConfigNodes(ConfigNodeProcedureEnv env) + throws SubscriptionException { LOGGER.info(ProcedureMessages.DROPTOPICPROCEDURE_ROLLBACKFROMCREATEONCONFIGNODES, topicName); + if (Objects.nonNull(existedTopicMeta)) { + writeConsensusPlan(env, new CreateTopicPlan(existedTopicMeta)); + for (final DropPipeProcedureV2 dropPipeProcedure : retainedPipeProcedures) { + final PipeMeta pipeMeta = dropPipeProcedure.getPipeMetaToDrop(); + if (!pipeTaskInfo + .get() + .isPipeExisted( + pipeMeta.getStaticMeta().getPipeName(), + pipeMeta.getStaticMeta().visibleUnderTableModel())) { + writeConsensusPlan( + env, new CreatePipePlanV2(pipeMeta.getStaticMeta(), pipeMeta.getRuntimeMeta())); + } + } + for (final ConsumerGroupMeta consumerGroupMeta : retainedConsumerGroupMetas) { + writeConsensusPlan(env, new AlterConsumerGroupPlan(consumerGroupMeta)); + } + restoreRetainedProgress(env); + } + } + + private void restoreRetainedProgress(final ConfigNodeProcedureEnv env) + throws SubscriptionException { + if (retainedProgress.isEmpty()) { + return; + } + final Map<String, ByteBuffer> progress = + subscriptionInfo.get().getCommitProgressKeeper().getAllRegionProgress(); + progress.putAll(retainedProgress); + writeConsensusPlan(env, new CommitProgressHandleMetaChangePlan(progress)); } @Override - protected void rollbackFromOperateOnDataNodes(ConfigNodeProcedureEnv env) { + protected void rollbackFromOperateOnDataNodes(ConfigNodeProcedureEnv env) + throws SubscriptionException, IOException { LOGGER.info(ProcedureMessages.DROPTOPICPROCEDURE_ROLLBACKFROMCREATEONDATANODES, topicName); + if (Objects.nonNull(existedTopicMeta)) { + for (final ConsumerGroupMeta consumerGroupMeta : retainedConsumerGroupMetas) { + final List<TSStatus> statuses = + env.pushSingleConsumerGroupOnDataNode(consumerGroupMeta.serialize()); + if (RpcUtils.squashResponseStatusList(statuses).getCode() + != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new SubscriptionException(statuses.toString()); + } + } + final List<TSStatus> statuses = env.pushSingleTopicOnDataNode(existedTopicMeta.serialize()); + if (RpcUtils.squashResponseStatusList(statuses).getCode() + != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new SubscriptionException(statuses.toString()); + } + if (!retainedPipeProcedures.isEmpty()) { + final String exceptionMessage = + AbstractOperatePipeProcedureV2.parsePushPipeMetaExceptionForPipe( + null, AbstractOperatePipeProcedureV2.pushPipeMetaToDataNodes(env, pipeTaskInfo)); + if (!exceptionMessage.isEmpty()) { + throw new SubscriptionException(exceptionMessage); + } + } + } } @Override public void serialize(DataOutputStream stream) throws IOException { - stream.writeShort(ProcedureType.DROP_TOPIC_PROCEDURE.getTypeCode()); + stream.writeShort( + (shouldSerializeRetainedProgressState() + ? ProcedureType.DROP_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE + : ProcedureType.DROP_TOPIC_PROCEDURE) + .getTypeCode()); super.serialize(stream); ReadWriteIOUtils.write(topicName, stream); - if (isTableModelSet) { + if (shouldSerializeRetainedProgressState()) { + ReadWriteIOUtils.write(Objects.nonNull(existedTopicMeta), stream); + if (Objects.nonNull(existedTopicMeta)) { + existedTopicMeta.serialize(stream); + } + ReadWriteIOUtils.write(isTableModelSet, stream); + if (isTableModelSet) { + ReadWriteIOUtils.write(isTableModel, stream); + } + ReadWriteIOUtils.write(retainedConsumerGroupMetas.size(), stream); + for (final ConsumerGroupMeta consumerGroupMeta : retainedConsumerGroupMetas) { + consumerGroupMeta.serialize(stream); + } + ReadWriteIOUtils.write(retainedPipeProcedures.size(), stream); + for (final DropPipeProcedureV2 pipeProcedure : retainedPipeProcedures) { + pipeProcedure.serialize(stream); + } + CommitProgressKeeper.serializeRegionProgressMapToStream(retainedProgress, stream); + } else if (isTableModelSet) { ReadWriteIOUtils.write(isTableModel, stream); } } @@ -156,9 +344,31 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { super.deserialize(byteBuffer); topicName = ReadWriteIOUtils.readString(byteBuffer); - isTableModelSet = byteBuffer.hasRemaining(); - if (isTableModelSet) { - isTableModel = ReadWriteIOUtils.readBool(byteBuffer); + if (shouldSerializeRetainedProgressState()) { + if (ReadWriteIOUtils.readBool(byteBuffer)) { + existedTopicMeta = TopicMeta.deserialize(byteBuffer); + } + isTableModelSet = ReadWriteIOUtils.readBool(byteBuffer); + if (isTableModelSet) { + isTableModel = ReadWriteIOUtils.readBool(byteBuffer); + } + int size = ReadWriteIOUtils.readInt(byteBuffer); + for (int i = 0; i < size; i++) { + retainedConsumerGroupMetas.add(ConsumerGroupMeta.deserialize(byteBuffer)); + } + size = ReadWriteIOUtils.readInt(byteBuffer); + for (int i = 0; i < size; i++) { + ReadWriteIOUtils.readShort(byteBuffer); + final DropPipeProcedureV2 pipeProcedure = new DropPipeProcedureV2(); + pipeProcedure.deserialize(byteBuffer); + retainedPipeProcedures.add(pipeProcedure); + } + retainedProgress = CommitProgressKeeper.deserializeRegionProgressFromBuffer(byteBuffer); + } else { + isTableModelSet = byteBuffer.hasRemaining(); + if (isTableModelSet) { + isTableModel = ReadWriteIOUtils.readBool(byteBuffer); + } } } @@ -176,12 +386,25 @@ public class DropTopicProcedure extends AbstractOperateSubscriptionProcedure { && getCycles() == that.getCycles() && Objects.equals(topicName, that.topicName) && isTableModel == that.isTableModel - && isTableModelSet == that.isTableModelSet; + && isTableModelSet == that.isTableModelSet + && Objects.equals(existedTopicMeta, that.existedTopicMeta) + && Objects.equals(retainedConsumerGroupMetas, that.retainedConsumerGroupMetas) + && Objects.equals(retainedPipeProcedures, that.retainedPipeProcedures) + && Objects.equals(retainedProgress, that.retainedProgress); } @Override public int hashCode() { return Objects.hash( - getProcId(), getCurrentState(), getCycles(), topicName, isTableModel, isTableModelSet); + getProcId(), + getCurrentState(), + getCycles(), + topicName, + isTableModel, + isTableModelSet, + existedTopicMeta, + retainedConsumerGroupMetas, + retainedPipeProcedures, + retainedProgress); } } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java index 0f6b2fa75e1..26d6ae02408 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java @@ -382,12 +382,21 @@ public class ProcedureFactory implements IProcedureFactory { case DROP_TOPIC_PROCEDURE: procedure = new DropTopicProcedure(); break; + case DROP_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE: + procedure = new DropTopicProcedure(true); + break; case ALTER_TOPIC_PROCEDURE: procedure = new AlterTopicProcedure(); break; case ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE: procedure = new AlterTopicProcedure(true); break; + case ALTER_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE: + procedure = new AlterTopicProcedure(false, true); + break; + case ALTER_TOPIC_WITH_ATTRIBUTES_AND_RETAINED_PROGRESS_PROCEDURE: + procedure = new AlterTopicProcedure(true, true); + break; case TOPIC_META_SYNC_PROCEDURE: procedure = new TopicMetaSyncProcedure(); break; @@ -550,9 +559,17 @@ public class ProcedureFactory implements IProcedureFactory { } else if (procedure instanceof CreateTopicProcedure) { return ProcedureType.CREATE_TOPIC_PROCEDURE; } else if (procedure instanceof DropTopicProcedure) { - return ProcedureType.DROP_TOPIC_PROCEDURE; + return ((DropTopicProcedure) procedure).shouldSerializeRetainedProgressState() + ? ProcedureType.DROP_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE + : ProcedureType.DROP_TOPIC_PROCEDURE; } else if (procedure instanceof AlterTopicProcedure) { - return ((AlterTopicProcedure) procedure).shouldMergeUpdatedTopicAttributes() + final AlterTopicProcedure alterTopicProcedure = (AlterTopicProcedure) procedure; + if (alterTopicProcedure.shouldSerializeRetainedProgressState()) { + return alterTopicProcedure.shouldMergeUpdatedTopicAttributes() + ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_AND_RETAINED_PROGRESS_PROCEDURE + : ProcedureType.ALTER_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE; + } + return alterTopicProcedure.shouldMergeUpdatedTopicAttributes() ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE : ProcedureType.ALTER_TOPIC_PROCEDURE; } else if (procedure instanceof TopicMetaSyncProcedure) { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java index 7514d06bce4..3656cddc714 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java @@ -172,6 +172,9 @@ public enum ProcedureType { COMMIT_PROGRESS_SYNC_PROCEDURE((short) 1510), SUBSCRIPTION_HANDLE_LEADER_CHANGE_PROCEDURE((short) 1511), ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE((short) 1512), + ALTER_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE((short) 1513), + ALTER_TOPIC_WITH_ATTRIBUTES_AND_RETAINED_PROGRESS_PROCEDURE((short) 1514), + DROP_TOPIC_WITH_RETAINED_PROGRESS_PROCEDURE((short) 1515), /** Other */ @TestOnly 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 fd97bebe74f..caa586b35fd 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 @@ -596,21 +596,48 @@ public class SubscriptionInfoTopicValidationTest { } @Test - public void testRejectRetainingProgressOnInitialTopic() { + public void testAcceptRetainingProgressOnInitialTopic() throws Exception { + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + for (final String value : new String[] {"true", " FALSE "}) { + final Map<String, String> attributes = newInitialTableTopicAttributes(); + attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, value); + + Assert.assertTrue( + subscriptionInfo.validateBeforeCreatingTopic( + new TCreateTopicReq("initial_topic").setTopicAttributes(attributes))); + } + } + + @Test + public void testRejectRetainingProgressOnSnapshotTopic() { final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); final Map<String, String> attributes = newInitialTableTopicAttributes(); + attributes.put(TopicConstant.MODE_KEY, TopicConstant.MODE_SNAPSHOT_VALUE); attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true"); + assertCreateRejected(subscriptionInfo, attributes, "not supported for snapshot topics"); + } + + @Test + public void testInitialRetainProgressDoesNotAllowIncrementalRetentionAttributes() { + final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); + final Map<String, String> attributes = newInitialTableTopicAttributes(); + attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true"); + attributes.put(TopicConstant.RETENTION_BYTES_KEY, "1024"); + assertCreateRejected(subscriptionInfo, attributes, "only supported for incremental topics"); } @Test public void testRejectIllegalRetainProgressValue() { final SubscriptionInfo subscriptionInfo = new SubscriptionInfo(); - final Map<String, String> attributes = newIncrementalTableTopicAttributes(); - attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "yes"); - - assertCreateRejected(subscriptionInfo, attributes, "expected true or false"); + for (final String mode : + new String[] {TopicConstant.MODE_INITIAL_VALUE, TopicConstant.MODE_INCREMENTAL_VALUE}) { + final Map<String, String> attributes = newIncrementalTableTopicAttributes(); + attributes.put(TopicConstant.MODE_KEY, mode); + attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "yes"); + assertCreateRejected(subscriptionInfo, attributes, "expected true or false"); + } } @Test diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/DropConsumerProcedureTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/DropConsumerProcedureTest.java index 6d9c16a7dd6..13b9312c558 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/DropConsumerProcedureTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/consumer/DropConsumerProcedureTest.java @@ -19,23 +19,84 @@ package org.apache.iotdb.confignode.procedure.impl.subscription.consumer; +import org.apache.iotdb.commons.pipe.config.constant.SystemConstant; import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerGroupMeta; import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerMeta; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.AlterConsumerGroupPlan; +import org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan; +import org.apache.iotdb.confignode.persistence.subscription.SubscriptionInfo; +import org.apache.iotdb.confignode.procedure.impl.subscription.AbstractOperateSubscriptionProcedure; import org.apache.iotdb.confignode.procedure.store.ProcedureFactory; import org.apache.iotdb.confignode.rpc.thrift.TCloseConsumerReq; +import org.apache.iotdb.rpc.subscription.config.ConsumerConstant; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.apache.tsfile.utils.PublicBAOS; import org.junit.Test; import java.io.DataOutputStream; +import java.lang.reflect.Field; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class DropConsumerProcedureTest { + @Test + public void testClosingLastConsumerPersistsConfiguredInitialRuntimeMarker() throws Exception { + for (final boolean retain : new boolean[] {false, true}) { + for (final String dialect : new String[] {"tree", "table"}) { + final SubscriptionInfo subscriptions = new SubscriptionInfo(); + final Map<String, String> attributes = new HashMap<>(); + attributes.put(SystemConstant.SQL_DIALECT_KEY, dialect); + attributes.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, String.valueOf(retain)); + subscriptions.createTopic(new CreateTopicPlan(new TopicMeta("topic", 1L, attributes))); + final ConsumerGroupMeta group = + new ConsumerGroupMeta( + "group", + 1L, + new ConsumerMeta( + "consumer", + 1L, + Collections.singletonMap(ConsumerConstant.SQL_DIALECT_KEY, dialect))); + group.addSubscription("consumer", Collections.singleton("topic")); + subscriptions.alterConsumerGroup(new AlterConsumerGroupPlan(group)); + final DropConsumerProcedure procedure = + new DropConsumerProcedure(new TCloseConsumerReq("consumer", "group")); + final Field field = + AbstractOperateSubscriptionProcedure.class.getDeclaredField("subscriptionInfo"); + field.setAccessible(true); + field.set(procedure, new AtomicReference<>(subscriptions)); + procedure.executeFromValidate(null); + assertEquals( + retain, + procedure + .getUpdatedConsumerGroupMeta() + .getRetainedInitialTopicNames() + .contains("topic")); + assertTrue(procedure.getUpdatedConsumerGroupMeta().isEmpty()); + + final PublicBAOS bytes = new PublicBAOS(); + procedure.serialize(new DataOutputStream(bytes)); + final ByteBuffer buffer = ByteBuffer.wrap(bytes.getBuf(), 0, bytes.size()); + final DropConsumerProcedure recovered = + (DropConsumerProcedure) ProcedureFactory.getInstance().create(buffer); + assertEquals(procedure, recovered); + assertEquals(0, buffer.remaining()); + subscriptions.alterConsumerGroup( + new AlterConsumerGroupPlan(recovered.getUpdatedConsumerGroupMeta())); + assertEquals(retain, subscriptions.getConsumerGroupMeta("group") != null); + } + } + } + @Test public void serializeDeserializeTest() { PublicBAOS byteArrayOutputStream = new PublicBAOS(); diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedureTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedureTest.java index 741e3e344be..047c917e107 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedureTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/CreateSubscriptionProcedureTest.java @@ -20,6 +20,7 @@ package org.apache.iotdb.confignode.procedure.impl.subscription.subscription; import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta; import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerGroupMeta; import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerMeta; import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; @@ -40,6 +41,7 @@ import org.apache.iotdb.confignode.procedure.store.ProcedureFactory; import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq; import org.apache.iotdb.confignode.rpc.thrift.TSubscribeReq; import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.apache.iotdb.rpc.subscription.exception.SubscriptionException; import org.apache.tsfile.utils.PublicBAOS; @@ -88,6 +90,7 @@ public class CreateSubscriptionProcedureTest { new ConsumerGroupMeta( "test_consumer_group", 1, new ConsumerMeta("old_consumer", 1, consumerAttributes)); newConsumerGroupMeta.addSubscription("old_consumer", subscribeTopics); + newConsumerGroupMeta.retainInitialTopic("retained_topic"); AlterConsumerGroupProcedure alterConsumerGroupProcedure = new AlterConsumerGroupProcedure(newConsumerGroupMeta); @@ -186,6 +189,50 @@ public class CreateSubscriptionProcedureTest { Assert.assertEquals(1, proc.getCreatePipeProcedures().size()); } + @Test + public void executeFromValidateShouldReuseRetainedInitialPipe() throws Exception { + final Map<String, String> consumerAttributes = new HashMap<>(); + consumerAttributes.put("username", "user"); + consumerAttributes.put("password", "password"); + + final ConsumerGroupMeta consumerGroupMeta = + new ConsumerGroupMeta( + "test_consumer_group", 1, new ConsumerMeta("old_consumer", 1, consumerAttributes)); + consumerGroupMeta.retainInitialTopic("test_topic"); + + final Map<String, String> topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + topicAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true"); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + + final SubscriptionInfo subscriptionInfo = Mockito.mock(SubscriptionInfo.class); + Mockito.when(subscriptionInfo.getConsumerGroupMeta("test_consumer_group")) + .thenReturn(consumerGroupMeta); + Mockito.when(subscriptionInfo.deepCopyConsumerGroupMeta("test_consumer_group")) + .thenAnswer(invocation -> consumerGroupMeta.deepCopy()); + Mockito.when(subscriptionInfo.deepCopyTopicMeta("test_topic", false)).thenReturn(topicMeta); + + final PipeTaskInfo pipeTaskInfo = Mockito.mock(PipeTaskInfo.class); + Mockito.when( + pipeTaskInfo.isPipeExisted( + PipeStaticMeta.generateSubscriptionPipeName("test_topic", "test_consumer_group"), + false)) + .thenReturn(true); + + final CreateSubscriptionProcedure proc = + new CreateSubscriptionProcedure( + new TSubscribeReq( + "old_consumer", "test_consumer_group", Collections.singleton("test_topic"))); + setField(proc, "subscriptionInfo", new AtomicReference<>(subscriptionInfo)); + setField(proc, "pipeTaskInfo", new AtomicReference<>(pipeTaskInfo)); + + proc.executeFromValidate(mockCreateSubscriptionValidationEnv()); + + Assert.assertTrue(proc.getCreatePipeProcedures().isEmpty()); + Mockito.verify(pipeTaskInfo, Mockito.never()) + .checkBeforeCreatePipe(Mockito.any(TCreatePipeReq.class)); + } + @Test public void newlySubscribedConsensusTopicsShouldBeBasedOnGroupTopics() { final ConsumerMeta firstConsumer = diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedureTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedureTest.java index 1bf74f8db24..d1c64462a5d 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedureTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/subscription/DropSubscriptionProcedureTest.java @@ -33,6 +33,7 @@ import org.apache.iotdb.confignode.procedure.impl.subscription.consumer.AlterCon import org.apache.iotdb.confignode.procedure.store.ProcedureFactory; import org.apache.iotdb.confignode.rpc.thrift.TUnsubscribeReq; import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.apache.iotdb.rpc.subscription.exception.SubscriptionException; import org.apache.tsfile.utils.PublicBAOS; @@ -165,6 +166,39 @@ public class DropSubscriptionProcedureTest { Assert.assertEquals(1, proc.getDropPipeProcedures().size()); } + @Test + public void executeFromValidateShouldRetainInitialPipeWhenConfigured() throws Exception { + final ConsumerMeta consumerMeta = new ConsumerMeta("old_consumer", 1, Collections.emptyMap()); + final ConsumerGroupMeta consumerGroupMeta = + new ConsumerGroupMeta("test_consumer_group", 1, consumerMeta); + consumerGroupMeta.addSubscription("old_consumer", Collections.singleton("test_topic")); + + final Map<String, String> topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + topicAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true"); + + final SubscriptionInfo subscriptionInfo = Mockito.mock(SubscriptionInfo.class); + Mockito.when(subscriptionInfo.deepCopyConsumerGroupMeta("test_consumer_group")) + .thenAnswer(invocation -> consumerGroupMeta.deepCopy()); + Mockito.when(subscriptionInfo.getConsumerGroupMeta("test_consumer_group")) + .thenReturn(consumerGroupMeta); + Mockito.when(subscriptionInfo.deepCopyTopicMeta("test_topic", false)) + .thenReturn(new TopicMeta("test_topic", 1, topicAttributes)); + + final PipeTaskInfo pipeTaskInfo = Mockito.mock(PipeTaskInfo.class); + final DropSubscriptionProcedure procedure = + new DropSubscriptionProcedure( + new TUnsubscribeReq( + "old_consumer", "test_consumer_group", Collections.singleton("test_topic"))); + setField(procedure, "subscriptionInfo", new AtomicReference<>(subscriptionInfo)); + setField(procedure, "pipeTaskInfo", new AtomicReference<>(pipeTaskInfo)); + + procedure.executeFromValidate(Mockito.mock(ConfigNodeProcedureEnv.class)); + + Assert.assertTrue(procedure.getDropPipeProcedures().isEmpty()); + Mockito.verify(pipeTaskInfo, Mockito.never()).checkBeforeDropPipe(Mockito.anyString()); + } + private static ConfigNodeProcedureEnv mockConsensusFailureEnv(final TSStatus response) throws Exception { final ConfigNodeProcedureEnv env = Mockito.mock(ConfigNodeProcedureEnv.class); diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java index 56f16b6e14a..e15d7cd7c3c 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java @@ -62,6 +62,12 @@ public class AlterTopicProcedureTest { new TopicMeta("test_topic", 1, topicAttributes), updatedTopicAttributes)); } + @Test + public void serializeDeserializeWithRetainedProgressStateTest() throws Exception { + final AlterTopicProcedure procedure = new AlterTopicProcedure(false, true); + assertSerializeDeserialize(procedure); + } + private void assertSerializeDeserialize(final AlterTopicProcedure procedure) throws Exception { final PublicBAOS byteArrayOutputStream = new PublicBAOS(); final DataOutputStream outputStream = new DataOutputStream(byteArrayOutputStream); @@ -71,6 +77,7 @@ public class AlterTopicProcedureTest { final AlterTopicProcedure deserializedProcedure = (AlterTopicProcedure) ProcedureFactory.getInstance().create(buffer); assertEquals(procedure, deserializedProcedure); + assertEquals(0, buffer.remaining()); } @Test diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedureTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedureTest.java index 0ad614408a3..f210738a31d 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedureTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/DropTopicProcedureTest.java @@ -19,18 +19,216 @@ package org.apache.iotdb.confignode.procedure.impl.subscription.topic; +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.consensus.index.impl.SimpleProgressIndex; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeRuntimeMeta; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStatus; +import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta; +import org.apache.iotdb.commons.pipe.config.constant.SystemConstant; +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.ConsumerMeta; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlan; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.CreatePipePlanV2; +import org.apache.iotdb.confignode.consensus.request.write.pipe.task.OperateMultiplePipesPlanV2; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.AlterConsumerGroupPlan; +import org.apache.iotdb.confignode.consensus.request.write.subscription.consumer.runtime.CommitProgressHandleMetaChangePlan; +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.request.write.subscription.topic.DropTopicPlan; +import org.apache.iotdb.confignode.manager.ConfigManager; +import org.apache.iotdb.confignode.manager.consensus.ConsensusManager; +import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo; +import org.apache.iotdb.confignode.persistence.subscription.SubscriptionInfo; +import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv; import org.apache.iotdb.confignode.procedure.store.ProcedureFactory; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.rpc.subscription.config.ConsumerConstant; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; import org.apache.tsfile.utils.PublicBAOS; import org.junit.Test; +import org.mockito.Mockito; import java.io.DataOutputStream; +import java.lang.reflect.Field; import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class DropTopicProcedureTest { + + @Test + public void testRetainedRuntimeCleanupAndRollbackAreModelScoped() throws Exception { + for (final boolean dropTopic : new boolean[] {false, true}) { + for (final boolean tableModel : new boolean[] {false, true}) { + assertRetainedRuntimeCleanupAndRollback(dropTopic, tableModel); + } + } + } + + private void assertRetainedRuntimeCleanupAndRollback( + final boolean dropTopic, final boolean tableModel) throws Exception { + final String topicName = "retained_topic"; + final SubscriptionInfo subscriptions = new SubscriptionInfo(); + final PipeTaskInfo pipes = new PipeTaskInfo(); + for (final boolean model : new boolean[] {false, true}) { + final String dialect = model ? "table" : "tree"; + final Map<String, String> attributes = new HashMap<>(); + attributes.put(SystemConstant.SQL_DIALECT_KEY, dialect); + attributes.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true"); + subscriptions.createTopic(new CreateTopicPlan(new TopicMeta(topicName, 1L, attributes))); + final String groupId = "retained_group_" + model; + final ConsumerGroupMeta group = + new ConsumerGroupMeta( + groupId, + 1L, + new ConsumerMeta( + "consumer", + 1L, + Collections.singletonMap(ConsumerConstant.SQL_DIALECT_KEY, dialect))); + group.retainInitialTopic(topicName); + group.removeConsumer("consumer"); + subscriptions.alterConsumerGroup(new AlterConsumerGroupPlan(group)); + final PipeRuntimeMeta runtime = new PipeRuntimeMeta(); + runtime.getStatus().set(PipeStatus.RUNNING); + runtime + .getConsensusGroupId2TaskMetaMap() + .put(1, new PipeTaskMeta(new SimpleProgressIndex(0, 42L), 1)); + pipes.createPipe( + new CreatePipePlanV2( + new PipeStaticMeta( + PipeStaticMeta.generateSubscriptionPipeName(topicName, groupId), + 1L, + Collections.singletonMap(SystemConstant.SQL_DIALECT_KEY, dialect), + Collections.emptyMap(), + Collections.emptyMap()), + runtime)); + final String regionId = "DataRegion[1]"; + subscriptions + .getCommitProgressKeeper() + .updateRegionProgress( + CommitProgressKeeper.generateKey(groupId, topicName, regionId, 1), + ByteBuffer.wrap(new byte[] {(byte) (model ? 2 : 1)})); + subscriptions + .getCommitProgressKeeper() + .updateRegionProgress( + CommitProgressKeeper.generateKey(groupId, "other_topic", regionId, 1), + ByteBuffer.wrap(new byte[] {(byte) (model ? 4 : 3)})); + } + final ConfigNodeProcedureEnv env = Mockito.mock(ConfigNodeProcedureEnv.class); + final ConfigManager manager = Mockito.mock(ConfigManager.class); + final ConsensusManager consensus = Mockito.mock(ConsensusManager.class); + Mockito.when(env.getConfigManager()).thenReturn(manager); + Mockito.when(manager.getConsensusManager()).thenReturn(consensus); + Mockito.when(consensus.write(Mockito.any(ConfigPhysicalPlan.class))) + .thenAnswer( + invocation -> { + final ConfigPhysicalPlan plan = invocation.getArgument(0); + if (plan instanceof CreatePipePlanV2) { + return pipes.createPipe((CreatePipePlanV2) plan); + } else if (plan instanceof OperateMultiplePipesPlanV2) { + return pipes.operateMultiplePipes((OperateMultiplePipesPlanV2) plan); + } else if (plan instanceof AlterConsumerGroupPlan) { + return subscriptions.alterConsumerGroup((AlterConsumerGroupPlan) plan); + } else if (plan instanceof DropTopicPlan) { + return subscriptions.dropTopic((DropTopicPlan) plan); + } else if (plan instanceof CreateTopicPlan) { + return subscriptions.createTopic((CreateTopicPlan) plan); + } else if (plan instanceof AlterTopicPlan) { + return subscriptions.alterTopic((AlterTopicPlan) plan); + } else if (plan instanceof CommitProgressHandleMetaChangePlan) { + return subscriptions.handleCommitProgressChanges( + (CommitProgressHandleMetaChangePlan) plan); + } + return new TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()); + }); + + final String groupId = "retained_group_" + tableModel; + final String pipeName = PipeStaticMeta.generateSubscriptionPipeName(topicName, groupId); + final String progressKey = + CommitProgressKeeper.generateKey(groupId, topicName, "DataRegion[1]", 1); + final String otherProgressKey = + CommitProgressKeeper.generateKey(groupId, "other_topic", "DataRegion[1]", 1); + final ByteBuffer originalProgress = + subscriptions.getCommitProgressKeeper().getRegionProgress(progressKey); + final ByteBuffer unrelatedProgress = + subscriptions.getCommitProgressKeeper().getRegionProgress(otherProgressKey); + final PipeMeta originalPipe = pipes.getPipeMetaByPipeName(pipeName, tableModel); + final DropTopicProcedure drop = new DropTopicProcedure(topicName, tableModel); + final AlterTopicProcedure alter = + new AlterTopicProcedure( + subscriptions.deepCopyTopicMetaWithUpdatedAttributes( + topicName, + Collections.singletonMap( + TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "false"), + tableModel)); + final Object procedure = dropTopic ? drop : alter; + setField(procedure, "subscriptionInfo", new AtomicReference<>(subscriptions)); + setField(procedure, "pipeTaskInfo", new AtomicReference<>(pipes)); + if (dropTopic) { + drop.executeFromValidate(env); + } else { + alter.executeFromValidate(env); + } + final PublicBAOS bytes = new PublicBAOS(); + if (dropTopic) { + drop.serialize(new DataOutputStream(bytes)); + } else { + alter.serialize(new DataOutputStream(bytes)); + } + final ByteBuffer buffer = ByteBuffer.wrap(bytes.getBuf(), 0, bytes.size()); + final Object recovered = ProcedureFactory.getInstance().create(buffer); + assertEquals(0, buffer.remaining()); + assertEquals(procedure, recovered); + setField(recovered, "subscriptionInfo", new AtomicReference<>(subscriptions)); + setField(recovered, "pipeTaskInfo", new AtomicReference<>(pipes)); + if (dropTopic) { + ((DropTopicProcedure) recovered).executeFromOperateOnConfigNodes(env); + } else { + ((AlterTopicProcedure) recovered).executeFromOperateOnConfigNodes(env); + } + assertFalse(pipes.isPipeExisted(pipeName, tableModel)); + assertNull(subscriptions.getCommitProgressKeeper().getRegionProgress(progressKey)); + assertTrue( + subscriptions.getConsumerGroupsRetainingInitialTopic(topicName, tableModel).isEmpty()); + assertEquals( + 1, subscriptions.getConsumerGroupsRetainingInitialTopic(topicName, !tableModel).size()); + if (dropTopic) { + ((DropTopicProcedure) recovered).rollbackFromOperateOnConfigNodes(env); + } else { + ((AlterTopicProcedure) recovered).rollbackFromOperateOnConfigNodes(env); + } + assertEquals( + 1, subscriptions.getConsumerGroupsRetainingInitialTopic(topicName, tableModel).size()); + assertTrue( + subscriptions + .deepCopyTopicMeta(topicName, tableModel) + .getConfig() + .isProgressRetainedAfterUnsubscribe()); + assertEquals( + originalPipe.getRuntimeMeta(), + pipes.getPipeMetaByPipeName(pipeName, tableModel).getRuntimeMeta()); + assertEquals( + originalProgress, subscriptions.getCommitProgressKeeper().getRegionProgress(progressKey)); + assertEquals( + unrelatedProgress, + subscriptions.getCommitProgressKeeper().getRegionProgress(otherProgressKey)); + } + @Test public void serializeDeserializeTest() { PublicBAOS byteArrayOutputStream = new PublicBAOS(); @@ -68,4 +266,50 @@ public class DropTopicProcedureTest { fail(); } } + + @Test + public void serializeDeserializeRetainedProgressStateTest() throws Exception { + final Map<String, String> topicAttributes = new HashMap<>(); + topicAttributes.put(TopicConstant.MODE_KEY, TopicConstant.MODE_INITIAL_VALUE); + topicAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true"); + final TopicMeta topicMeta = new TopicMeta("test_topic", 1, topicAttributes); + final ConsumerGroupMeta consumerGroupMeta = + new ConsumerGroupMeta( + "test_group", 1, new ConsumerMeta("consumer", 1, Collections.emptyMap())); + consumerGroupMeta.retainInitialTopic("test_topic"); + + final DropTopicProcedure procedure = new DropTopicProcedure("test_topic", false); + setField(procedure, "existedTopicMeta", topicMeta); + final Field retainedConsumerGroupMetasField = + DropTopicProcedure.class.getDeclaredField("retainedConsumerGroupMetas"); + retainedConsumerGroupMetasField.setAccessible(true); + @SuppressWarnings("unchecked") + final List<ConsumerGroupMeta> retainedConsumerGroupMetas = + (List<ConsumerGroupMeta>) retainedConsumerGroupMetasField.get(procedure); + retainedConsumerGroupMetas.add(consumerGroupMeta); + final PublicBAOS byteArrayOutputStream = new PublicBAOS(); + procedure.serialize(new DataOutputStream(byteArrayOutputStream)); + final ByteBuffer buffer = + ByteBuffer.wrap(byteArrayOutputStream.getBuf(), 0, byteArrayOutputStream.size()); + final DropTopicProcedure deserializedProcedure = + (DropTopicProcedure) ProcedureFactory.getInstance().create(buffer); + + assertEquals(procedure, deserializedProcedure); + assertEquals(0, buffer.remaining()); + } + + private static void setField(final Object target, final String fieldName, final Object value) + throws Exception { + for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) { + try { + final Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + return; + } catch (final NoSuchFieldException ignored) { + // Look for inherited procedure state. + } + } + throw new NoSuchFieldException(fieldName); + } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java index 89d877665eb..cea20c85689 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionBrokerAgent.java @@ -1061,6 +1061,14 @@ public class SubscriptionBrokerAgent { prefetchingQueueCount.invalidate(); } + public void requeuePipeInFlightEvents( + final String consumerGroupId, final String topicName, final String consumerId) { + final SubscriptionBroker broker = getPipeBroker(consumerGroupId); + if (Objects.nonNull(broker)) { + broker.requeueInFlightEvents(topicName, consumerId); + } + } + public boolean executePrefetch(final String consumerGroupId, final String topicName) { final ISubscriptionBroker broker = getBrokerForTopic(consumerGroupId, topicName); if (Objects.isNull(broker)) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java index 8af488c5923..74095491e50 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgent.java @@ -100,13 +100,19 @@ public class SubscriptionConsumerAgent { if (metaFromCoordinator.isEmpty()) { if (Objects.nonNull(metaInAgent)) { - cleanupConsumerGroupSubscriptions(consumerGroupId, metaInAgent, true); + cleanupConsumerGroupSubscriptions( + consumerGroupId, metaInAgent, true, metaFromCoordinator.getRetainedInitialTopicNames()); } final boolean retainProgress = ConsensusSubscriptionSetupHandler.retainConfiguredProgressForConsumerGroup( consumerGroupId, metaFromCoordinator.visibleUnder(true)); - dropConsumerGroupBroker(consumerGroupId); - if (retainProgress) { + final boolean retainInitialProgress = + !metaFromCoordinator.getRetainedInitialTopicNames().isEmpty(); + if (!retainInitialProgress) { + dropConsumerGroupBroker(consumerGroupId); + } + if (retainProgress || retainInitialProgress) { + requeueRetainedInitialEvents(consumerGroupId, metaInAgent, metaFromCoordinator); replaceConsumerGroupMeta(consumerGroupId, metaFromCoordinator); } else { removeConsumerGroupMeta(consumerGroupId); @@ -157,6 +163,11 @@ public class SubscriptionConsumerAgent { // remove prefetching queues for topics unsubscribed by the consumer group final Set<String> topicsUnsubByGroup = ConsumerGroupMeta.getTopicsUnsubByGroup(metaInAgent, metaFromCoordinator); + final Set<String> removedRetainedTopics = + new LinkedHashSet<>(metaInAgent.getRetainedInitialTopicNames()); + removedRetainedTopics.removeAll(metaFromCoordinator.getRetainedInitialTopicNames()); + removedRetainedTopics.removeAll(metaFromCoordinator.getSubscribedTopicNames()); + topicsUnsubByGroup.addAll(removedRetainedTopics); final Set<String> pipeTopicsUnsubByGroup = new LinkedHashSet<>(); final Set<String> consensusTopicsUnsubByGroup = new LinkedHashSet<>(); for (final String topicName : topicsUnsubByGroup) { @@ -165,6 +176,9 @@ public class SubscriptionConsumerAgent { consensusTopicsUnsubByGroup.add(topicName); continue; } + if (metaFromCoordinator.getRetainedInitialTopicNames().contains(topicName)) { + continue; + } pipeTopicsUnsubByGroup.add(topicName); } // Detect newly subscribed topics (present in new meta but not in old meta) @@ -197,6 +211,7 @@ public class SubscriptionConsumerAgent { () -> { // TODO: Currently we fully replace the entire ConsumerGroupMeta without carefully // checking the changes in its fields. + requeueRetainedInitialEvents(consumerGroupId, metaInAgent, metaFromCoordinator); replaceConsumerGroupMeta(consumerGroupId, metaFromCoordinator); }); } @@ -210,6 +225,21 @@ public class SubscriptionConsumerAgent { publishMeta.run(); } + private void requeueRetainedInitialEvents( + final String consumerGroupId, + final ConsumerGroupMeta existingMeta, + final ConsumerGroupMeta updatedMeta) { + if (Objects.isNull(existingMeta)) { + return; + } + for (final String topicName : updatedMeta.getRetainedInitialTopicNames()) { + for (final String consumerId : existingMeta.getConsumersSubscribingTopic(topicName)) { + SubscriptionAgent.broker() + .requeuePipeInFlightEvents(consumerGroupId, topicName, consumerId); + } + } + } + public TPushConsumerGroupMetaRespExceptionMessage handleConsumerGroupMetaChanges( final List<ConsumerGroupMeta> consumerGroupMetasFromCoordinator) { for (final ConsumerGroupMeta consumerGroupMetaFromCoordinator : @@ -349,8 +379,25 @@ public class SubscriptionConsumerAgent { final String consumerGroupId, final ConsumerGroupMeta consumerGroupMeta, final boolean retainConfiguredProgress) { + cleanupConsumerGroupSubscriptions( + consumerGroupId, + consumerGroupMeta, + retainConfiguredProgress, + java.util.Collections.emptySet()); + } + + private void cleanupConsumerGroupSubscriptions( + final String consumerGroupId, + final ConsumerGroupMeta consumerGroupMeta, + final boolean retainConfiguredProgress, + final Set<String> retainedInitialTopicNames) { final Set<String> consensusTopics = new LinkedHashSet<>(); - for (final String topicName : consumerGroupMeta.getSubscribedTopicNames()) { + final Set<String> topicNames = new LinkedHashSet<>(consumerGroupMeta.getSubscribedTopicNames()); + topicNames.addAll(consumerGroupMeta.getRetainedInitialTopicNames()); + for (final String topicName : topicNames) { + if (retainedInitialTopicNames.contains(topicName)) { + continue; + } if (ConsensusSubscriptionSetupHandler.isConsensusBasedTopic( topicName, consumerGroupMeta.visibleUnder(true))) { consensusTopics.add(topicName); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java index 3cfb34014d7..5bccf861544 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/SubscriptionBroker.java @@ -402,6 +402,13 @@ public class SubscriptionBroker implements ISubscriptionBroker { .sum(); } + public void requeueInFlightEvents(final String topicName, final String consumerId) { + final SubscriptionPrefetchingQueue queue = topicNameToPrefetchingQueue.get(topicName); + if (Objects.nonNull(queue)) { + queue.requeueInFlightEvents(consumerId); + } + } + @Override public int refreshInFlightEventLeases( final String consumerId, final List<SubscriptionCommitContext> commitContexts) { @@ -491,9 +498,8 @@ public class SubscriptionBroker implements ISubscriptionBroker { public void updateCompletedTopicNames(final String topicName) { // mark topic name completed only for topic of snapshot mode - if (SubscriptionAgent.topic() - .getTopicMode(topicName, isTableModel()) - .equals(TopicConstant.MODE_SNAPSHOT_VALUE)) { + if (TopicConstant.MODE_SNAPSHOT_VALUE.equals( + SubscriptionAgent.topic().getTopicMode(topicName, isTableModel()))) { completedTopicNames.put(topicName, topicName); } } @@ -514,9 +520,8 @@ public class SubscriptionBroker implements ISubscriptionBroker { prefetchingQueue.markClosed(); // mark topic name completed only for topic of snapshot mode - if (SubscriptionAgent.topic() - .getTopicMode(topicName, isTableModel()) - .equals(TopicConstant.MODE_SNAPSHOT_VALUE) + if (TopicConstant.MODE_SNAPSHOT_VALUE.equals( + SubscriptionAgent.topic().getTopicMode(topicName, isTableModel())) && prefetchingQueue.isCompleted()) { completedTopicNames.put(topicName, topicName); } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java index 0d564ed0c72..100cc78d4cc 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/agent/SubscriptionConsumerAgentTest.java @@ -19,12 +19,23 @@ package org.apache.iotdb.db.subscription.agent; +import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerGroupMeta; +import org.apache.iotdb.commons.subscription.meta.consumer.ConsumerMeta; +import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta; +import org.apache.iotdb.db.subscription.broker.ISubscriptionBroker; +import org.apache.iotdb.db.subscription.broker.SubscriptionBroker; +import org.apache.iotdb.db.subscription.broker.SubscriptionPrefetchingQueue; +import org.apache.iotdb.rpc.subscription.config.TopicConstant; + import org.junit.Test; +import org.mockito.Mockito; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -33,6 +44,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -40,6 +52,83 @@ public class SubscriptionConsumerAgentTest { private static final long TEST_TIMEOUT_SECONDS = 5L; + @Test + @SuppressWarnings("unchecked") + public void testRetainedInitialQueueSurvivesUnsubscribeAndConsumerClose() throws Exception { + final SubscriptionConsumerAgent agent = SubscriptionAgent.consumer(); + final SubscriptionBrokerAgent brokers = SubscriptionAgent.broker(); + assertNull( + SubscriptionAgent.topic() + .handleSingleTopicMetaChanges( + new TopicMeta( + "topic", + 1L, + Map.of( + TopicConstant.MODE_KEY, + TopicConstant.MODE_INITIAL_VALUE, + TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, + "true")))); + try { + for (final boolean closeConsumer : new boolean[] {false, true}) { + final String groupId = "retained_initial_" + closeConsumer; + final ConsumerGroupMeta subscribed = + new ConsumerGroupMeta( + groupId, 1L, new ConsumerMeta("consumer", 1L, Collections.emptyMap())); + subscribed.addSubscription("consumer", Collections.singleton("topic")); + try { + assertNull(agent.handleSingleConsumerGroupMetaChanges(subscribed)); + final Field brokersField = + SubscriptionBrokerAgent.class.getDeclaredField("consumerGroupIdToBrokers"); + brokersField.setAccessible(true); + final Map<String, List<ISubscriptionBroker>> brokerMap = + (Map<String, List<ISubscriptionBroker>>) brokersField.get(brokers); + final SubscriptionBroker broker = (SubscriptionBroker) brokerMap.get(groupId).get(0); + final Field queuesField = + SubscriptionBroker.class.getDeclaredField("topicNameToPrefetchingQueue"); + queuesField.setAccessible(true); + final Map<String, SubscriptionPrefetchingQueue> queues = + (Map<String, SubscriptionPrefetchingQueue>) queuesField.get(broker); + final SubscriptionPrefetchingQueue queue = + Mockito.mock(SubscriptionPrefetchingQueue.class); + Mockito.when(queue.getPrefetchingQueueId()).thenReturn(groupId + "_topic"); + queues.put("topic", queue); + + final ConsumerGroupMeta detached = subscribed.deepCopy(); + detached.removeSubscription("consumer", Collections.singleton("topic")); + detached.retainInitialTopic("topic"); + if (closeConsumer) { + detached.removeConsumer("consumer"); + } + assertNull(agent.handleSingleConsumerGroupMetaChanges(detached)); + assertTrue(brokers.isBrokerExist(groupId)); + assertTrue(broker.hasQueue("topic")); + Mockito.verify(queue).requeueInFlightEvents("consumer"); + Mockito.verify(queue, Mockito.never()).cleanUp(); + + final ConsumerGroupMeta rejoined = detached.deepCopy(); + if (closeConsumer) { + rejoined.addConsumer(new ConsumerMeta("consumer", 2L, Collections.emptyMap())); + } + rejoined.addSubscription("consumer", Collections.singleton("topic")); + rejoined.removeRetainedInitialTopic("topic"); + assertNull(agent.handleSingleConsumerGroupMetaChanges(rejoined)); + assertTrue(broker.hasQueue("topic")); + Mockito.verify(queue, Mockito.never()).cleanUp(); + + final ConsumerGroupMeta cleared = detached.deepCopy(); + cleared.removeRetainedInitialTopic("topic"); + assertNull(agent.handleSingleConsumerGroupMetaChanges(cleared)); + assertFalse(broker.hasQueue("topic")); + Mockito.verify(queue).cleanUp(); + } finally { + assertNull(agent.handleDropConsumerGroup(groupId)); + } + } + } finally { + assertNull(SubscriptionAgent.topic().handleDropTopic("topic", false)); + } + } + @Test public void testTopicDiffSetsUpBeforeTeardownAndMetaPublication() { final List<String> operations = new ArrayList<>(); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeper.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeper.java index 934ef58dd20..e36c4a0633e 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeper.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeper.java @@ -119,6 +119,20 @@ public class CommitProgressKeeper { .anyMatch(prefix -> isValidRegionAndDataNodeProgressKey(key, prefix))); } + public synchronized Map<String, ByteBuffer> getTopicProgress( + final String consumerGroupId, final String topicName) { + final Set<String> prefixes = getTopicProgressKeyPrefixes(consumerGroupId, topicName); + final Map<String, ByteBuffer> result = new HashMap<>(); + regionProgressMap.forEach( + (key, value) -> { + if (prefixes.stream() + .anyMatch(prefix -> isValidRegionAndDataNodeProgressKey(key, prefix))) { + result.put(key, copyBuffer(value)); + } + }); + return result; + } + private static Set<String> getTopicProgressKeyPrefixes( final String consumerGroupId, final String topicName) { final Set<String> prefixes = new HashSet<>(); diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java index a129c5ae70e..7a29d30fc6f 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java @@ -54,6 +54,7 @@ public class ConsumerGroupMeta { private Map<String, ConsumerMeta> consumerIdToConsumerMeta; private boolean tableModel; private Map<String, Long> topicNameToSubscriptionCreationTime; // used when creationTime < 0 + private final Set<String> retainedInitialTopicNames = ConcurrentHashMap.newKeySet(); public ConsumerGroupMeta() { this.topicNameToSubscribedConsumerIdSet = new ConcurrentHashMap<>(); @@ -85,6 +86,7 @@ public class ConsumerGroupMeta { copied.tableModel = tableModel; copied.topicNameToSubscriptionCreationTime = new ConcurrentHashMap<>(topicNameToSubscriptionCreationTime); + copied.retainedInitialTopicNames.addAll(retainedInitialTopicNames); return copied; } @@ -218,6 +220,18 @@ public class ConsumerGroupMeta { return Collections.unmodifiableSet(topicNameToSubscribedConsumerIdSet.keySet()); } + public Set<String> getRetainedInitialTopicNames() { + return Collections.unmodifiableSet(retainedInitialTopicNames); + } + + public void retainInitialTopic(final String topicName) { + retainedInitialTopicNames.add(topicName); + } + + public void removeRetainedInitialTopic(final String topicName) { + retainedInitialTopicNames.remove(topicName); + } + /** * Get the consumers subscribing the given topic in this group. * @@ -338,7 +352,14 @@ public class ConsumerGroupMeta { ReadWriteIOUtils.write(consumerGroupId, outputStream); ReadWriteIOUtils.write(creationTime, outputStream); - ReadWriteIOUtils.write(topicNameToSubscribedConsumerIdSet.size(), outputStream); + // A negative topic count identifies the extended, self-delimiting format. Do not infer the + // presence of retained topics from remaining bytes: this meta is also nested in procedures. + final boolean hasRetainedInitialTopics = !retainedInitialTopicNames.isEmpty(); + ReadWriteIOUtils.write( + hasRetainedInitialTopics + ? -topicNameToSubscribedConsumerIdSet.size() - 1 + : topicNameToSubscribedConsumerIdSet.size(), + outputStream); for (final Map.Entry<String, Set<String>> entry : topicNameToSubscribedConsumerIdSet.entrySet()) { ReadWriteIOUtils.write(entry.getKey(), outputStream); @@ -365,6 +386,12 @@ public class ConsumerGroupMeta { ReadWriteIOUtils.write(entry.getValue(), outputStream); } } + if (hasRetainedInitialTopics) { + ReadWriteIOUtils.write(retainedInitialTopicNames.size(), outputStream); + for (final String topicName : retainedInitialTopicNames) { + ReadWriteIOUtils.write(topicName, outputStream); + } + } } public static ConsumerGroupMeta deserialize(final InputStream inputStream) throws IOException { @@ -375,6 +402,10 @@ public class ConsumerGroupMeta { consumerGroupMeta.topicNameToSubscribedConsumerIdSet = new ConcurrentHashMap<>(); int size = ReadWriteIOUtils.readInt(inputStream); + final boolean hasRetainedInitialTopics = size < 0; + if (hasRetainedInitialTopics) { + size = -size - 1; + } for (int i = 0; i < size; ++i) { final String key = ReadWriteIOUtils.readString(inputStream); @@ -407,6 +438,12 @@ public class ConsumerGroupMeta { } } + if (hasRetainedInitialTopics) { + size = ReadWriteIOUtils.readInt(inputStream); + for (int i = 0; i < size; ++i) { + consumerGroupMeta.retainedInitialTopicNames.add(ReadWriteIOUtils.readString(inputStream)); + } + } return consumerGroupMeta; } @@ -418,6 +455,10 @@ public class ConsumerGroupMeta { consumerGroupMeta.topicNameToSubscribedConsumerIdSet = new ConcurrentHashMap<>(); int size = ReadWriteIOUtils.readInt(byteBuffer); + final boolean hasRetainedInitialTopics = size < 0; + if (hasRetainedInitialTopics) { + size = -size - 1; + } for (int i = 0; i < size; ++i) { final String key = ReadWriteIOUtils.readString(byteBuffer); @@ -450,6 +491,12 @@ public class ConsumerGroupMeta { } } + if (hasRetainedInitialTopics) { + size = ReadWriteIOUtils.readInt(byteBuffer); + for (int i = 0; i < size; ++i) { + consumerGroupMeta.retainedInitialTopicNames.add(ReadWriteIOUtils.readString(byteBuffer)); + } + } return consumerGroupMeta; } @@ -470,6 +517,7 @@ public class ConsumerGroupMeta { && Objects.equals( this.topicNameToSubscribedConsumerIdSet, that.topicNameToSubscribedConsumerIdSet) && Objects.equals(this.consumerIdToConsumerMeta, that.consumerIdToConsumerMeta) + && Objects.equals(this.retainedInitialTopicNames, that.retainedInitialTopicNames) && Objects.equals( this.topicNameToSubscriptionCreationTime, that.topicNameToSubscriptionCreationTime); } @@ -482,6 +530,7 @@ public class ConsumerGroupMeta { tableModel, topicNameToSubscribedConsumerIdSet, consumerIdToConsumerMeta, + retainedInitialTopicNames, topicNameToSubscriptionCreationTime); } @@ -498,6 +547,8 @@ public class ConsumerGroupMeta { + consumerIdToConsumerMeta + ", topicNameToSubscriptionCreationTime=" + topicNameToSubscriptionCreationTime + + ", retainedInitialTopicNames=" + + retainedInitialTopicNames + "}"; } } diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java index 2d2014d7e97..6f1cfc3ec5a 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java @@ -29,6 +29,8 @@ import org.junit.Assert; import org.junit.Test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Collections; @@ -39,6 +41,39 @@ import static org.junit.Assert.fail; public class ConsumerGroupDeSerTest { + @Test + public void testRetainedInitialTopicsAreSelfDelimitedAndPreserveModel() throws Exception { + for (final String dialect : new String[] {"tree", "table"}) { + final ConsumerGroupMeta group = + new ConsumerGroupMeta( + "retained", + 1L, + new ConsumerMeta( + "consumer", + 1L, + Collections.singletonMap(ConsumerConstant.SQL_DIALECT_KEY, dialect))); + group.retainInitialTopic("topic"); + group.removeConsumer("consumer"); + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final DataOutputStream output = new DataOutputStream(bytes); + group.serialize(output); + output.writeInt(123456); + + final ByteBuffer buffer = ByteBuffer.wrap(bytes.toByteArray()); + final ConsumerGroupMeta restored = ConsumerGroupMeta.deserialize(buffer); + Assert.assertEquals(group, restored); + Assert.assertEquals(123456, buffer.getInt()); + final ByteArrayInputStream input = new ByteArrayInputStream(bytes.toByteArray()); + Assert.assertEquals(group, ConsumerGroupMeta.deserialize(input)); + Assert.assertEquals(Integer.BYTES, input.available()); + final ConsumerGroupMeta copied = group.deepCopy(); + copied.removeRetainedInitialTopic("topic"); + Assert.assertEquals(Collections.singleton("topic"), group.getRetainedInitialTopicNames()); + Assert.assertTrue(copied.getRetainedInitialTopicNames().isEmpty()); + Assert.assertEquals("table".equals(dialect), restored.visibleUnder(true)); + } + } + @Test public void testEmptyGroupPreservesDialectAcrossSerializationAndRejoin() throws Exception { for (final String dialect : new String[] {"tree", "table"}) {
