This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 8effe5d6ce4 Support retaining subscription progress after unsubscribe
(#18718)
8effe5d6ce4 is described below
commit 8effe5d6ce475eadfd3160afa3baf0662b684ad8
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 14 17:58:38 2026 +0800
Support retaining subscription progress after unsubscribe (#18718)
* Support retaining subscription progress after unsubscribe
* [Subscription] Address WAL retention review comments
---
.../iotdb/rpc/subscription/config/TopicConfig.java | 8 +
.../rpc/subscription/config/TopicConstant.java | 3 +
.../rpc/subscription/config/TopicConfigTest.java | 12 +
.../iotdb/confignode/i18n/ConfigNodeMessages.java | 6 +
.../iotdb/confignode/i18n/ConfigNodeMessages.java | 6 +
.../persistence/subscription/SubscriptionInfo.java | 109 ++++++--
.../subscription/SubscriptionInfoTest.java | 125 +++++++++
.../SubscriptionInfoTopicValidationTest.java | 44 +++
.../apache/iotdb/consensus/iot/IoTConsensus.java | 15 +-
.../consensus/iot/IoTConsensusServerImpl.java | 38 ++-
.../subscription/SubscriptionQueueRegistry.java | 50 +++-
.../SubscriptionQueueRegistryTest.java | 42 +++
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 2 +
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 2 +
.../java/org/apache/iotdb/db/service/DataNode.java | 4 +
.../agent/SubscriptionBrokerAgent.java | 12 +-
.../agent/SubscriptionConsumerAgent.java | 42 +++
.../agent/SubscriptionRuntimeAgent.java | 2 +
.../subscription/agent/SubscriptionTopicAgent.java | 5 +
.../broker/ConsensusSubscriptionBroker.java | 24 +-
.../consensus/ConsensusPrefetchingQueue.java | 113 ++++++--
.../ConsensusSubscriptionCommitManager.java | 76 +++++
.../ConsensusSubscriptionSetupHandler.java | 308 ++++++++++++++++++++-
.../ConsensusSubscriptionWalRetention.java | 163 +++++++++++
.../consensus/ConsensusPrefetchingQueueTest.java | 33 ++-
.../ConsensusSubscriptionCommitStateTest.java | 35 +++
.../ConsensusSubscriptionSetupHandlerTest.java | 47 ++++
.../ConsensusSubscriptionWalRetentionTest.java | 102 +++++++
.../meta/consumer/CommitProgressKeeper.java | 15 +
.../meta/consumer/CommitProgressKeeperTest.java | 19 ++
30 files changed, 1389 insertions(+), 73 deletions(-)
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConfig.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConfig.java
index 3d107476b85..51b87f910e2 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConfig.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConfig.java
@@ -136,6 +136,14 @@ public class TopicConfig extends PipeParameters {
return TopicConstant.MODE_INCREMENTAL_VALUE.equalsIgnoreCase(getMode());
}
+ public boolean isProgressRetainedAfterUnsubscribe() {
+ return Boolean.parseBoolean(
+ getStringIgnoreCase(
+ TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
+ TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_DEFAULT_VALUE)
+ .trim());
+ }
+
/**
* @deprecated Use {@link #isInitialMode()}.
*/
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java
index 4d11f2fe8a2..960e3bab524 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/config/TopicConstant.java
@@ -33,6 +33,9 @@ public class TopicConstant {
public static final String COLUMN_FILTER_KEY = "column-filter";
public static final String RETENTION_BYTES_KEY = "retention.bytes";
public static final String RETENTION_MS_KEY = "retention.ms";
+ public static final String RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY =
+ "retain.progress.after.unsubscribe";
+ public static final String RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_DEFAULT_VALUE =
"false";
public static final String DATABASE_DEFAULT_VALUE = ".*";
public static final String TABLE_DEFAULT_VALUE = ".*";
public static final String COLUMN_FILTER_DEFAULT_VALUE = "true";
diff --git
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/config/TopicConfigTest.java
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/config/TopicConfigTest.java
index d308ef123cf..3b32d407eba 100644
---
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/config/TopicConfigTest.java
+++
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/config/TopicConfigTest.java
@@ -110,6 +110,18 @@ public class TopicConfigTest {
Assert.assertTrue(new TopicConfig(attributes).isColumnFilterTrivial());
}
+ @Test
+ public void testRetainProgressAfterUnsubscribeDefaultsToFalse() {
+ Assert.assertFalse(new TopicConfig().isProgressRetainedAfterUnsubscribe());
+ }
+
+ @Test
+ public void testRetainProgressAfterUnsubscribeIsCaseInsensitiveAndTrimmed() {
+ Assert.assertTrue(
+ new
TopicConfig(Collections.singletonMap("Retain.Progress.After.Unsubscribe", "
TRUE "))
+ .isProgressRetainedAfterUnsubscribe());
+ }
+
private static TopicConfig topicConfigWithMode(final String mode) {
return new TopicConfig(Collections.singletonMap(TopicConstant.MODE_KEY,
mode));
}
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 0e834e0603f..fb73cc93376 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
@@ -617,6 +617,9 @@ public final class ConfigNodeMessages {
public static final String EXCEPTION_NO_SUCH_USER_ID_99CA691B = "No such
user id: ";
public static final String
LOG_HANDLING_COMMIT_PROGRESS_META_CHANGES_FA21A080 = "Handling commit progress
meta changes ...";
public static final String
EXCEPTION_FAILED_CREATE_ALTER_TOPIC_ILLEGAL_ARG_ARG_EXPECTED_1_POSITIVE_A33070FB
= "Failed to create or alter topic, illegal %s=%s, expected -1 or a positive
long value";
+ public static final String
+
EXCEPTION_FAILED_CREATE_ALTER_TOPIC_ILLEGAL_ARG_ARG_EXPECTED_TRUE_OR_FALSE_855A6926
=
+ "Failed to create or alter topic, illegal %s=%s, expected true or
false";
public static final String
LOG_TRYING_GET_MAX_TTL_UNDER_ONE_DATABASE_USE_LONG_MAX_9D70ACB2 = " when trying
to get max ttl under one database, use Long.MAX_VALUE.";
public static final String
MESSAGE_ENABLE_IOTDB_CLUSTER_S_DATA_SERVICE_PLEASE_REGISTER_ARG_MORE_F48F3890 =
"To enable IoTDB-Cluster's data service, please register %d more
IoTDB-DataNode";
public static final String
MESSAGE_APPLY_NEW_CONFIGNODE_FAILED_BECAUSE_CURRENT_CONFIGNODE_CAN_T_STORE_1BB6A6BF
= "Apply new ConfigNode failed because current ConfigNode can't store
ConfigNode information.";
@@ -675,6 +678,9 @@ public final class ConfigNodeMessages {
public static final String
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ARG_AND_ARG_ARE_ONLY_SUPPORTED_FOR_INCREMENTAL_TOPICS_D86CEA8E
=
"Failed to create or alter topic, %s and %s are only supported for
incremental topics";
+ public static final String
+
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ATTRIBUTES_ARG_ARE_ONLY_SUPPORTED_FOR_INCREMENTAL_TOPICS_55CB0BF2
=
+ "Failed to create or alter topic, attributes %s are only supported
for incremental topics";
public static final String
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_SUBSCRIBING_ONLY_TO_THE_AUDIT_DATABASE_OR_PATHS_UNDER_IT_IS_NOT_ALLOWED_3E96A6BA
=
"Failed to create or alter topic, subscribing only to the __audit
database or paths under it is not allowed";
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 9f21686d880..948f4a64765 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
@@ -638,6 +638,9 @@ public final class ConfigNodeMessages {
public static final String
EXCEPTION_FAILED_CREATE_ALTER_TOPIC_ILLEGAL_ARG_ARG_EXPECTED_1_POSITIVE_A33070FB
=
"创建或修改 topic 失败,非法参数 %s=%s,期望为 -1 或正 long 值";
+ public static final String
+
EXCEPTION_FAILED_CREATE_ALTER_TOPIC_ILLEGAL_ARG_ARG_EXPECTED_TRUE_OR_FALSE_855A6926
=
+ "创建或修改 topic 失败,非法参数 %s=%s,期望为 true 或 false";
public static final String
LOG_TRYING_GET_MAX_TTL_UNDER_ONE_DATABASE_USE_LONG_MAX_9D70ACB2 =
" 尝试获取单个数据库下的最大 ttl 时,使用 Long.MAX_VALUE。";
public static final String
@@ -717,6 +720,9 @@ public final class ConfigNodeMessages {
public static final String
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ARG_AND_ARG_ARE_ONLY_SUPPORTED_FOR_INCREMENTAL_TOPICS_D86CEA8E
=
"创建或修改 topic 失败,%s 和 %s 仅支持 incremental 模式的 topic";
+ public static final String
+
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ATTRIBUTES_ARG_ARE_ONLY_SUPPORTED_FOR_INCREMENTAL_TOPICS_55CB0BF2
=
+ "创建或修改 topic 失败,属性 %s 仅支持 incremental 模式的 topic";
public static final String
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_SUBSCRIBING_ONLY_TO_THE_AUDIT_DATABASE_OR_PATHS_UNDER_IT_IS_NOT_ALLOWED_3E96A6BA
=
"创建或修改 topic 失败,不允许仅订阅 __audit 数据库或其下的路径";
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 6ea5152f4df..9fab5d1f536 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
@@ -108,6 +108,7 @@ public class SubscriptionInfo implements SnapshotProcessor {
TopicConstant.COLUMN_FILTER_KEY,
TopicConstant.RETENTION_BYTES_KEY,
TopicConstant.RETENTION_MS_KEY,
+ TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
TopicConstant.START_TIME_KEY,
TopicConstant.END_TIME_KEY,
TopicConstant.MODE_KEY,
@@ -129,6 +130,7 @@ public class SubscriptionInfo implements SnapshotProcessor {
TopicConstant.COLUMN_FILTER_KEY,
TopicConstant.RETENTION_BYTES_KEY,
TopicConstant.RETENTION_MS_KEY,
+ TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
TopicConstant.MODE_KEY,
TopicConstant.ORDER_MODE_KEY,
TopicConstant.FORMAT_KEY,
@@ -586,8 +588,10 @@ public class SubscriptionInfo implements SnapshotProcessor
{
private void validateIncrementalTopicRetentionConfig(final TopicConfig
topicConfig)
throws SubscriptionException {
- if (!topicConfig.hasAttribute(TopicConstant.RETENTION_BYTES_KEY)
- && !topicConfig.hasAttribute(TopicConstant.RETENTION_MS_KEY)) {
+ if (!hasTopicAttributeIgnoreCase(topicConfig,
TopicConstant.RETENTION_BYTES_KEY)
+ && !hasTopicAttributeIgnoreCase(topicConfig,
TopicConstant.RETENTION_MS_KEY)
+ && !hasTopicAttributeIgnoreCase(
+ topicConfig, TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY))
{
return;
}
@@ -595,24 +599,48 @@ public class SubscriptionInfo implements
SnapshotProcessor {
final String exceptionMessage =
String.format(
ConfigNodeMessages
-
.EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_ARG_AND_ARG_ARE_ONLY_SUPPORTED_FOR_INCREMENTAL_TOPICS_D86CEA8E,
- TopicConstant.RETENTION_BYTES_KEY,
- TopicConstant.RETENTION_MS_KEY);
+
.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));
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)
+ throws SubscriptionException {
+ if (!hasTopicAttributeIgnoreCase(
+ topicConfig, TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY)) {
+ return;
+ }
+ final String value =
+ getTopicAttributeIgnoreCase(
+ topicConfig, TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY);
+ if (Objects.isNull(value)
+ || (!Boolean.TRUE.toString().equalsIgnoreCase(value.trim())
+ && !Boolean.FALSE.toString().equalsIgnoreCase(value.trim()))) {
+ throw new SubscriptionException(
+ String.format(
+ ConfigNodeMessages
+
.EXCEPTION_FAILED_CREATE_ALTER_TOPIC_ILLEGAL_ARG_ARG_EXPECTED_TRUE_OR_FALSE_855A6926,
+ TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
+ value));
+ }
}
private void validateRetentionValue(final TopicConfig topicConfig, final
String key)
throws SubscriptionException {
- if (!topicConfig.hasAttribute(key)) {
+ if (!hasTopicAttributeIgnoreCase(topicConfig, key)) {
return;
}
- final String rawValue = topicConfig.getAttribute().get(key);
+ final String rawValue = getTopicAttributeIgnoreCase(topicConfig, key);
try {
final long parsedValue = Long.parseLong(rawValue);
if (parsedValue == 0 || parsedValue < -1) {
@@ -636,6 +664,23 @@ public class SubscriptionInfo implements SnapshotProcessor
{
}
}
+ private static boolean hasTopicAttributeIgnoreCase(
+ final TopicConfig topicConfig, final String expectedKey) {
+ return topicConfig.getAttribute().keySet().stream()
+ .filter(Objects::nonNull)
+ .anyMatch(expectedKey::equalsIgnoreCase);
+ }
+
+ private static String getTopicAttributeIgnoreCase(
+ final TopicConfig topicConfig, final String expectedKey) {
+ return topicConfig.getAttribute().entrySet().stream()
+ .filter(entry -> Objects.nonNull(entry.getKey()))
+ .filter(entry -> expectedKey.equalsIgnoreCase(entry.getKey()))
+ .map(Map.Entry::getValue)
+ .findFirst()
+ .orElse(null);
+ }
+
private void validateUnsupportedHotUpdatedTopicConfig(
final String topicName, final TopicConfig existedConfig, final
TopicConfig updatedConfig)
throws SubscriptionException {
@@ -665,8 +710,8 @@ public class SubscriptionInfo implements SnapshotProcessor {
final String... retentionKeys)
throws SubscriptionException {
for (final String retentionKey : retentionKeys) {
- final String existedValue =
existedConfig.getAttribute().get(retentionKey);
- final String updatedValue =
updatedConfig.getAttribute().get(retentionKey);
+ final String existedValue = getTopicAttributeIgnoreCase(existedConfig,
retentionKey);
+ final String updatedValue = getTopicAttributeIgnoreCase(updatedConfig,
retentionKey);
if (!Objects.equals(existedValue, updatedValue)) {
final String exceptionMessage =
String.format(
@@ -837,15 +882,29 @@ public class SubscriptionInfo implements
SnapshotProcessor {
private TSStatus alterTopicInternal(final AlterTopicPlan plan) {
final boolean isTableModel = plan.getTopicMeta().visibleUnderTableModel();
+ final TopicMeta oldTopicMeta =
+ topicMetaKeeper.getTopicMeta(plan.getTopicMeta().getTopicName(),
isTableModel);
try {
- TopicMeta.validateOwnerProgression(
- topicMetaKeeper.getTopicMeta(plan.getTopicMeta().getTopicName(),
isTableModel),
- plan.getTopicMeta());
+ TopicMeta.validateOwnerProgression(oldTopicMeta, plan.getTopicMeta());
} catch (final IllegalArgumentException e) {
return new
TSStatus(TSStatusCode.SUBSCRIPTION_OWNER_EPOCH_CONFLICT.getStatusCode())
.setMessage(e.getMessage());
}
+ if (Objects.nonNull(oldTopicMeta)
+ && oldTopicMeta.getConfig().isProgressRetainedAfterUnsubscribe()
+ &&
!plan.getTopicMeta().getConfig().isProgressRetainedAfterUnsubscribe()) {
+ for (final ConsumerGroupMeta consumerGroupMeta :
+ consumerGroupMetaKeeper.getAllConsumerGroupMeta()) {
+ if (consumerGroupMeta.visibleUnder(isTableModel)
+ && !consumerGroupMeta.isTopicSubscribedByConsumerGroup(
+ plan.getTopicMeta().getTopicName())) {
+ commitProgressKeeper.removeTopicProgress(
+ consumerGroupMeta.getConsumerGroupId(),
plan.getTopicMeta().getTopicName());
+ }
+ }
+ }
+
topicMetaKeeper.removeTopicMeta(plan.getTopicMeta().getTopicName(),
isTableModel);
topicMetaKeeper.addTopicMeta(plan.getTopicMeta().getTopicName(),
plan.getTopicMeta());
return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
@@ -879,6 +938,13 @@ public class SubscriptionInfo implements SnapshotProcessor
{
public TSStatus dropTopic(DropTopicPlan plan) {
acquireWriteLock();
try {
+ for (final ConsumerGroupMeta consumerGroupMeta :
+ consumerGroupMetaKeeper.getAllConsumerGroupMeta()) {
+ if (!plan.isTableModelSet() ||
consumerGroupMeta.visibleUnder(plan.isTableModel())) {
+ commitProgressKeeper.removeTopicProgress(
+ consumerGroupMeta.getConsumerGroupId(), plan.getTopicName());
+ }
+ }
if (plan.isTableModelSet()) {
topicMetaKeeper.removeTopicMeta(plan.getTopicName(),
plan.isTableModel());
} else {
@@ -1052,10 +1118,21 @@ public class SubscriptionInfo implements
SnapshotProcessor {
? consumerGroupMetaKeeper.getConsumerGroupMeta(consumerGroupId)
: null;
if (Objects.nonNull(currentConsumerGroupMeta)) {
- ConsumerGroupMeta.getTopicsUnsubByGroup(currentConsumerGroupMeta,
consumerGroupMeta)
- .forEach(
- topicName ->
-
commitProgressKeeper.removeTopicProgress(consumerGroupId, topicName));
+ if (consumerGroupMeta.isEmpty()) {
+ commitProgressKeeper.removeConsumerGroupProgress(consumerGroupId);
+ } else {
+ ConsumerGroupMeta.getTopicsUnsubByGroup(currentConsumerGroupMeta,
consumerGroupMeta)
+ .forEach(
+ topicName -> {
+ final TopicMeta topicMeta =
+ topicMetaKeeper.getTopicMeta(
+ topicName,
currentConsumerGroupMeta.visibleUnder(true));
+ if (Objects.isNull(topicMeta)
+ ||
!topicMeta.getConfig().isProgressRetainedAfterUnsubscribe()) {
+
commitProgressKeeper.removeTopicProgress(consumerGroupId, topicName);
+ }
+ });
+ }
}
consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId);
if (!consumerGroupMeta.isEmpty()) {
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java
index 170bbe2d3e1..087cb5868f5 100644
---
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java
@@ -251,6 +251,13 @@ public class SubscriptionInfoTest {
final String retainedTopic = "retained-topic";
final String regionId = "DataRegion[1]";
final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ subscriptionInfo.createTopic(
+ new CreateTopicPlan(
+ new TopicMeta(
+ removedTopic,
+ 1L,
+ Collections.singletonMap(
+ TopicConstant.MODE_KEY,
TopicConstant.MODE_INCREMENTAL_VALUE))));
final ConsumerGroupMeta currentMeta =
new ConsumerGroupMeta(
consumerGroupId, 1, new ConsumerMeta(consumerId, 1,
Collections.emptyMap()));
@@ -277,6 +284,124 @@ public class SubscriptionInfoTest {
Assert.assertNotNull(keeper.getRegionProgress(retainedKey));
}
+ @Test
+ public void
testAlterConsumerGroupRetainsUnsubscribedTopicProgressWhenConfigured() {
+ final String consumerGroupId = "consumer-group";
+ final String consumerId = "consumer";
+ final String topicName = "retained-topic";
+ final String regionId = "DataRegion[1]";
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final Map<String, String> topicAttributes = new HashMap<>();
+ topicAttributes.put(TopicConstant.MODE_KEY,
TopicConstant.MODE_INCREMENTAL_VALUE);
+ topicAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"true");
+ subscriptionInfo.createTopic(
+ new CreateTopicPlan(new TopicMeta(topicName, 1L, topicAttributes)));
+
+ final ConsumerGroupMeta currentMeta =
+ new ConsumerGroupMeta(
+ consumerGroupId, 1, new ConsumerMeta(consumerId, 1,
Collections.emptyMap()));
+ currentMeta.addSubscription(consumerId, Collections.singleton(topicName));
+ subscriptionInfo.alterConsumerGroup(new
AlterConsumerGroupPlan(currentMeta));
+
+ final CommitProgressKeeper keeper =
subscriptionInfo.getCommitProgressKeeper();
+ final String progressKey =
+ CommitProgressKeeper.generateKey(consumerGroupId, topicName, regionId,
1);
+ keeper.updateRegionProgress(progressKey, ByteBuffer.wrap(new byte[] {1}));
+
+ final ConsumerGroupMeta updatedMeta = currentMeta.deepCopy();
+ updatedMeta.removeSubscription(consumerId,
Collections.singleton(topicName));
+ subscriptionInfo.alterConsumerGroup(new
AlterConsumerGroupPlan(updatedMeta));
+
+ Assert.assertNotNull(keeper.getRegionProgress(progressKey));
+ }
+
+ @Test
+ public void testAlterTopicDisablingRetentionRemovesUnsubscribedProgress() {
+ final String consumerGroupId = "consumer-group";
+ final String consumerId = "consumer";
+ final String topicName = "retained-topic";
+ final String regionId = "DataRegion[1]";
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final Map<String, String> topicAttributes = new HashMap<>();
+ topicAttributes.put(TopicConstant.MODE_KEY,
TopicConstant.MODE_INCREMENTAL_VALUE);
+ topicAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"true");
+ subscriptionInfo.createTopic(
+ new CreateTopicPlan(new TopicMeta(topicName, 1L, topicAttributes)));
+
+ final ConsumerGroupMeta currentMeta =
+ new ConsumerGroupMeta(
+ consumerGroupId, 1, new ConsumerMeta(consumerId, 1,
Collections.emptyMap()));
+ currentMeta.addSubscription(consumerId, Collections.singleton(topicName));
+ subscriptionInfo.alterConsumerGroup(new
AlterConsumerGroupPlan(currentMeta));
+
+ final CommitProgressKeeper keeper =
subscriptionInfo.getCommitProgressKeeper();
+ final String progressKey =
+ CommitProgressKeeper.generateKey(consumerGroupId, topicName, regionId,
1);
+ keeper.updateRegionProgress(progressKey, ByteBuffer.wrap(new byte[] {1}));
+
+ final ConsumerGroupMeta unsubscribedMeta = currentMeta.deepCopy();
+ unsubscribedMeta.removeSubscription(consumerId,
Collections.singleton(topicName));
+ subscriptionInfo.alterConsumerGroup(new
AlterConsumerGroupPlan(unsubscribedMeta));
+ Assert.assertNotNull(keeper.getRegionProgress(progressKey));
+
+ final Map<String, String> updatedAttributes = new
HashMap<>(topicAttributes);
+ updatedAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"false");
+ subscriptionInfo.alterTopic(
+ new AlterTopicPlan(new TopicMeta(topicName, 2L, updatedAttributes)));
+
+ Assert.assertNull(keeper.getRegionProgress(progressKey));
+ }
+
+ @Test
+ public void testDropTopicRemovesRetainedProgress() {
+ final String consumerGroupId = "consumer-group";
+ final String consumerId = "consumer";
+ final String topicName = "retained-topic";
+ final String regionId = "DataRegion[1]";
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final Map<String, String> topicAttributes = new HashMap<>();
+ topicAttributes.put(TopicConstant.MODE_KEY,
TopicConstant.MODE_INCREMENTAL_VALUE);
+ topicAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"true");
+ subscriptionInfo.createTopic(
+ new CreateTopicPlan(new TopicMeta(topicName, 1L, topicAttributes)));
+
+ final ConsumerGroupMeta currentMeta =
+ new ConsumerGroupMeta(
+ consumerGroupId, 1, new ConsumerMeta(consumerId, 1,
Collections.emptyMap()));
+ subscriptionInfo.alterConsumerGroup(new
AlterConsumerGroupPlan(currentMeta));
+ final CommitProgressKeeper keeper =
subscriptionInfo.getCommitProgressKeeper();
+ final String progressKey =
+ CommitProgressKeeper.generateKey(consumerGroupId, topicName, regionId,
1);
+ keeper.updateRegionProgress(progressKey, ByteBuffer.wrap(new byte[] {1}));
+
+ subscriptionInfo.dropTopic(new DropTopicPlan(topicName, false));
+
+ Assert.assertNull(keeper.getRegionProgress(progressKey));
+ }
+
+ @Test
+ public void testDropConsumerGroupRemovesAllRetainedProgress() {
+ final String consumerGroupId = "consumer-group";
+ final String consumerId = "consumer";
+ final String topicName = "retained-topic";
+ final String regionId = "DataRegion[1]";
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final ConsumerGroupMeta currentMeta =
+ new ConsumerGroupMeta(
+ consumerGroupId, 1, new ConsumerMeta(consumerId, 1,
Collections.emptyMap()));
+ subscriptionInfo.alterConsumerGroup(new
AlterConsumerGroupPlan(currentMeta));
+ final CommitProgressKeeper keeper =
subscriptionInfo.getCommitProgressKeeper();
+ final String progressKey =
+ CommitProgressKeeper.generateKey(consumerGroupId, topicName, regionId,
1);
+ keeper.updateRegionProgress(progressKey, ByteBuffer.wrap(new byte[] {1}));
+
+ final ConsumerGroupMeta emptyMeta = currentMeta.deepCopy();
+ emptyMeta.removeConsumer(consumerId);
+ subscriptionInfo.alterConsumerGroup(new AlterConsumerGroupPlan(emptyMeta));
+
+ Assert.assertNull(keeper.getRegionProgress(progressKey));
+ }
+
private TopicMeta createTopicMeta(
final String topicName, final String ownerId, final long ownerEpoch) {
final Map<String, String> topicAttributes = new HashMap<>();
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 48523171345..da9c356190c 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
@@ -325,6 +325,21 @@ public class SubscriptionInfoTopicValidationTest {
}
}
+ @Test
+ public void testAcceptAlteringRetainProgressAfterUnsubscribe() throws
Exception {
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final Map<String, String> originalAttributes =
newIncrementalTableTopicAttributes();
+
originalAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"false");
+ subscriptionInfo.createTopic(
+ new CreateTopicPlan(new TopicMeta("table_topic", 1L,
originalAttributes)));
+
+ final Map<String, String> updatedAttributes =
newIncrementalTableTopicAttributes();
+ updatedAttributes.put("Retain.Progress.After.Unsubscribe", "true");
+
+ subscriptionInfo.validateBeforeAlteringTopic(
+ new TopicMeta("table_topic", 2L, updatedAttributes));
+ }
+
@Test
public void testRejectIllegalMode() {
final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
@@ -385,6 +400,35 @@ public class SubscriptionInfoTopicValidationTest {
assertCreateRejected(subscriptionInfo, attributes, "only supported for
incremental topics");
}
+ @Test
+ public void testAcceptRetainingProgressOnIncrementalTopic() throws Exception
{
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final Map<String, String> attributes =
newIncrementalTableTopicAttributes();
+ attributes.put("Retain.Progress.After.Unsubscribe", " TRUE ");
+
+ Assert.assertTrue(
+ subscriptionInfo.validateBeforeCreatingTopic(
+ new
TCreateTopicReq("table_topic").setTopicAttributes(attributes)));
+ }
+
+ @Test
+ public void testRejectRetainingProgressOnInitialTopic() {
+ final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+ final Map<String, String> attributes = newInitialTableTopicAttributes();
+ attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"true");
+
+ 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");
+ }
+
@Test
public void testRejectOwnerLeaseDurationBelowMin() {
final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
diff --git
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java
index 9c0692132f0..8ee9ff96b7a 100644
---
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java
+++
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java
@@ -116,6 +116,12 @@ public class IoTConsensus implements IConsensus {
*/
public static volatile BiConsumer<ConsensusGroupId, IoTConsensusServerImpl>
onNewPeerCreated;
+ /**
+ * Optional callback invoked after a recovered local peer is constructed and
before it starts.
+ * Used by the subscription system to restore WAL-retention guards before
WAL cleanup is enabled.
+ */
+ public static volatile BiConsumer<ConsensusGroupId, IoTConsensusServerImpl>
onPeerRecovering;
+
/**
* Optional callback invoked before a local peer is deleted via {@link
#deleteLocalPeer}. Used by
* the subscription system to unbind and clean up prefetching queues before
the region is removed.
@@ -244,7 +250,14 @@ public class IoTConsensus implements IConsensus {
consensusGroupId ->
resetPeerListWithoutThrow.accept(consensusGroupId,
Collections.emptyList()));
}
- stateMachineMap.values().forEach(IoTConsensusServerImpl::start);
+ stateMachineMap.forEach(
+ (consensusGroupId, consensus) -> {
+ final BiConsumer<ConsensusGroupId, IoTConsensusServerImpl> callback
= onPeerRecovering;
+ if (callback != null) {
+ callback.accept(consensusGroupId, consensus);
+ }
+ consensus.start();
+ });
}
@Override
diff --git
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
index ba4ad87b1fd..8e70866e379 100644
---
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
+++
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
@@ -359,7 +359,7 @@ public class IoTConsensusServerImpl {
subscriptionQueueRegistry.offer(indexedConsensusRequest);
logDispatcher.offer(indexedConsensusRequest, offeredToSubscription);
if (!offeredToSubscription
- && subscriptionQueueRegistry.isEmpty()
+ && !subscriptionQueueRegistry.hasQueues()
&& logger.isDebugEnabled()
&& indexedConsensusRequest.getSearchIndex() % 50 == 0) {
// Log periodically when no subscription queues are registered
@@ -1106,7 +1106,7 @@ public class IoTConsensusServerImpl {
}
public boolean hasSubscriptionConsumers() {
- return !subscriptionQueueRegistry.isEmpty();
+ return subscriptionQueueRegistry.hasQueues();
}
private long assignPhysicalTimeInMs() {
@@ -1280,6 +1280,16 @@ public class IoTConsensusServerImpl {
System.identityHashCode(this));
}
+ public void replaceDetachedSubscriptionRetentionWithQueue(
+ final String retentionId,
+ final BlockingQueue<IndexedConsensusRequest> queue,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier committedRetainedMinVersionIdSupplier) {
+ subscriptionQueueRegistry.replaceDetachedRetentionWithQueue(
+ retentionId, queue, retentionPolicy,
committedRetainedMinVersionIdSupplier);
+ checkAndUpdateSafeDeletedSearchIndex();
+ }
+
public void deregisterSubscriptionQueue(final
BlockingQueue<IndexedConsensusRequest> queue) {
subscriptionQueueRegistry.unregister(queue);
// Re-evaluate: with fewer subscribers, more WAL may be deletable
@@ -1291,6 +1301,30 @@ public class IoTConsensusServerImpl {
subscriptionQueueRegistry.size());
}
+ public void replaceSubscriptionQueueWithDetachedRetention(
+ final BlockingQueue<IndexedConsensusRequest> queue,
+ final String retentionId,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier committedRetainedMinVersionIdSupplier) {
+ subscriptionQueueRegistry.replaceQueueWithDetachedRetention(
+ queue, retentionId, retentionPolicy,
committedRetainedMinVersionIdSupplier);
+ checkAndUpdateSafeDeletedSearchIndex();
+ }
+
+ public void registerDetachedSubscriptionRetention(
+ final String retentionId,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier committedRetainedMinVersionIdSupplier) {
+ subscriptionQueueRegistry.registerDetachedRetention(
+ retentionId, retentionPolicy, committedRetainedMinVersionIdSupplier);
+ checkAndUpdateSafeDeletedSearchIndex();
+ }
+
+ public void deregisterDetachedSubscriptionRetention(final String
retentionId) {
+ subscriptionQueueRegistry.unregisterDetachedRetention(retentionId);
+ checkAndUpdateSafeDeletedSearchIndex();
+ }
+
public long getSyncLag() {
long minSyncIndex = getMinSyncIndex();
return getSearchIndex() - minSyncIndex;
diff --git
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistry.java
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistry.java
index d2de8f93fd9..995b876cbd1 100644
---
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistry.java
+++
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistry.java
@@ -43,6 +43,8 @@ public class SubscriptionQueueRegistry {
private final String consensusGroupId;
private final Map<BlockingQueue<IndexedConsensusRequest>,
SubscriptionQueueRegistration> queues =
new ConcurrentHashMap<>();
+ private final Map<String, SubscriptionQueueRegistration>
detachedRetentionRegistrations =
+ new ConcurrentHashMap<>();
// offer is synchronized, so these counts remain scoped to one reason
without extra atomics.
private final long[] rejectedEntriesByReason =
new long[SubscriptionQueueRejectionReason.values().length];
@@ -79,8 +81,46 @@ public class SubscriptionQueueRegistry {
queues.remove(queue);
}
+ public synchronized void replaceQueueWithDetachedRetention(
+ final BlockingQueue<IndexedConsensusRequest> queue,
+ final String retentionId,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier retainedMinVersionIdSupplier) {
+ queues.remove(queue);
+ detachedRetentionRegistrations.put(
+ retentionId,
+ new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ }
+
+ public synchronized void replaceDetachedRetentionWithQueue(
+ final String retentionId,
+ final BlockingQueue<IndexedConsensusRequest> queue,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier retainedMinVersionIdSupplier) {
+ detachedRetentionRegistrations.remove(retentionId);
+ queues.put(
+ queue, new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ }
+
+ public synchronized void registerDetachedRetention(
+ final String retentionId,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier retainedMinVersionIdSupplier) {
+ detachedRetentionRegistrations.put(
+ retentionId,
+ new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ }
+
+ public synchronized void unregisterDetachedRetention(final String
retentionId) {
+ detachedRetentionRegistrations.remove(retentionId);
+ }
+
public synchronized boolean isEmpty() {
- return queues.isEmpty();
+ return queues.isEmpty() && detachedRetentionRegistrations.isEmpty();
+ }
+
+ public synchronized boolean hasQueues() {
+ return !queues.isEmpty();
}
public synchronized int size() {
@@ -93,6 +133,10 @@ public class SubscriptionQueueRegistry {
for (final SubscriptionQueueRegistration registration : queues.values()) {
retentionPolicies.add(registration.retentionPolicy);
}
+ for (final SubscriptionQueueRegistration registration :
+ detachedRetentionRegistrations.values()) {
+ retentionPolicies.add(registration.retentionPolicy);
+ }
return retentionPolicies;
}
@@ -102,6 +146,10 @@ public class SubscriptionQueueRegistry {
for (final SubscriptionQueueRegistration registration : queues.values())
{
suppliers.add(registration.retainedMinVersionIdSupplier);
}
+ for (final SubscriptionQueueRegistration registration :
+ detachedRetentionRegistrations.values()) {
+ suppliers.add(registration.retainedMinVersionIdSupplier);
+ }
}
final Collection<Long> retainedMinVersionIds = new
ArrayList<>(suppliers.size());
diff --git
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistryTest.java
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistryTest.java
index e9d0f7f3b0f..214cc2ec8f2 100644
---
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistryTest.java
+++
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/subscription/SubscriptionQueueRegistryTest.java
@@ -28,6 +28,7 @@ import org.junit.Test;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -63,6 +64,47 @@ public class SubscriptionQueueRegistryTest {
assertSame(request, queue.poll());
}
+ @Test
+ public void testDetachedRetentionProtectsWalWithoutReceivingRequests() {
+ final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("test");
+ final SubscriptionWalRetentionPolicy policy =
+ new SubscriptionWalRetentionPolicy("topic", 100, 200);
+ registry.registerDetachedRetention("retention", policy, () -> 7L);
+ final IndexedConsensusRequest request = newRequest();
+
+ assertFalse(registry.isEmpty());
+ assertFalse(registry.hasQueues());
+ assertFalse(registry.offer(request));
+ assertTrue(request.getSerializedRequests().isEmpty());
+ assertEquals(Collections.singletonList(policy),
registry.getRetentionPolicies());
+ assertEquals(Collections.singletonList(7L),
registry.getRetainedMinVersionIds());
+ }
+
+ @Test
+ public void testReplaceQueueAndDetachedRetentionAtomically() {
+ final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("test");
+ final InspectingQueue firstQueue = new InspectingQueue();
+ final InspectingQueue secondQueue = new InspectingQueue();
+ final SubscriptionWalRetentionPolicy policy =
+ new SubscriptionWalRetentionPolicy("topic", 100, 200);
+ final AtomicLong retainedVersion = new AtomicLong(3L);
+ registry.register(firstQueue, policy, retainedVersion::get);
+
+ registry.replaceQueueWithDetachedRetention(
+ firstQueue, "retention", policy, retainedVersion::get);
+ assertFalse(registry.hasQueues());
+ assertFalse(registry.isEmpty());
+ assertFalse(registry.offer(newRequest()));
+
+ retainedVersion.set(9L);
+ registry.replaceDetachedRetentionWithQueue(
+ "retention", secondQueue, policy, retainedVersion::get);
+ assertTrue(registry.hasQueues());
+ assertTrue(registry.offer(newRequest()));
+ assertEquals(1, secondQueue.size());
+ assertEquals(Collections.singletonList(9L),
registry.getRetainedMinVersionIds());
+ }
+
private static IndexedConsensusRequest newRequest() {
return new IndexedConsensusRequest(
1, Collections.singletonList(new
ByteBufferConsensusRequest(ByteBuffer.allocate(1))));
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 836ac88d84f..9d400f9d40b 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2692,6 +2692,8 @@ public final class DataNodePipeMessages {
"Topic metadata for %s is unavailable during consensus subscription
setup";
public static final String
EXCEPTION_TOPIC_CONFIG_FOR_ARG_IS_UNAVAILABLE_DURING_CONSENSUS_SUBSCRIPTION_SETUP_B94404EE
=
"Topic config for %s is unavailable during consensus subscription setup";
+ public static final String
LOG_FAILED_TO_RESTORE_DETACHED_CONSENSUS_SUBSCRIPTION_WAL_RETENTION_FOR_CONSUMER_GROUP_ARG_TOPIC_ARG_REGION_ARG_296129FE
=
+ "Failed to restore detached consensus subscription WAL retention for
consumer group {}, topic {}, region {}";
public static final String
LOG_FAILED_TO_RELEASE_TSFILE_PARSER_MEMORY_FOR_PIPE_ARG_CREATION_TIME_ARG_IN_DATAREGION_ARG_BECAUSE_NO_RESERVATION_EXISTS_BB8321C0
=
"Failed to release TsFile parser memory for Pipe {} (creation time {})
in DataRegion {} because no reservation exists.";
public static final String
LOG_PIPE_PROCESSOR_WORKER_ARG_HAS_BEEN_PROCESSING_THE_SAME_EVENT_FOR_ARG_MS_PIPE_ARG_DATAREGION_ARG_SUBTASK_ARG_EVENT_ARG_THREAD_STATE_ARG_STACK_ARG_63B40775
=
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 5840da12daf..4891920e079 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2517,6 +2517,8 @@ public final class DataNodePipeMessages {
"共识订阅设置期间 topic %s 的元数据不可用";
public static final String
EXCEPTION_TOPIC_CONFIG_FOR_ARG_IS_UNAVAILABLE_DURING_CONSENSUS_SUBSCRIPTION_SETUP_B94404EE
=
"共识订阅设置期间 topic %s 的配置不可用";
+ public static final String
LOG_FAILED_TO_RESTORE_DETACHED_CONSENSUS_SUBSCRIPTION_WAL_RETENTION_FOR_CONSUMER_GROUP_ARG_TOPIC_ARG_REGION_ARG_296129FE
=
+ "为 consumer group {}、topic {}、region {} 恢复 detached consensus
subscription WAL retention 失败";
public static final String
LOG_FAILED_TO_RELEASE_TSFILE_PARSER_MEMORY_FOR_PIPE_ARG_CREATION_TIME_ARG_IN_DATAREGION_ARG_BECAUSE_NO_RESERVATION_EXISTS_BB8321C0
=
"无法释放 Pipe {}(创建时间 {})在 DataRegion {} 中的 TsFile 解析器内存,因为不存在对应的预留。";
public static final String
LOG_PIPE_PROCESSOR_WORKER_ARG_HAS_BEEN_PROCESSING_THE_SAME_EVENT_FOR_ARG_MS_PIPE_ARG_DATAREGION_ARG_SUBTASK_ARG_EVENT_ARG_THREAD_STATE_ARG_STACK_ARG_63B40775
=
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
index 8291200b6ca..6bca0cace69 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNode.java
@@ -120,6 +120,7 @@ import
org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALMode;
import org.apache.iotdb.db.storageengine.load.active.ActiveLoadAgent;
import org.apache.iotdb.db.storageengine.rescon.disk.TierManager;
import org.apache.iotdb.db.subscription.agent.SubscriptionAgent;
+import
org.apache.iotdb.db.subscription.broker.consensus.ConsensusSubscriptionSetupHandler;
import org.apache.iotdb.db.trigger.executor.TriggerExecutor;
import org.apache.iotdb.db.trigger.service.TriggerInformationUpdater;
import org.apache.iotdb.db.trigger.service.TriggerManagementService;
@@ -300,6 +301,9 @@ public class DataNode extends ServerCommandLine implements
DataNodeMBean {
// TierManager need DataNodeId to do some operations so the reset method
need to be invoked
// after DataNode adding
TierManager.getInstance().resetFolders();
+ if (SubscriptionConfig.getInstance().getSubscriptionEnabled()) {
+ ConsensusSubscriptionSetupHandler.ensureNewRegionListenerRegistered();
+ }
// Active DataNode
active();
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 93f64aa0381..1c429cd97dc 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
@@ -670,7 +670,8 @@ public class SubscriptionBrokerAgent {
final RegionProgress fallbackCommittedRegionProgress,
final long tailStartSearchIndex,
final long initialRuntimeVersion,
- final boolean initialActive) {
+ final boolean initialActive,
+ final boolean replaceDetachedRetention) {
final ConsensusPrefetchingQueue queue =
getOrCreateBroker(
consumerGroupId,
@@ -692,7 +693,8 @@ public class SubscriptionBrokerAgent {
fallbackCommittedRegionProgress,
tailStartSearchIndex,
initialRuntimeVersion,
- initialActive);
+ initialActive,
+ replaceDetachedRetention);
prefetchingQueueCount.invalidate();
return queue;
}
@@ -824,7 +826,9 @@ public class SubscriptionBrokerAgent {
}
public void unbindConsensusPrefetchingQueue(
- final String consumerGroupId, final String topicName) {
+ final String consumerGroupId,
+ final String topicName,
+ final boolean retainProgressAfterUnsubscribe) {
final ConsensusSubscriptionBroker broker =
getConsensusBroker(consumerGroupId);
if (Objects.isNull(broker)) {
LOGGER.warn(
@@ -833,7 +837,7 @@ public class SubscriptionBrokerAgent {
consumerGroupId);
return;
}
- broker.unbindConsensusPrefetchingQueue(topicName);
+ broker.unbindConsensusPrefetchingQueue(topicName,
retainProgressAfterUnsubscribe);
prefetchingQueueCount.invalidate();
}
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 79f0c59ec7e..7917fa39a5c 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
@@ -118,6 +118,7 @@ public class SubscriptionConsumerAgent {
// if the creation time of consumer group meta on local agent is
inconsistent with meta from
// coordinator
if (metaInAgent.getCreationTime() !=
metaFromCoordinator.getCreationTime()) {
+ cleanupConsumerGroupSubscriptions(consumerGroupId, metaInAgent);
if (SubscriptionAgent.broker().isBrokerExist(consumerGroupId)) {
LOGGER.warn(
DataNodePipeMessages
@@ -242,6 +243,12 @@ public class SubscriptionConsumerAgent {
}
private void handleDropConsumerGroupInternal(final String consumerGroupId) {
+ final ConsumerGroupMeta consumerGroupMeta =
+ consumerGroupMetaKeeper.getConsumerGroupMeta(consumerGroupId);
+ if (Objects.nonNull(consumerGroupMeta)) {
+ cleanupConsumerGroupSubscriptions(consumerGroupId, consumerGroupMeta);
+ }
+
if (SubscriptionAgent.broker().isBrokerExist(consumerGroupId)) {
if (!SubscriptionAgent.broker().dropBroker(consumerGroupId)) {
final String exceptionMessage =
@@ -320,6 +327,22 @@ public class SubscriptionConsumerAgent {
private int referenceCount = 1;
}
+ private void cleanupConsumerGroupSubscriptions(
+ final String consumerGroupId, final ConsumerGroupMeta consumerGroupMeta)
{
+ final Set<String> consensusTopics = new LinkedHashSet<>();
+ for (final String topicName : consumerGroupMeta.getSubscribedTopicNames())
{
+ if (ConsensusSubscriptionSetupHandler.isConsensusBasedTopic(
+ topicName, consumerGroupMeta.visibleUnder(true))) {
+ consensusTopics.add(topicName);
+ } else {
+ SubscriptionAgent.broker().removePrefetchingQueue(consumerGroupId,
topicName);
+ }
+ }
+ ConsensusSubscriptionSetupHandler.teardownConsensusSubscriptions(
+ consumerGroupId, consensusTopics, false);
+
ConsensusSubscriptionSetupHandler.cleanupRetainedProgressForConsumerGroup(consumerGroupId);
+ }
+
public boolean isConsumerExisted(final String consumerGroupId, final String
consumerId) {
acquireReadLock();
try {
@@ -352,6 +375,25 @@ public class SubscriptionConsumerAgent {
}
}
+ public boolean containsConsumerGroup(final String consumerGroupId) {
+ acquireReadLock();
+ try {
+ return
consumerGroupMetaKeeper.containsConsumerGroupMeta(consumerGroupId);
+ } finally {
+ releaseReadLock();
+ }
+ }
+
+ public boolean isTopicSubscribedByConsumerGroup(
+ final String consumerGroupId, final String topicName) {
+ acquireReadLock();
+ try {
+ return
consumerGroupMetaKeeper.isTopicSubscribedByConsumerGroup(topicName,
consumerGroupId);
+ } finally {
+ releaseReadLock();
+ }
+ }
+
/**
* Get all active subscriptions: consumerGroupId → set of subscribed topic
names. Used by
* consensus subscription auto-binding when a new DataRegion is created.
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionRuntimeAgent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionRuntimeAgent.java
index e942453f7bd..2962f525e70 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionRuntimeAgent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionRuntimeAgent.java
@@ -23,6 +23,7 @@ import org.apache.iotdb.commons.exception.StartupException;
import org.apache.iotdb.commons.service.IService;
import org.apache.iotdb.commons.service.ServiceType;
import org.apache.iotdb.commons.subscription.config.SubscriptionConfig;
+import
org.apache.iotdb.db.subscription.broker.consensus.ConsensusSubscriptionSetupHandler;
import
org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutorManager;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -72,6 +73,7 @@ public class SubscriptionRuntimeAgent implements IService {
SubscriptionAgentLauncher.launchSubscriptionTopicAgent();
SubscriptionAgentLauncher.launchSubscriptionConsumerAgent();
+ ConsensusSubscriptionSetupHandler.restoreDetachedRetentions();
isShutdown.set(false);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
index 256c87e521d..f4cb98d823e 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
@@ -31,6 +31,7 @@ import
org.apache.iotdb.commons.subscription.meta.topic.TopicMetaKeeper;
import org.apache.iotdb.db.auth.AuthorityChecker;
import org.apache.iotdb.db.i18n.DataNodeMiscMessages;
import org.apache.iotdb.db.i18n.DataNodePipeMessages;
+import
org.apache.iotdb.db.subscription.broker.consensus.ConsensusSubscriptionSetupHandler;
import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaRespExceptionMessage;
import org.apache.iotdb.mpp.rpc.thrift.TTopicOwnerLeaseEntry;
import org.apache.iotdb.rpc.RpcUtils;
@@ -121,6 +122,8 @@ public class SubscriptionTopicAgent {
SubscriptionAgent.broker()
.refreshConsensusQueueOrderMode(
topicName, isTableModel,
metaFromCoordinator.getConfig().getOrderMode());
+ ConsensusSubscriptionSetupHandler.refreshDetachedRetentionsForTopic(
+ topicName, isTableModel, metaFromCoordinator.getConfig());
}
static boolean shouldRefreshColumnFilter(final TopicMeta oldMeta, final
TopicMeta newMeta) {
@@ -225,6 +228,8 @@ public class SubscriptionTopicAgent {
Objects.isNull(isTableModel)
? topicMetaKeeper.getTopicMeta(topicName)
: topicMetaKeeper.getTopicMeta(topicName, isTableModel);
+ ConsensusSubscriptionSetupHandler.cleanupRetainedProgressForDroppedTopic(
+ topicName, Objects.nonNull(topicMeta) ?
topicMeta.visibleUnderTableModel() : isTableModel);
if (Objects.isNull(isTableModel)) {
topicMetaKeeper.removeTopicMeta(topicName);
} else {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
index 2792dff39e2..cd7abf043cf 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
@@ -679,7 +679,8 @@ public class ConsensusSubscriptionBroker implements
ISubscriptionBroker {
final RegionProgress fallbackCommittedRegionProgress,
final long tailStartSearchIndex,
final long initialRuntimeVersion,
- final boolean initialActive) {
+ final boolean initialActive,
+ final boolean replaceDetachedRetention) {
synchronized (queueLifecycleLock) {
// Get or create the list of queues for this topic
final List<ConsensusPrefetchingQueue> queues =
@@ -713,7 +714,8 @@ public class ConsensusSubscriptionBroker implements
ISubscriptionBroker {
fallbackCommittedRegionProgress,
tailStartSearchIndex,
initialRuntimeVersion,
- initialActive);
+ initialActive,
+ replaceDetachedRetention);
queues.add(consensusQueue);
LOGGER.info(
DataNodePipeMessages
@@ -742,16 +744,16 @@ public class ConsensusSubscriptionBroker implements
ISubscriptionBroker {
}
}
- public void unbindConsensusPrefetchingQueue(final String topicName) {
- // Subscription setup teardown and rollback manage commit state after this
call returns. Let
- // queue close persist the current state, while avoiding an asynchronous
close deleting state
- // restored by a concurrent setup rollback.
- closeAndRemoveConsensusPrefetchingQueues(topicName, true, false);
+ public void unbindConsensusPrefetchingQueue(
+ final String topicName, final boolean retainProgressAfterUnsubscribe) {
+ // Subscription setup teardown and rollback manage commit state after this
call returns.
+ // Persist it on close so asynchronous cleanup cannot delete state
restored by a rollback.
+ closeAndRemoveConsensusPrefetchingQueues(topicName, true,
retainProgressAfterUnsubscribe);
}
@Override
public void unbind(final String topicName) {
- unbindConsensusPrefetchingQueue(topicName);
+ unbindConsensusPrefetchingQueue(topicName, false);
}
public int unbindByRegion(final ConsensusGroupId regionId) {
@@ -856,7 +858,9 @@ public class ConsensusSubscriptionBroker implements
ISubscriptionBroker {
}
private void closeAndRemoveConsensusPrefetchingQueues(
- final String topicName, final boolean warnIfMissing, final boolean
removeProgressAfterClose) {
+ final String topicName,
+ final boolean warnIfMissing,
+ final boolean retainProgressAfterUnsubscribe) {
final List<ConsensusPrefetchingQueue> queuesToClose;
synchronized (queueLifecycleLock) {
final List<ConsensusPrefetchingQueue> queues =
@@ -877,7 +881,7 @@ public class ConsensusSubscriptionBroker implements
ISubscriptionBroker {
}
for (final ConsensusPrefetchingQueue q : queuesToClose) {
- q.closeAsync(removeProgressAfterClose);
+ q.closeAsync(false, retainProgressAfterUnsubscribe);
}
LOGGER.info(
DataNodePipeMessages
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index a678646a735..c19420c130c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -617,6 +617,36 @@ public class ConsensusPrefetchingQueue {
final long tailStartSearchIndex,
final long initialRuntimeVersion,
final boolean initialActive) {
+ this(
+ consumerGroupId,
+ topicName,
+ orderMode,
+ consensusGroupId,
+ serverImpl,
+ retentionPolicy,
+ converter,
+ commitManager,
+ fallbackCommittedRegionProgress,
+ tailStartSearchIndex,
+ initialRuntimeVersion,
+ initialActive,
+ false);
+ }
+
+ public ConsensusPrefetchingQueue(
+ final String consumerGroupId,
+ final String topicName,
+ final String orderMode,
+ final ConsensusGroupId consensusGroupId,
+ final IoTConsensusServerImpl serverImpl,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final ConsensusLogToTabletConverter converter,
+ final ConsensusSubscriptionCommitManager commitManager,
+ final RegionProgress fallbackCommittedRegionProgress,
+ final long tailStartSearchIndex,
+ final long initialRuntimeVersion,
+ final boolean initialActive,
+ final boolean replaceDetachedRetention) {
this.consumerGroupId = consumerGroupId;
this.topicName = topicName;
this.consensusGroupId = consensusGroupId;
@@ -649,8 +679,17 @@ public class ConsensusPrefetchingQueue {
this::requestPrefetchForRealtimeEntry,
this::getRealtimeAdmissionRejectionReason,
this::recordRealtimeAdmissionRejection);
- serverImpl.registerSubscriptionQueue(
- pendingEntries, retentionPolicy,
this::getRequiredRetainedMinVersionId);
+ if (replaceDetachedRetention) {
+ serverImpl.replaceDetachedSubscriptionRetentionWithQueue(
+ ConsensusSubscriptionWalRetention.generateRetentionId(
+ consumerGroupId, topicName, consensusGroupId),
+ pendingEntries,
+ retentionPolicy,
+ this::getRequiredRetainedMinVersionId);
+ } else {
+ serverImpl.registerSubscriptionQueue(
+ pendingEntries, retentionPolicy,
this::getRequiredRetainedMinVersionId);
+ }
LOGGER.info(
DataNodePipeMessages
@@ -4191,10 +4230,17 @@ public class ConsensusPrefetchingQueue {
}
public void close(final boolean removeProgressAfterClose) {
+ close(removeProgressAfterClose, false);
+ }
+
+ public void close(
+ final boolean removeProgressAfterClose, final boolean
retainProgressAfterUnsubscribe) {
if (!lock.writeLock().tryLock()) {
if (beginClose()) {
CLOSE_CLEANUP_EXECUTOR.execute(
- () -> finishCloseAfterWriteLock(removeProgressAfterClose, true));
+ () ->
+ finishCloseAfterWriteLock(
+ removeProgressAfterClose, retainProgressAfterUnsubscribe,
true));
}
return;
}
@@ -4211,7 +4257,8 @@ public class ConsensusPrefetchingQueue {
}
try {
- finishCloseRequest(seekRequestToFail, removeProgressAfterClose, false);
+ finishCloseRequest(
+ seekRequestToFail, removeProgressAfterClose,
retainProgressAfterUnsubscribe, false);
} catch (final Exception e) {
handleCloseFailure(e);
}
@@ -4219,9 +4266,16 @@ public class ConsensusPrefetchingQueue {
/** Starts teardown without waiting for an active prefetch round or
consensus queue writes. */
public void closeAsync(final boolean removeProgressAfterClose) {
+ closeAsync(removeProgressAfterClose, false);
+ }
+
+ public void closeAsync(
+ final boolean removeProgressAfterClose, final boolean
retainProgressAfterUnsubscribe) {
if (beginClose()) {
CLOSE_CLEANUP_EXECUTOR.execute(
- () -> finishCloseAfterWriteLock(removeProgressAfterClose, true));
+ () ->
+ finishCloseAfterWriteLock(
+ removeProgressAfterClose, retainProgressAfterUnsubscribe,
true));
}
}
@@ -4238,7 +4292,9 @@ public class ConsensusPrefetchingQueue {
}
private void finishCloseAfterWriteLock(
- final boolean removeProgressAfterClose, final boolean backgroundCleanup)
{
+ final boolean removeProgressAfterClose,
+ final boolean retainProgressAfterUnsubscribe,
+ final boolean backgroundCleanup) {
try {
final PendingSeekRequest seekRequestToFail;
acquireWriteLock();
@@ -4248,7 +4304,11 @@ public class ConsensusPrefetchingQueue {
} finally {
releaseWriteLock();
}
- finishCloseRequest(seekRequestToFail, removeProgressAfterClose,
backgroundCleanup);
+ finishCloseRequest(
+ seekRequestToFail,
+ removeProgressAfterClose,
+ retainProgressAfterUnsubscribe,
+ backgroundCleanup);
} catch (final Exception e) {
handleCloseFailure(e);
}
@@ -4257,6 +4317,7 @@ public class ConsensusPrefetchingQueue {
private void finishCloseRequest(
final PendingSeekRequest seekRequestToFail,
final boolean removeProgressAfterClose,
+ final boolean retainProgressAfterUnsubscribe,
final boolean backgroundCleanup) {
final Pair<ConsensusSubscriptionPrefetchExecutor,
ConsensusPrefetchSubtask> prefetchBinding =
detachPrefetchSubtask();
@@ -4278,22 +4339,26 @@ public class ConsensusPrefetchingQueue {
&& Objects.nonNull(prefetchBinding.right)
&& prefetchBinding.right.isScheduledOrRunning()) {
CLOSE_CLEANUP_EXECUTOR.execute(
- () -> finishCloseInBackground(prefetchBinding,
removeProgressAfterClose));
+ () ->
+ finishCloseInBackground(
+ prefetchBinding, removeProgressAfterClose,
retainProgressAfterUnsubscribe));
return;
}
if (backgroundCleanup) {
- finishCloseInBackground(prefetchBinding, removeProgressAfterClose);
+ finishCloseInBackground(
+ prefetchBinding, removeProgressAfterClose,
retainProgressAfterUnsubscribe);
} else {
- finishClose(prefetchBinding, removeProgressAfterClose);
+ finishClose(prefetchBinding, removeProgressAfterClose,
retainProgressAfterUnsubscribe);
}
}
private void finishCloseInBackground(
final Pair<ConsensusSubscriptionPrefetchExecutor,
ConsensusPrefetchSubtask> prefetchBinding,
- final boolean removeProgressAfterClose) {
+ final boolean removeProgressAfterClose,
+ final boolean retainProgressAfterUnsubscribe) {
try {
- finishClose(prefetchBinding, removeProgressAfterClose);
+ finishClose(prefetchBinding, removeProgressAfterClose,
retainProgressAfterUnsubscribe);
} catch (final Exception e) {
handleCloseFailure(e);
}
@@ -4312,12 +4377,13 @@ public class ConsensusPrefetchingQueue {
private void finishClose(
final Pair<ConsensusSubscriptionPrefetchExecutor,
ConsensusPrefetchSubtask> prefetchBinding,
- final boolean removeProgressAfterClose) {
+ final boolean removeProgressAfterClose,
+ final boolean retainProgressAfterUnsubscribe) {
// Queue deregistration may wait for consensus-side WAL/index work.
Metadata callers use
// closeAsync so this cleanup cannot hold up the consumer-group metadata
push.
// Requests left in pendingEntries have not advanced subscription progress
and can be replayed
// from WAL if this queue is created again.
- deregisterPendingEntriesFromConsensusServer();
+
deregisterPendingEntriesFromConsensusServer(retainProgressAfterUnsubscribe);
if (Objects.nonNull(prefetchBinding.right)) {
prefetchBinding.right.awaitIdle();
@@ -4362,9 +4428,24 @@ public class ConsensusPrefetchingQueue {
}
}
- private void deregisterPendingEntriesFromConsensusServer() {
+ private void deregisterPendingEntriesFromConsensusServer(
+ final boolean retainProgressAfterUnsubscribe) {
try {
- serverImpl.deregisterSubscriptionQueue(pendingEntries);
+ if (retainProgressAfterUnsubscribe) {
+ serverImpl.replaceSubscriptionQueueWithDetachedRetention(
+ pendingEntries,
+ ConsensusSubscriptionWalRetention.generateRetentionId(
+ consumerGroupId, topicName, consensusGroupId),
+ retentionPolicy,
+
ConsensusSubscriptionWalRetention.createCommittedRetainedMinVersionIdSupplier(
+ consensusGroupId,
+ () -> consensusReqReader,
+ () ->
+ commitManager.getCommittedRegionProgress(
+ consumerGroupId, topicName, consensusGroupId)));
+ } else {
+ serverImpl.deregisterSubscriptionQueue(pendingEntries);
+ }
} catch (final Exception e) {
LOGGER.warn(
DataNodePipeMessages.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_ERROR_DURING_DEREGISTER_34C332E7,
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitManager.java
index 14e50ae44d0..ed49dda3f46 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitManager.java
@@ -68,6 +68,7 @@ import java.nio.file.StandardOpenOption;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -725,6 +726,46 @@ public class ConsensusSubscriptionCommitManager {
}
}
+ public void removeAllStatesForConsumerGroup(final String consumerGroupId) {
+ recoverAllTopicStatesIfNeeded();
+ final Set<String> topicNames = new HashSet<>();
+ for (final CommitStateKey stateKey : commitStateKeys.values()) {
+ if (Objects.equals(stateKey.consumerGroupId, consumerGroupId)) {
+ topicNames.add(stateKey.topicName);
+ }
+ }
+ for (final String topicName : topicNames) {
+ removeAllStatesForTopic(consumerGroupId, topicName);
+ }
+ }
+
+ public void removeAllStatesForRegion(final ConsensusGroupId regionId) {
+ final String regionIdStr = regionId.toString();
+ for (final PersistedStateKey stateKey : getPersistedStateKeys()) {
+ if (Objects.equals(stateKey.regionId, regionIdStr)) {
+ removeState(
+ stateKey.consumerGroupId,
+ stateKey.topicName,
+ ConsensusGroupId.Factory.createFromString(stateKey.regionId));
+ }
+ }
+ }
+
+ public List<PersistedStateKey> getPersistedStateKeys() {
+ recoverAllTopicStatesIfNeeded();
+ final List<PersistedStateKey> result = new java.util.ArrayList<>();
+ for (final CommitStateKey stateKey : commitStateKeys.values()) {
+ if (topicProgressIndexes
+ .getOrDefault(stateKey.topicFileKey, Collections.emptyMap())
+ .containsKey(stateKey.regionIdStr)) {
+ result.add(
+ new PersistedStateKey(
+ stateKey.consumerGroupId, stateKey.topicName,
stateKey.regionIdStr));
+ }
+ }
+ return Collections.unmodifiableList(result);
+ }
+
public void resetState(
final String consumerGroupId,
final String topicName,
@@ -1487,6 +1528,13 @@ public class ConsensusSubscriptionCommitManager {
this.consumerGroupId = consumerGroupId;
this.topicSnapshots = topicSnapshots;
}
+
+ Set<String> getRegionIds(final String topicName) {
+ final TopicSetupSnapshot topicSnapshot = topicSnapshots.get(topicName);
+ return Objects.isNull(topicSnapshot)
+ ? Collections.emptySet()
+ : Collections.unmodifiableSet(topicSnapshot.stateSnapshots.keySet());
+ }
}
private static final class TopicSetupSnapshot {
@@ -1559,6 +1607,34 @@ public class ConsensusSubscriptionCommitManager {
}
}
+ public static final class PersistedStateKey {
+
+ private final String consumerGroupId;
+
+ private final String topicName;
+
+ private final String regionId;
+
+ private PersistedStateKey(
+ final String consumerGroupId, final String topicName, final String
regionId) {
+ this.consumerGroupId = consumerGroupId;
+ this.topicName = topicName;
+ this.regionId = regionId;
+ }
+
+ public String getConsumerGroupId() {
+ return consumerGroupId;
+ }
+
+ public String getTopicName() {
+ return topicName;
+ }
+
+ public String getRegionId() {
+ return regionId;
+ }
+ }
+
private static final class TopicProgressSnapshot {
private final Map<String, ConsensusSubscriptionCommitState> states;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandler.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandler.java
index 87b3c66f698..92cfade03c2 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandler.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandler.java
@@ -119,6 +119,9 @@ public class ConsensusSubscriptionSetupHandler {
* region, and when a DataRegion is removed, all subscription queues are
properly cleaned up.
*/
public static void ensureNewRegionListenerRegistered() {
+ if (IoTConsensus.onPeerRecovering == null) {
+ IoTConsensus.onPeerRecovering =
ConsensusSubscriptionSetupHandler::onRecoveredRegionCreated;
+ }
if (IoTConsensus.onNewPeerCreated == null) {
IoTConsensus.onNewPeerCreated =
ConsensusSubscriptionSetupHandler::onNewRegionCreated;
LOGGER.info(
@@ -133,6 +136,37 @@ public class ConsensusSubscriptionSetupHandler {
}
}
+ private static void onRecoveredRegionCreated(
+ final ConsensusGroupId groupId, final IoTConsensusServerImpl serverImpl)
{
+ if (!(groupId instanceof DataRegionId)) {
+ return;
+ }
+ registerStartupDetachedRetentions(
+ groupId, serverImpl, ConsensusSubscriptionCommitManager.getInstance());
+ }
+
+ static void registerStartupDetachedRetentions(
+ final ConsensusGroupId groupId,
+ final IoTConsensusServerImpl serverImpl,
+ final ConsensusSubscriptionCommitManager commitManager) {
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
+ commitManager.getPersistedStateKeys()) {
+ if (!groupId.toString().equals(stateKey.getRegionId())) {
+ continue;
+ }
+ ConsensusSubscriptionWalRetention.registerDetached(
+ stateKey.getConsumerGroupId(),
+ stateKey.getTopicName(),
+ groupId,
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ stateKey.getTopicName(),
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ commitManager);
+ }
+ }
+
/**
* Callback invoked when a new DataRegion (IoTConsensusServerImpl) is
created locally. Queries
* existing subscription metadata to find all active consensus subscriptions
and binds prefetching
@@ -254,7 +288,8 @@ public class ConsensusSubscriptionSetupHandler {
committedRegionProgress,
tailStartSearchIndex,
initialRuntimeVersion,
- initialActive);
+ initialActive,
+ hasLocalPersistedState);
SubscriptionAgent.broker().applyRuntimeStateForRegion(groupId,
initialRuntimeState);
} catch (final Exception e) {
LOGGER.error(
@@ -287,6 +322,21 @@ public class ConsensusSubscriptionSetupHandler {
groupId);
try {
SubscriptionAgent.broker().unbindByRegion(groupId);
+ final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
+ if (dataRegionConsensus instanceof IoTConsensus) {
+ final IoTConsensusServerImpl serverImpl =
+ ((IoTConsensus) dataRegionConsensus).getImpl(groupId);
+ if (Objects.nonNull(serverImpl)) {
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey
stateKey :
+
ConsensusSubscriptionCommitManager.getInstance().getPersistedStateKeys()) {
+ if (groupId.toString().equals(stateKey.getRegionId())) {
+ ConsensusSubscriptionWalRetention.unregisterDetached(
+ stateKey.getConsumerGroupId(), stateKey.getTopicName(),
groupId, serverImpl);
+ }
+ }
+ }
+ }
+
ConsensusSubscriptionCommitManager.getInstance().removeAllStatesForRegion(groupId);
} catch (final Exception e) {
LOGGER.error(
DataNodePipeMessages
@@ -392,7 +442,8 @@ public class ConsensusSubscriptionSetupHandler {
RuntimeException rollbackFailure = null;
for (final String topicName : attemptedTopicNames) {
try {
-
SubscriptionAgent.broker().unbindConsensusPrefetchingQueue(consumerGroupId,
topicName);
+ SubscriptionAgent.broker()
+ .unbindConsensusPrefetchingQueue(consumerGroupId, topicName,
false);
} catch (final RuntimeException e) {
if (Objects.isNull(rollbackFailure)) {
rollbackFailure = e;
@@ -403,6 +454,8 @@ public class ConsensusSubscriptionSetupHandler {
}
try {
commitManager.restoreSetupSnapshot(setupSnapshot, attemptedTopicNames);
+ restoreStartupDetachedRetentions(
+ consumerGroupId, attemptedTopicNames, setupSnapshot, commitManager);
} catch (final RuntimeException e) {
if (Objects.isNull(rollbackFailure)) {
rollbackFailure = e;
@@ -415,6 +468,36 @@ public class ConsensusSubscriptionSetupHandler {
}
}
+ private static void restoreStartupDetachedRetentions(
+ final String consumerGroupId,
+ final Set<String> attemptedTopicNames,
+ final ConsensusSubscriptionCommitManager.SetupSnapshot setupSnapshot,
+ final ConsensusSubscriptionCommitManager commitManager) {
+ final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
+ if (!(dataRegionConsensus instanceof IoTConsensus)) {
+ return;
+ }
+ final IoTConsensus ioTConsensus = (IoTConsensus) dataRegionConsensus;
+ for (final String topicName : attemptedTopicNames) {
+ for (final String regionId : setupSnapshot.getRegionIds(topicName)) {
+ final ConsensusGroupId groupId =
ConsensusGroupId.Factory.createFromString(regionId);
+ final IoTConsensusServerImpl serverImpl =
ioTConsensus.getImpl(groupId);
+ if (Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.registerDetached(
+ consumerGroupId,
+ topicName,
+ groupId,
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ topicName,
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ commitManager);
+ }
+ }
+ }
+ }
+
static void setupConsensusTopics(
final String consumerGroupId,
final Set<String> topicNames,
@@ -604,7 +687,8 @@ public class ConsensusSubscriptionSetupHandler {
committedRegionProgress,
tailStartSearchIndex,
initialRuntimeVersion,
- initialActive);
+ initialActive,
+ hasLocalPersistedState);
commitManager.initializeStateFromTailProposal(
consumerGroupId, topicName, groupId,
queue.computeTailRegionProgress());
@@ -698,16 +782,20 @@ public class ConsensusSubscriptionSetupHandler {
private static long resolveRetentionValue(
final TopicConfig topicConfig, final String key, final long
defaultValue) {
- if (!topicConfig.hasAttribute(key)) {
+ final String rawValue =
+ topicConfig.getAttribute().entrySet().stream()
+ .filter(entry -> Objects.nonNull(entry.getKey()))
+ .filter(entry -> key.equalsIgnoreCase(entry.getKey()))
+ .map(Map.Entry::getValue)
+ .findFirst()
+ .orElse(null);
+ if (Objects.isNull(rawValue)) {
return normalizeRetentionValue(defaultValue);
}
- final long parsedValue =
Long.parseLong(topicConfig.getAttribute().get(key));
+ final long parsedValue = Long.parseLong(rawValue);
if (parsedValue == 0 || parsedValue <
SubscriptionWalRetentionPolicy.UNBOUNDED) {
throw new IllegalArgumentException(
- String.format(
- DataNodePipeMessages.PIPE_EXCEPTION_ILLEGAL_S_S_72D743AA,
- key,
- topicConfig.getAttribute().get(key)));
+
String.format(DataNodePipeMessages.PIPE_EXCEPTION_ILLEGAL_S_S_72D743AA, key,
rawValue));
}
return normalizeRetentionValue(parsedValue);
}
@@ -718,13 +806,32 @@ public class ConsensusSubscriptionSetupHandler {
public static void teardownConsensusSubscriptions(
final String consumerGroupId, final Set<String> topicNames) {
+ teardownConsensusSubscriptions(consumerGroupId, topicNames, true);
+ }
+
+ public static void teardownConsensusSubscriptions(
+ final String consumerGroupId,
+ final Set<String> topicNames,
+ final boolean retainConfiguredProgress) {
for (final String topicName : topicNames) {
try {
-
SubscriptionAgent.broker().unbindConsensusPrefetchingQueue(consumerGroupId,
topicName);
-
- // Clean up commit state for all regions of this topic
- ConsensusSubscriptionCommitManager.getInstance()
- .removeAllStatesForTopic(consumerGroupId, topicName);
+ final boolean isTableModel =
SubscriptionAgent.consumer().isTableModel(consumerGroupId);
+ final TopicConfig topicConfig =
+ SubscriptionAgent.topic()
+ .getTopicConfigs(Collections.singleton(topicName),
isTableModel)
+ .get(topicName);
+ final boolean retainProgressAfterUnsubscribe =
+ retainConfiguredProgress
+ && Objects.nonNull(topicConfig)
+ && topicConfig.isProgressRetainedAfterUnsubscribe();
+ SubscriptionAgent.broker()
+ .unbindConsensusPrefetchingQueue(
+ consumerGroupId, topicName, retainProgressAfterUnsubscribe);
+
+ if (!retainProgressAfterUnsubscribe) {
+ ConsensusSubscriptionCommitManager.getInstance()
+ .removeAllStatesForTopic(consumerGroupId, topicName);
+ }
LOGGER.info(
DataNodePipeMessages
@@ -742,6 +849,179 @@ public class ConsensusSubscriptionSetupHandler {
}
}
+ public static void restoreDetachedRetentions() {
+ final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
+ if (!(dataRegionConsensus instanceof IoTConsensus)) {
+ return;
+ }
+
+ ensureNewRegionListenerRegistered();
+ final IoTConsensus ioTConsensus = (IoTConsensus) dataRegionConsensus;
+ final ConsensusSubscriptionCommitManager commitManager =
+ ConsensusSubscriptionCommitManager.getInstance();
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
+ commitManager.getPersistedStateKeys()) {
+ try {
+ final String consumerGroupId = stateKey.getConsumerGroupId();
+ final String topicName = stateKey.getTopicName();
+ final ConsensusGroupId groupId =
+ ConsensusGroupId.Factory.createFromString(stateKey.getRegionId());
+
+ final IoTConsensusServerImpl serverImpl =
ioTConsensus.getImpl(groupId);
+ if
(!SubscriptionAgent.consumer().containsConsumerGroup(consumerGroupId)) {
+ if (Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.unregisterDetached(
+ consumerGroupId, topicName, groupId, serverImpl);
+ }
+ commitManager.removeState(consumerGroupId, topicName, groupId);
+ continue;
+ }
+
+ final boolean isTableModel =
SubscriptionAgent.consumer().isTableModel(consumerGroupId);
+ final TopicConfig topicConfig =
+ SubscriptionAgent.topic()
+ .getTopicConfigs(Collections.singleton(topicName),
isTableModel)
+ .get(topicName);
+ if (Objects.isNull(topicConfig)
+ || !topicConfig.isIncrementalMode()
+ || !topicConfig.isProgressRetainedAfterUnsubscribe()) {
+ if (Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.unregisterDetached(
+ consumerGroupId, topicName, groupId, serverImpl);
+ }
+ commitManager.removeState(consumerGroupId, topicName, groupId);
+ continue;
+ }
+ if (SubscriptionAgent.consumer()
+ .isTopicSubscribedByConsumerGroup(consumerGroupId, topicName)) {
+ continue;
+ }
+
+ if (Objects.isNull(serverImpl)) {
+ commitManager.removeState(consumerGroupId, topicName, groupId);
+ continue;
+ }
+ ConsensusSubscriptionWalRetention.registerDetached(
+ consumerGroupId,
+ topicName,
+ groupId,
+ serverImpl,
+ buildSubscriptionWalRetentionPolicy(topicName, topicConfig,
serverImpl),
+ commitManager);
+ } catch (final Exception e) {
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_FAILED_TO_RESTORE_DETACHED_CONSENSUS_SUBSCRIPTION_WAL_RETENTION_FOR_CONSUMER_GROUP_ARG_TOPIC_ARG_REGION_ARG_296129FE,
+ stateKey.getConsumerGroupId(),
+ stateKey.getTopicName(),
+ stateKey.getRegionId(),
+ e);
+ }
+ }
+ }
+
+ public static void cleanupRetainedProgressForDroppedTopic(
+ final String topicName, final Boolean isTableModel) {
+ final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
+ final IoTConsensus ioTConsensus =
+ dataRegionConsensus instanceof IoTConsensus ? (IoTConsensus)
dataRegionConsensus : null;
+ final ConsensusSubscriptionCommitManager commitManager =
+ ConsensusSubscriptionCommitManager.getInstance();
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
+ commitManager.getPersistedStateKeys()) {
+ if (!topicName.equals(stateKey.getTopicName())) {
+ continue;
+ }
+ final String consumerGroupId = stateKey.getConsumerGroupId();
+ if (Objects.nonNull(isTableModel)
+ &&
SubscriptionAgent.consumer().containsConsumerGroup(consumerGroupId)
+ && SubscriptionAgent.consumer().isTableModel(consumerGroupId) !=
isTableModel) {
+ continue;
+ }
+ final ConsensusGroupId groupId =
+ ConsensusGroupId.Factory.createFromString(stateKey.getRegionId());
+ if (Objects.nonNull(ioTConsensus)) {
+ final IoTConsensusServerImpl serverImpl =
ioTConsensus.getImpl(groupId);
+ if (Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.unregisterDetached(
+ consumerGroupId, topicName, groupId, serverImpl);
+ }
+ }
+ commitManager.removeState(consumerGroupId, topicName, groupId);
+ }
+ }
+
+ public static void cleanupRetainedProgressForConsumerGroup(final String
consumerGroupId) {
+ final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
+ final IoTConsensus ioTConsensus =
+ dataRegionConsensus instanceof IoTConsensus ? (IoTConsensus)
dataRegionConsensus : null;
+ final ConsensusSubscriptionCommitManager commitManager =
+ ConsensusSubscriptionCommitManager.getInstance();
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
+ commitManager.getPersistedStateKeys()) {
+ if (!consumerGroupId.equals(stateKey.getConsumerGroupId())) {
+ continue;
+ }
+ final ConsensusGroupId groupId =
+ ConsensusGroupId.Factory.createFromString(stateKey.getRegionId());
+ if (Objects.nonNull(ioTConsensus)) {
+ final IoTConsensusServerImpl serverImpl =
ioTConsensus.getImpl(groupId);
+ if (Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.unregisterDetached(
+ consumerGroupId, stateKey.getTopicName(), groupId, serverImpl);
+ }
+ }
+ }
+ commitManager.removeAllStatesForConsumerGroup(consumerGroupId);
+ }
+
+ public static void refreshDetachedRetentionsForTopic(
+ final String topicName, final boolean isTableModel, final TopicConfig
topicConfig) {
+ final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
+ final IoTConsensus ioTConsensus =
+ dataRegionConsensus instanceof IoTConsensus ? (IoTConsensus)
dataRegionConsensus : null;
+ final ConsensusSubscriptionCommitManager commitManager =
+ ConsensusSubscriptionCommitManager.getInstance();
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
+ commitManager.getPersistedStateKeys()) {
+ if (!topicName.equals(stateKey.getTopicName())) {
+ continue;
+ }
+ final String consumerGroupId = stateKey.getConsumerGroupId();
+ if
(!SubscriptionAgent.consumer().containsConsumerGroup(consumerGroupId)) {
+ continue;
+ }
+ if (SubscriptionAgent.consumer().isTableModel(consumerGroupId) !=
isTableModel) {
+ continue;
+ }
+ if (SubscriptionAgent.consumer()
+ .isTopicSubscribedByConsumerGroup(consumerGroupId, topicName)) {
+ continue;
+ }
+ final ConsensusGroupId groupId =
+ ConsensusGroupId.Factory.createFromString(stateKey.getRegionId());
+ final IoTConsensusServerImpl serverImpl =
+ Objects.nonNull(ioTConsensus) ? ioTConsensus.getImpl(groupId) : null;
+ if (topicConfig.isIncrementalMode()
+ && topicConfig.isProgressRetainedAfterUnsubscribe()
+ && Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.registerDetached(
+ consumerGroupId,
+ topicName,
+ groupId,
+ serverImpl,
+ buildSubscriptionWalRetentionPolicy(topicName, topicConfig,
serverImpl),
+ commitManager);
+ } else {
+ if (Objects.nonNull(serverImpl)) {
+ ConsensusSubscriptionWalRetention.unregisterDetached(
+ consumerGroupId, topicName, groupId, serverImpl);
+ }
+ commitManager.removeState(consumerGroupId, topicName, groupId);
+ }
+ }
+ }
+
public static void handleNewSubscriptions(
final String consumerGroupId, final Set<String> newTopicNames) {
if (newTopicNames == null || newTopicNames.isEmpty()) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionWalRetention.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionWalRetention.java
new file mode 100644
index 00000000000..8e3042d5e95
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionWalRetention.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.subscription.broker.consensus;
+
+import org.apache.iotdb.commons.consensus.ConsensusGroupId;
+import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl;
+import org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy;
+import org.apache.iotdb.consensus.iot.log.ConsensusReqReader;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.ProgressWALReader;
+import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
+import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterId;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterProgress;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.LongSupplier;
+import java.util.function.Supplier;
+
+final class ConsensusSubscriptionWalRetention {
+
+ private ConsensusSubscriptionWalRetention() {}
+
+ static String generateRetentionId(
+ final String consumerGroupId, final String topicName, final
ConsensusGroupId regionId) {
+ return consumerGroupId + "\u0000" + topicName + "\u0000" + regionId;
+ }
+
+ static boolean isProgressMonotonic(
+ final RegionProgress previousProgress, final RegionProgress
currentProgress) {
+ if (Objects.isNull(previousProgress)) {
+ return true;
+ }
+ if (Objects.isNull(currentProgress)) {
+ return false;
+ }
+ for (final Map.Entry<WriterId, WriterProgress> entry :
+ previousProgress.getWriterPositions().entrySet()) {
+ final WriterProgress previous = entry.getValue();
+ final WriterProgress current =
currentProgress.getWriterPositions().get(entry.getKey());
+ if (Objects.isNull(current)
+ || current.getPhysicalTime() < previous.getPhysicalTime()
+ || (current.getPhysicalTime() == previous.getPhysicalTime()
+ && current.getLocalSeq() < previous.getLocalSeq())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static final class RetainedWalVersionCalculator {
+
+ private final ConsensusGroupId consensusGroupId;
+ private ConsensusReqReader lastConsensusReqReader;
+ private RegionProgress lastCommittedRegionProgress;
+ private long lastCoveredWalVersionId = -1L;
+
+ private RetainedWalVersionCalculator(final ConsensusGroupId
consensusGroupId) {
+ this.consensusGroupId = consensusGroupId;
+ }
+
+ private synchronized long compute(
+ final ConsensusReqReader consensusReqReader, final RegionProgress
committedRegionProgress) {
+ if (lastConsensusReqReader != consensusReqReader
+ || !isProgressMonotonic(lastCommittedRegionProgress,
committedRegionProgress)) {
+ lastCoveredWalVersionId = -1L;
+ }
+ lastConsensusReqReader = consensusReqReader;
+ lastCommittedRegionProgress = committedRegionProgress;
+ if (!(consensusReqReader instanceof WALNode)) {
+ return 0L;
+ }
+
+ final WALNode walNode = (WALNode) consensusReqReader;
+ final long currentWalVersion = walNode.getCurrentWALFileVersion();
+ if (currentWalVersion <= lastCoveredWalVersionId) {
+ lastCoveredWalVersionId = -1L;
+ }
+ final File[] walFiles = walNode.getSortedWalFilesSnapshot();
+ if (Objects.isNull(walFiles) || walFiles.length == 0) {
+ return Math.max(0L, currentWalVersion);
+ }
+
+ for (final File walFile : walFiles) {
+ final long versionId = WALFileUtils.parseVersionId(walFile.getName());
+ if (versionId >= currentWalVersion) {
+ return Math.max(0L, currentWalVersion);
+ }
+ if (versionId <= lastCoveredWalVersionId
+ || ProgressWALIterator.isHeaderOnlyWalFile(walFile)) {
+ continue;
+ }
+
+ try (final ProgressWALReader reader = new ProgressWALReader(walFile)) {
+ if (!ConsensusPrefetchingQueue.WalFileCommitRequirement.fromMetadata(
+ consensusGroupId.toString(), reader.getMetaData())
+ .isCoveredBy(committedRegionProgress)) {
+ return versionId;
+ }
+ lastCoveredWalVersionId = versionId;
+ } catch (final IOException e) {
+ return versionId;
+ }
+ }
+ return Math.max(0L, currentWalVersion);
+ }
+ }
+
+ static LongSupplier createCommittedRetainedMinVersionIdSupplier(
+ final ConsensusGroupId consensusGroupId,
+ final Supplier<ConsensusReqReader> consensusReqReaderSupplier,
+ final Supplier<RegionProgress> committedRegionProgressSupplier) {
+ final RetainedWalVersionCalculator calculator =
+ new RetainedWalVersionCalculator(consensusGroupId);
+ return () ->
+ calculator.compute(consensusReqReaderSupplier.get(),
committedRegionProgressSupplier.get());
+ }
+
+ static void registerDetached(
+ final String consumerGroupId,
+ final String topicName,
+ final ConsensusGroupId regionId,
+ final IoTConsensusServerImpl serverImpl,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final ConsensusSubscriptionCommitManager commitManager) {
+ serverImpl.registerDetachedSubscriptionRetention(
+ generateRetentionId(consumerGroupId, topicName, regionId),
+ retentionPolicy,
+ createCommittedRetainedMinVersionIdSupplier(
+ regionId,
+ serverImpl::getConsensusReqReader,
+ () -> commitManager.getCommittedRegionProgress(consumerGroupId,
topicName, regionId)));
+ }
+
+ static void unregisterDetached(
+ final String consumerGroupId,
+ final String topicName,
+ final ConsensusGroupId regionId,
+ final IoTConsensusServerImpl serverImpl) {
+ serverImpl.deregisterDetachedSubscriptionRetention(
+ generateRetentionId(consumerGroupId, topicName, regionId));
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index 7f9c4139040..c8bd2734e91 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -269,6 +269,16 @@ public class ConsensusPrefetchingQueueTest {
@Test
public void testCloseDetachesBlockedConsensusQueueDeregistration() throws
Exception {
+ assertCloseDetachesBlockedConsensusRegistration(false);
+ }
+
+ @Test
+ public void testRetainingCloseDetachesBlockedConsensusQueueReplacement()
throws Exception {
+ assertCloseDetachesBlockedConsensusRegistration(true);
+ }
+
+ private void assertCloseDetachesBlockedConsensusRegistration(
+ final boolean retainProgressAfterUnsubscribe) throws Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
final File systemDir =
temporaryFolder.newFolder("async-prefetch-deregister");
final CountDownLatch deregistrationStarted = new CountDownLatch(1);
@@ -279,14 +289,19 @@ public class ConsensusPrefetchingQueueTest {
final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(reader);
when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
- doAnswer(
- invocation -> {
- deregistrationStarted.countDown();
- releaseDeregistration.await(TEST_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
- return null;
- })
- .when(serverImpl)
- .deregisterSubscriptionQueue(any());
+ final IoTConsensusServerImpl blockedServer =
+ doAnswer(
+ invocation -> {
+ deregistrationStarted.countDown();
+ releaseDeregistration.await(TEST_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
+ return null;
+ })
+ .when(serverImpl);
+ if (retainProgressAfterUnsubscribe) {
+ blockedServer.replaceSubscriptionQueueWithDetachedRetention(any(),
any(), any(), any());
+ } else {
+ blockedServer.deregisterSubscriptionQueue(any());
+ }
final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
when(converter.getDatabaseName()).thenReturn("db");
@@ -318,7 +333,7 @@ public class ConsensusPrefetchingQueueTest {
setPrefetchBinding(queue, prefetchExecutor, subtask);
final long closeStartNanos = System.nanoTime();
- queue.closeAsync(false);
+ queue.closeAsync(false, retainProgressAfterUnsubscribe);
final long closeElapsedMillis =
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - closeStartNanos);
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitStateTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitStateTest.java
index 24778e2c175..9734e19593f 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitStateTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionCommitStateTest.java
@@ -564,6 +564,41 @@ public class ConsensusSubscriptionCommitStateTest {
}
}
+ @Test
+ public void testPersistedStateKeysAndRegionCleanup() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir = temporaryFolder.newFolder("persistedStateKeys");
+ try {
+ final ConsensusSubscriptionCommitManager manager =
newCommitManager(systemDir);
+ final DataRegionId firstRegion = new DataRegionId(31);
+ final DataRegionId secondRegion = new DataRegionId(32);
+ manager.receiveProgressBroadcast(
+ "cg",
+ "topic",
+ firstRegion.toString(),
+ new WriterId(firstRegion.toString(), 7),
+ new WriterProgress(100L, 1L));
+ manager.receiveProgressBroadcast(
+ "cg",
+ "topic",
+ secondRegion.toString(),
+ new WriterId(secondRegion.toString(), 7),
+ new WriterProgress(200L, 2L));
+
+ assertEquals(2, manager.getPersistedStateKeys().size());
+ manager.removeAllStatesForRegion(firstRegion);
+
+ assertFalse(manager.hasPersistedState("cg", "topic", firstRegion));
+ assertTrue(manager.hasPersistedState("cg", "topic", secondRegion));
+ assertEquals(1, manager.getPersistedStateKeys().size());
+
+ manager.removeAllStatesForConsumerGroup("cg");
+ assertTrue(manager.getPersistedStateKeys().isEmpty());
+ } finally {
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
@Test
public void testProgressFileNameEncodesForbiddenTopicComponents() throws
Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandlerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandlerTest.java
index 30cbd487668..9401fc3e4af 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandlerTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionSetupHandlerTest.java
@@ -21,10 +21,14 @@ package org.apache.iotdb.db.subscription.broker.consensus;
import org.apache.iotdb.commons.consensus.DataRegionId;
import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
+import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl;
+import org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.rpc.subscription.config.TopicConfig;
import org.apache.iotdb.rpc.subscription.config.TopicConstant;
import org.apache.iotdb.rpc.subscription.exception.SubscriptionException;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterId;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterProgress;
import org.junit.Rule;
import org.junit.Test;
@@ -38,12 +42,18 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.LongSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
public class ConsensusSubscriptionSetupHandlerTest {
@@ -143,6 +153,43 @@ public class ConsensusSubscriptionSetupHandlerTest {
}
}
+ @Test
+ public void testStartupDetachedRetentionProtectsPersistedProgress() throws
Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir =
temporaryFolder.newFolder("startupDetachedRetention");
+ try {
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(systemDir.getAbsolutePath());
+ final ConsensusSubscriptionCommitManager commitManager =
+ new ConsensusSubscriptionCommitManager(
+ (consumerGroupId, topicName, regionId) ->
+
ConsensusSubscriptionCommitManager.ConfigNodeProgressQueryResult.absent());
+ final DataRegionId regionId = new DataRegionId(41);
+ commitManager.receiveProgressBroadcast(
+ "consumerGroup",
+ "topic",
+ regionId.toString(),
+ new WriterId(regionId.toString(), 7),
+ new WriterProgress(100L, 1L));
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+
+ ConsensusSubscriptionSetupHandler.registerStartupDetachedRetentions(
+ regionId, serverImpl, commitManager);
+
+ verify(serverImpl)
+ .registerDetachedSubscriptionRetention(
+ eq(
+ ConsensusSubscriptionWalRetention.generateRetentionId(
+ "consumerGroup", "topic", regionId)),
+ argThat(
+ policy ->
+ policy.getRetentionBytes() ==
SubscriptionWalRetentionPolicy.UNBOUNDED
+ && policy.getRetentionMs() ==
SubscriptionWalRetentionPolicy.UNBOUNDED),
+ any(LongSupplier.class));
+ } finally {
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
@Test
public void testAuditDatabaseNeverMatchesTopic() {
final Map<String, String> tableTopicAttributes = new HashMap<>();
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionWalRetentionTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionWalRetentionTest.java
new file mode 100644
index 00000000000..18312370782
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusSubscriptionWalRetentionTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.subscription.broker.consensus;
+
+import org.apache.iotdb.commons.consensus.DataRegionId;
+import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl;
+import org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy;
+import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
+import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterId;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterProgress;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.function.LongSupplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ConsensusSubscriptionWalRetentionTest {
+
+ private static final WriterId WRITER_ID = new WriterId("region", 7);
+
+ @Test
+ public void testProgressRegressionInvalidatesCoveredWalVersion() {
+ final RegionProgress previous = progress(100L, 2L);
+
+ assertTrue(ConsensusSubscriptionWalRetention.isProgressMonotonic(null,
previous));
+ assertTrue(ConsensusSubscriptionWalRetention.isProgressMonotonic(previous,
previous));
+ assertTrue(ConsensusSubscriptionWalRetention.isProgressMonotonic(previous,
progress(100L, 3L)));
+ assertTrue(ConsensusSubscriptionWalRetention.isProgressMonotonic(previous,
progress(101L, 0L)));
+ assertFalse(
+ ConsensusSubscriptionWalRetention.isProgressMonotonic(previous,
progress(100L, 1L)));
+
assertFalse(ConsensusSubscriptionWalRetention.isProgressMonotonic(previous,
progress(99L, 3L)));
+ assertFalse(
+ ConsensusSubscriptionWalRetention.isProgressMonotonic(
+ previous, new RegionProgress(Collections.emptyMap())));
+
assertFalse(ConsensusSubscriptionWalRetention.isProgressMonotonic(previous,
null));
+ }
+
+ @Test
+ public void testDetachedRetentionUsesSortedWalSnapshot() {
+ final DataRegionId regionId = new DataRegionId(1);
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ final WALNode walNode = mock(WALNode.class);
+ final ConsensusSubscriptionCommitManager commitManager =
+ mock(ConsensusSubscriptionCommitManager.class);
+ final SubscriptionWalRetentionPolicy retentionPolicy =
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED);
+ when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
+ when(walNode.getCurrentWALFileVersion()).thenReturn(7L);
+ when(walNode.getSortedWalFilesSnapshot()).thenReturn(new File[0]);
+ when(commitManager.getCommittedRegionProgress("group", "topic", regionId))
+ .thenReturn(new RegionProgress(Collections.emptyMap()));
+
+ ConsensusSubscriptionWalRetention.registerDetached(
+ "group", "topic", regionId, serverImpl, retentionPolicy,
commitManager);
+
+ final ArgumentCaptor<LongSupplier> supplierCaptor =
ArgumentCaptor.forClass(LongSupplier.class);
+ verify(serverImpl)
+ .registerDetachedSubscriptionRetention(
+ eq(ConsensusSubscriptionWalRetention.generateRetentionId("group",
"topic", regionId)),
+ same(retentionPolicy),
+ supplierCaptor.capture());
+ assertEquals(7L, supplierCaptor.getValue().getAsLong());
+ verify(walNode).getSortedWalFilesSnapshot();
+ }
+
+ private static RegionProgress progress(final long physicalTime, final long
localSeq) {
+ return new RegionProgress(
+ Collections.singletonMap(WRITER_ID, new WriterProgress(physicalTime,
localSeq)));
+ }
+}
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 2b291311fab..a0b16d9b7c1 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
@@ -116,6 +116,21 @@ public class CommitProgressKeeper {
&& isValidRegionAndDataNodeProgressKey(key,
legacyTopicKeyPrefix)));
}
+ public synchronized void removeConsumerGroupProgress(final String
consumerGroupId) {
+ final String versionedConsumerGroupKeyPrefix =
+ VERSIONED_KEY_PREFIX + encodeKeyComponent(consumerGroupId) +
KEY_COMPONENT_SEPARATOR;
+ final String consumerGroupString = String.valueOf(consumerGroupId);
+ final String legacyConsumerGroupKeyPrefix =
+ consumerGroupString.contains(KEY_SEPARATOR) ? null :
consumerGroupString + KEY_SEPARATOR;
+ regionProgressMap
+ .keySet()
+ .removeIf(
+ key ->
+ key.startsWith(versionedConsumerGroupKeyPrefix)
+ || (legacyConsumerGroupKeyPrefix != null
+ && key.startsWith(legacyConsumerGroupKeyPrefix)));
+ }
+
private static boolean isValidRegionAndDataNodeProgressKey(
final String key, final String topicKeyPrefix) {
if (!key.startsWith(topicKeyPrefix)) {
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeperTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeperTest.java
index ad040b4c8eb..7f18e4875d1 100644
---
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeperTest.java
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/meta/consumer/CommitProgressKeeperTest.java
@@ -50,6 +50,7 @@ public class CommitProgressKeeperTest {
@Test
public void testStateAccessUsesOneInstanceMonitor() throws Exception {
assertSynchronized("removeTopicProgress", String.class, String.class);
+ assertSynchronized("removeConsumerGroupProgress", String.class);
assertSynchronized("updateRegionProgress", String.class, ByteBuffer.class);
assertSynchronized("getRegionProgress", String.class);
assertSynchronized("getAllRegionProgress");
@@ -116,6 +117,24 @@ public class CommitProgressKeeperTest {
assertNotNull(keeper.getRegionProgress(masqueradingLegacyKey));
}
+ @Test
+ public void testRemoveConsumerGroupProgressRemovesAllTopics() {
+ final CommitProgressKeeper keeper = new CommitProgressKeeper();
+ final ByteBuffer progress = ByteBuffer.wrap(new byte[] {1});
+ final String firstTopicKey = CommitProgressKeeper.generateKey("cg",
"topic1", "1_1", 1);
+ final String secondTopicKey = CommitProgressKeeper.generateLegacyKey("cg",
"topic2", "1_2", 2);
+ final String otherGroupKey = CommitProgressKeeper.generateKey("other",
"topic1", "1_1", 1);
+ keeper.updateRegionProgress(firstTopicKey, progress);
+ keeper.updateRegionProgress(secondTopicKey, progress);
+ keeper.updateRegionProgress(otherGroupKey, progress);
+
+ keeper.removeConsumerGroupProgress("cg");
+
+ assertNull(keeper.getRegionProgress(firstTopicKey));
+ assertNull(keeper.getRegionProgress(secondTopicKey));
+ assertNotNull(keeper.getRegionProgress(otherGroupKey));
+ }
+
@Test
public void testUpdateAndReplaceAllUseDefensiveCopies() throws Exception {
final CommitProgressKeeper keeper = new CommitProgressKeeper();