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 9b807abb93f [Bugfix] Preserve retained subscription progress after the
last consumer closes (#18894)
9b807abb93f is described below
commit 9b807abb93f06ae1e1339dd41d990bcad93e897e
Author: Caideyipi <[email protected]>
AuthorDate: Fri Oct 16 10:30:42 2026 +0800
[Bugfix] Preserve retained subscription progress after the last consumer
closes (#18894)
* [Bugfix] Preserve retained subscription progress after the last consumer
closes
* Fix detached retention queue capture
---
.../IoTDBConsensusSubscriptionBasicTableIT.java | 60 +++++++++++++
.../persistence/subscription/SubscriptionInfo.java | 45 +++++++---
.../subscription/SubscriptionInfoTest.java | 99 ++++++++++++++++++++++
.../consensus/iot/IoTConsensusServerImpl.java | 15 +++-
.../subscription/SubscriptionQueueRegistry.java | 46 ++++++++--
.../SubscriptionQueueRegistryTest.java | 46 ++++++++++
.../agent/SubscriptionConsumerAgent.java | 44 +++++++---
.../consensus/ConsensusPrefetchingQueue.java | 22 +++--
.../ConsensusSubscriptionSetupHandler.java | 36 +++++++-
.../consensus/ConsensusPrefetchingQueueTest.java | 75 +++++++++++++++-
.../meta/consumer/CommitProgressKeeper.java | 68 ++++++++++++---
.../meta/consumer/ConsumerGroupMeta.java | 37 +++++---
.../consumer/ConsumerGroupDeSerTest.java | 39 +++++++++
.../meta/consumer/CommitProgressKeeperTest.java | 26 ++++++
14 files changed, 590 insertions(+), 68 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionBasicTableIT.java
b/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionBasicTableIT.java
index 1a64e70d4fd..aeb1850882e 100644
---
a/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionBasicTableIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/subscription/it/consensus/local/tablemodel/IoTDBConsensusSubscriptionBasicTableIT.java
@@ -19,8 +19,12 @@
package org.apache.iotdb.subscription.it.consensus.local.tablemodel;
+import org.apache.iotdb.it.env.EnvFactory;
import org.apache.iotdb.it.framework.IoTDBTestRunner;
import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+import org.apache.iotdb.rpc.subscription.config.TopicConstant;
+import org.apache.iotdb.session.subscription.ISubscriptionTableSession;
+import org.apache.iotdb.session.subscription.SubscriptionTableSessionBuilder;
import
org.apache.iotdb.session.subscription.consumer.table.SubscriptionTablePullConsumer;
import
org.apache.iotdb.subscription.it.consensus.local.AbstractSubscriptionConsensusLocalIT;
@@ -31,12 +35,68 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.LinkedHashSet;
+import java.util.Properties;
import java.util.Set;
@RunWith(IoTDBTestRunner.class)
@Category({LocalStandaloneIT.class})
public class IoTDBConsensusSubscriptionBasicTableIT extends
AbstractSubscriptionConsensusLocalIT {
+ @Test
+ public void testLastConsumerCloseRetainsProgress() throws Exception {
+ for (final boolean unsubscribeFirst : new boolean[] {false, true}) {
+ final ConsensusSubscriptionTableITSupport.TestIdentifiers ids =
+
ConsensusSubscriptionTableITSupport.newIdentifiers("retained_last_close");
+ SubscriptionTablePullConsumer consumer = null;
+ try {
+ ConsensusSubscriptionTableITSupport.bootstrapDatabaseAndTable(
+ ids.getDatabase(), "t1",
ConsensusSubscriptionTableITSupport.DEFAULT_TABLE_SCHEMA);
+ ConsensusSubscriptionTableITSupport.createConsensusTopic(
+ ids.getTopic(), ids.getDatabase(), "t1");
+ try (final ISubscriptionTableSession session =
+ new SubscriptionTableSessionBuilder()
+ .host(EnvFactory.getEnv().getIP())
+ .port(Integer.parseInt(EnvFactory.getEnv().getPort()))
+ .build()) {
+ session.open();
+ final Properties config = new Properties();
+
config.setProperty(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY, "true");
+ session.alterTopic(ids.getTopic(), config);
+ }
+ consumer =
+ ConsensusSubscriptionTableITSupport.createConsumer(
+ ids.getConsumerId(), ids.getConsumerGroupId());
+ consumer.subscribe(ids.getTopic());
+ final Set<String> firstRows =
+ ConsensusSubscriptionTableITSupport.insertRows(ids.getDatabase(),
"t1", 100L, 10, true);
+ ConsensusSubscriptionTableITSupport.assertExactRowKeys(
+ firstRows,
+ ConsensusSubscriptionTableITSupport.pollAndCommitUntilAtLeast(
+ consumer, firstRows.size(), 40));
+ if (unsubscribeFirst) {
+ consumer.unsubscribe(ids.getTopic());
+ }
+ consumer.close();
+ consumer = null;
+
+ final Set<String> offlineRows =
+ ConsensusSubscriptionTableITSupport.insertRows(ids.getDatabase(),
"t1", 200L, 10, true);
+ consumer =
+ ConsensusSubscriptionTableITSupport.createConsumer(
+ ids.consumer("replacement"), ids.getConsumerGroupId());
+ consumer.subscribe(ids.getTopic());
+ ConsensusSubscriptionTableITSupport.assertExactRowKeys(
+ offlineRows,
+ ConsensusSubscriptionTableITSupport.pollAndCommitUntilAtLeast(
+ consumer, offlineRows.size(), 40));
+ ConsensusSubscriptionTableITSupport.assertNoMoreMessages(
+ consumer, 3, Duration.ofMillis(500));
+ } finally {
+ ConsensusSubscriptionTableITSupport.cleanup(consumer, ids.getTopic(),
ids.getDatabase());
+ }
+ }
+ }
+
@Test
public void testRealtimeOnlyAfterSubscribe() throws Exception {
final ConsensusSubscriptionTableITSupport.TestIdentifiers ids =
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 58fe0a10690..c0b6d10e848 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
@@ -929,6 +929,7 @@ public class SubscriptionInfo implements SnapshotProcessor {
plan.getTopicMeta().getTopicName())) {
commitProgressKeeper.removeTopicProgress(
consumerGroupMeta.getConsumerGroupId(),
plan.getTopicMeta().getTopicName());
+ removeEmptyConsumerGroupWithoutProgress(consumerGroupMeta);
}
}
}
@@ -971,6 +972,7 @@ public class SubscriptionInfo implements SnapshotProcessor {
if (!plan.isTableModelSet() ||
consumerGroupMeta.visibleUnder(plan.isTableModel())) {
commitProgressKeeper.removeTopicProgress(
consumerGroupMeta.getConsumerGroupId(), plan.getTopicName());
+ removeEmptyConsumerGroupWithoutProgress(consumerGroupMeta);
}
}
if (plan.isTableModelSet()) {
@@ -1146,24 +1148,32 @@ public class SubscriptionInfo implements
SnapshotProcessor {
? consumerGroupMetaKeeper.getConsumerGroupMeta(consumerGroupId)
: null;
if (Objects.nonNull(currentConsumerGroupMeta)) {
+ 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);
+ }
+ });
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);
- }
- });
+ final Set<String> retainedTopicNames = new HashSet<>();
+ for (final TopicMeta topicMeta :
topicMetaKeeper.getAllTopicMeta()) {
+ if (topicMeta.visibleUnderTableModel() ==
currentConsumerGroupMeta.visibleUnder(true)
+ &&
topicMeta.getConfig().isProgressRetainedAfterUnsubscribe()) {
+ retainedTopicNames.add(topicMeta.getTopicName());
+ }
+ }
+ commitProgressKeeper.removeConsumerGroupProgressExceptTopics(
+ consumerGroupId, retainedTopicNames);
}
}
consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupId);
- if (!consumerGroupMeta.isEmpty()) {
+ if (!consumerGroupMeta.isEmpty()
+ || commitProgressKeeper.hasConsumerGroupProgress(consumerGroupId))
{
consumerGroupMetaKeeper.addConsumerGroupMeta(consumerGroupId,
consumerGroupMeta);
}
}
@@ -1173,6 +1183,13 @@ public class SubscriptionInfo implements
SnapshotProcessor {
}
}
+ private void removeEmptyConsumerGroupWithoutProgress(final ConsumerGroupMeta
consumerGroupMeta) {
+ if (consumerGroupMeta.isEmpty()
+ &&
!commitProgressKeeper.hasConsumerGroupProgress(consumerGroupMeta.getConsumerGroupId()))
{
+
consumerGroupMetaKeeper.removeConsumerGroupMeta(consumerGroupMeta.getConsumerGroupId());
+ }
+ }
+
public TSStatus
handleConsumerGroupMetaChanges(ConsumerGroupHandleMetaChangePlan plan) {
acquireWriteLock();
try {
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/subscription/SubscriptionInfoTest.java
index 087cb5868f5..35cd29dc617 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
@@ -34,10 +34,13 @@ import
org.apache.iotdb.confignode.rpc.thrift.TShowTopicInfo;
import org.apache.iotdb.confignode.rpc.thrift.TShowTopicResp;
import org.apache.iotdb.mpp.rpc.thrift.TTopicOwnerLeaseEntry;
import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
import org.apache.iotdb.rpc.subscription.config.TopicConstant;
import org.junit.Assert;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import java.nio.ByteBuffer;
import java.util.Collections;
@@ -49,6 +52,102 @@ import java.util.UUID;
public class SubscriptionInfoTest {
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void testLastConsumerClosePreservesConfiguredProgressAcrossSnapshot()
throws Exception {
+ for (final boolean tableModel : new boolean[] {false, true}) {
+ for (final boolean unsubscribeFirst : new boolean[] {false, true}) {
+ assertLastConsumerClosePreservesProgress(tableModel, unsubscribeFirst);
+ }
+ }
+ }
+
+ private void assertLastConsumerClosePreservesProgress(
+ final boolean tableModel, final boolean unsubscribeFirst) throws
Exception {
+ final SubscriptionInfo info = new SubscriptionInfo();
+ final Map<String, String> attributes = new HashMap<>();
+ attributes.put(TopicConstant.MODE_KEY,
TopicConstant.MODE_INCREMENTAL_VALUE);
+ attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"true");
+ attributes.put(SystemConstant.SQL_DIALECT_KEY, tableModel ? "table" :
"tree");
+ info.createTopic(new CreateTopicPlan(new TopicMeta("kept", 1L,
attributes)));
+ final Map<String, String> discardedAttributes = new HashMap<>(attributes);
+
discardedAttributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"false");
+ info.createTopic(new CreateTopicPlan(new TopicMeta("discarded", 1L,
discardedAttributes)));
+ final ConsumerGroupMeta group =
+ new ConsumerGroupMeta(
+ "group",
+ 1L,
+ new ConsumerMeta(
+ "consumer",
+ 1L,
+ Collections.singletonMap(
+ ConsumerConstant.SQL_DIALECT_KEY, tableModel ? "table" :
"tree")));
+ group.addSubscription(
+ "consumer", new java.util.HashSet<>(java.util.Arrays.asList("kept",
"discarded")));
+ info.alterConsumerGroup(new AlterConsumerGroupPlan(group));
+ final String keptKey = CommitProgressKeeper.generateKey("group", "kept",
"DataRegion[1]", 1);
+ final String legacyKey =
+ CommitProgressKeeper.generateLegacyKey("group", "kept",
"DataRegion[1]", 2);
+ final String discardedKey =
+ CommitProgressKeeper.generateKey("group", "discarded",
"DataRegion[1]", 1);
+ for (final String key : new String[] {keptKey, legacyKey, discardedKey}) {
+ info.getCommitProgressKeeper().updateRegionProgress(key,
ByteBuffer.wrap(new byte[] {1}));
+ }
+ final ConsumerGroupMeta updated = group.deepCopy();
+ if (unsubscribeFirst) {
+ updated.removeSubscription(
+ "consumer", new java.util.HashSet<>(java.util.Arrays.asList("kept",
"discarded")));
+ info.alterConsumerGroup(new AlterConsumerGroupPlan(updated.deepCopy()));
+ }
+ updated.removeConsumer("consumer");
+ info.alterConsumerGroup(new AlterConsumerGroupPlan(updated));
+ Assert.assertNotNull(
+ "tableModel=" + tableModel + ", unsubscribeFirst=" + unsubscribeFirst,
+ info.getCommitProgressKeeper().getRegionProgress(keptKey));
+
Assert.assertNotNull(info.getCommitProgressKeeper().getRegionProgress(legacyKey));
+
Assert.assertNull(info.getCommitProgressKeeper().getRegionProgress(discardedKey));
+ Assert.assertTrue(info.getConsumerGroupMeta("group").isEmpty());
+
Assert.assertTrue(info.getConsumerGroupMeta("group").visibleUnder(tableModel));
+
+ final java.io.File snapshotDir = temporaryFolder.newFolder();
+ Assert.assertTrue(info.processTakeSnapshot(snapshotDir));
+ final SubscriptionInfo restored = new SubscriptionInfo();
+ restored.processLoadSnapshot(snapshotDir);
+
Assert.assertNotNull(restored.getCommitProgressKeeper().getRegionProgress(keptKey));
+ final ConsumerGroupMeta restoredGroup =
restored.deepCopyConsumerGroupMeta("group");
+ Assert.assertTrue(restoredGroup.isEmpty());
+ Assert.assertTrue(restoredGroup.visibleUnder(tableModel));
+ final ConsumerMeta replacement =
+ new ConsumerMeta(
+ "replacement",
+ 2L,
+ Collections.singletonMap(
+ ConsumerConstant.SQL_DIALECT_KEY, tableModel ? "table" :
"tree"));
+ restoredGroup.checkAuthorityBeforeJoinConsumerGroup(replacement);
+ restoredGroup.addConsumer(replacement);
+ restored.alterConsumerGroup(new AlterConsumerGroupPlan(restoredGroup));
+
Assert.assertNotNull(restored.getCommitProgressKeeper().getRegionProgress(keptKey));
+ restoredGroup.removeConsumer("replacement");
+ restored.alterConsumerGroup(new AlterConsumerGroupPlan(restoredGroup));
+
+ // Same-name topics in the other model must not remove this retained
group's progress.
+ final Map<String, String> otherAttributes = new HashMap<>(attributes);
+ otherAttributes.put(SystemConstant.SQL_DIALECT_KEY, tableModel ? "tree" :
"table");
+ restored.createTopic(new CreateTopicPlan(new TopicMeta("kept", 2L,
otherAttributes)));
+ restored.dropTopic(new DropTopicPlan("kept", !tableModel));
+
Assert.assertNotNull(restored.getCommitProgressKeeper().getRegionProgress(keptKey));
+ if (unsubscribeFirst) {
+ attributes.put(TopicConstant.RETAIN_PROGRESS_AFTER_UNSUBSCRIBE_KEY,
"false");
+ restored.alterTopic(new AlterTopicPlan(new TopicMeta("kept", 2L,
attributes)));
+ } else {
+ restored.dropTopic(new DropTopicPlan("kept", tableModel));
+ }
+
Assert.assertNull(restored.getCommitProgressKeeper().getRegionProgress(keptKey));
+
Assert.assertNull(restored.getCommitProgressKeeper().getRegionProgress(legacyKey));
+ Assert.assertFalse(restored.isConsumerGroupExisted("group"));
+ }
+
@Test
public void testSameNameTreeAndTableTopicsCanBeManagedIndependently() {
final String topicName = "same-name-topic";
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 ca29931f1c9..aab4c7523cf 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
@@ -1267,7 +1267,20 @@ public class IoTConsensusServerImpl {
final BlockingQueue<IndexedConsensusRequest> queue,
final SubscriptionWalRetentionPolicy retentionPolicy,
final LongSupplier retainedMinVersionIdSupplier) {
- subscriptionQueueRegistry.register(queue, retentionPolicy,
retainedMinVersionIdSupplier);
+ registerSubscriptionQueue(queue, retentionPolicy,
retainedMinVersionIdSupplier, null);
+ }
+
+ public void registerSubscriptionQueue(
+ final BlockingQueue<IndexedConsensusRequest> queue,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier retainedMinVersionIdSupplier,
+ final String retentionId) {
+ if (retentionId == null) {
+ subscriptionQueueRegistry.register(queue, retentionPolicy,
retainedMinVersionIdSupplier);
+ } else {
+ subscriptionQueueRegistry.register(
+ queue, retentionPolicy, retainedMinVersionIdSupplier, retentionId);
+ }
// Immediately re-evaluate the safe delete index with new subscription
awareness
checkAndUpdateSafeDeletedSearchIndex();
logger.info(
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 995b876cbd1..e75324a8854 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
@@ -45,6 +45,8 @@ public class SubscriptionQueueRegistry {
new ConcurrentHashMap<>();
private final Map<String, SubscriptionQueueRegistration>
detachedRetentionRegistrations =
new ConcurrentHashMap<>();
+ private final Map<String, BlockingQueue<IndexedConsensusRequest>>
retentionOwners =
+ 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];
@@ -73,12 +75,25 @@ public class SubscriptionQueueRegistry {
final SubscriptionWalRetentionPolicy retentionPolicy,
final LongSupplier retainedMinVersionIdSupplier) {
queues.put(
- queue, new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ queue,
+ new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier, null));
+ }
+
+ public synchronized void register(
+ final BlockingQueue<IndexedConsensusRequest> queue,
+ final SubscriptionWalRetentionPolicy retentionPolicy,
+ final LongSupplier retainedMinVersionIdSupplier,
+ final String retentionId) {
+ replaceDetachedRetentionWithQueue(
+ retentionId, queue, retentionPolicy, retainedMinVersionIdSupplier);
}
// Shares the monitor with offer() so unregister() is a real stop-receiving
barrier.
public synchronized void unregister(final
BlockingQueue<IndexedConsensusRequest> queue) {
- queues.remove(queue);
+ final SubscriptionQueueRegistration registration = queues.remove(queue);
+ if (registration != null && registration.retentionId != null) {
+ retentionOwners.remove(registration.retentionId, queue);
+ }
}
public synchronized void replaceQueueWithDetachedRetention(
@@ -86,10 +101,18 @@ public class SubscriptionQueueRegistry {
final String retentionId,
final SubscriptionWalRetentionPolicy retentionPolicy,
final LongSupplier retainedMinVersionIdSupplier) {
- queues.remove(queue);
+ final SubscriptionQueueRegistration registration = queues.remove(queue);
+ // A later unsubscribe cleanup or a new queue may have superseded this
asynchronous close.
+ // The old queue must still stop receiving writes, but cannot recreate
detached retention.
+ if (registration == null
+ || (registration.retentionId != null
+ && !retentionOwners.remove(registration.retentionId, queue))) {
+ return;
+ }
detachedRetentionRegistrations.put(
retentionId,
- new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ new SubscriptionQueueRegistration(
+ retentionPolicy, retainedMinVersionIdSupplier, retentionId));
}
public synchronized void replaceDetachedRetentionWithQueue(
@@ -98,20 +121,26 @@ public class SubscriptionQueueRegistry {
final SubscriptionWalRetentionPolicy retentionPolicy,
final LongSupplier retainedMinVersionIdSupplier) {
detachedRetentionRegistrations.remove(retentionId);
+ retentionOwners.put(retentionId, queue);
queues.put(
- queue, new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ queue,
+ new SubscriptionQueueRegistration(
+ retentionPolicy, retainedMinVersionIdSupplier, retentionId));
}
public synchronized void registerDetachedRetention(
final String retentionId,
final SubscriptionWalRetentionPolicy retentionPolicy,
final LongSupplier retainedMinVersionIdSupplier) {
+ retentionOwners.remove(retentionId);
detachedRetentionRegistrations.put(
retentionId,
- new SubscriptionQueueRegistration(retentionPolicy,
retainedMinVersionIdSupplier));
+ new SubscriptionQueueRegistration(
+ retentionPolicy, retainedMinVersionIdSupplier, retentionId));
}
public synchronized void unregisterDetachedRetention(final String
retentionId) {
+ retentionOwners.remove(retentionId);
detachedRetentionRegistrations.remove(retentionId);
}
@@ -230,12 +259,15 @@ public class SubscriptionQueueRegistry {
private final SubscriptionWalRetentionPolicy retentionPolicy;
private final LongSupplier retainedMinVersionIdSupplier;
+ private final String retentionId;
private SubscriptionQueueRegistration(
final SubscriptionWalRetentionPolicy retentionPolicy,
- final LongSupplier retainedMinVersionIdSupplier) {
+ final LongSupplier retainedMinVersionIdSupplier,
+ final String retentionId) {
this.retentionPolicy = retentionPolicy;
this.retainedMinVersionIdSupplier = retainedMinVersionIdSupplier;
+ this.retentionId = retentionId;
}
}
}
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 214cc2ec8f2..86222dd1118 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
@@ -37,6 +37,52 @@ import static org.junit.Assert.assertTrue;
public class SubscriptionQueueRegistryTest {
+ @Test
+ public void testCancelledRetentionCannotBeRestoredByDelayedQueueClose() {
+ final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("test");
+ final InspectingQueue queue = new InspectingQueue();
+ final SubscriptionWalRetentionPolicy policy =
+ new SubscriptionWalRetentionPolicy("topic", -1, -1);
+ registry.register(queue, policy, () -> 3L, "retention");
+ registry.unregisterDetachedRetention("retention");
+ registry.replaceQueueWithDetachedRetention(queue, "retention", policy, ()
-> 3L);
+ assertTrue(registry.isEmpty());
+ }
+
+ @Test
+ public void testDelayedOldQueueCloseCannotReplaceNewQueueRetention() {
+ final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("test");
+ final InspectingQueue oldQueue = new InspectingQueue();
+ final InspectingQueue newQueue = new InspectingQueue();
+ final SubscriptionWalRetentionPolicy policy =
+ new SubscriptionWalRetentionPolicy("topic", -1, -1);
+ registry.register(oldQueue, policy, () -> 3L, "retention");
+ registry.replaceDetachedRetentionWithQueue("retention", newQueue, policy,
() -> 9L);
+ registry.replaceQueueWithDetachedRetention(oldQueue, "retention", policy,
() -> 3L);
+ assertEquals(1, registry.size());
+ assertEquals(Collections.singletonList(9L),
registry.getRetainedMinVersionIds());
+ registry.replaceQueueWithDetachedRetention(newQueue, "retention", policy,
() -> 9L);
+ assertFalse(registry.hasQueues());
+ assertEquals(Collections.singletonList(9L),
registry.getRetainedMinVersionIds());
+ registry.unregisterDetachedRetention("retention");
+ assertTrue(registry.isEmpty());
+ }
+
+ @Test
+ public void testUpdatedDetachedPolicyIsNotOverwrittenByOldQueueClose() {
+ final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("test");
+ final InspectingQueue queue = new InspectingQueue();
+ final SubscriptionWalRetentionPolicy oldPolicy =
+ new SubscriptionWalRetentionPolicy("topic", -1, -1);
+ final SubscriptionWalRetentionPolicy newPolicy =
+ new SubscriptionWalRetentionPolicy("topic", 100, 200);
+ registry.register(queue, oldPolicy, () -> 3L, "retention");
+ registry.registerDetachedRetention("retention", newPolicy, () -> 9L);
+ registry.replaceQueueWithDetachedRetention(queue, "retention", oldPolicy,
() -> 3L);
+ assertFalse(registry.hasQueues());
+ assertEquals(Collections.singletonList(newPolicy),
registry.getRetentionPolicies());
+ }
+
@Test
public void testOfferWithoutQueuesDoesNotSerializeRequest() {
final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("test");
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 7917fa39a5c..8af488c5923 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
@@ -76,13 +76,7 @@ public class SubscriptionConsumerAgent {
try {
executeWithConsumerGroupLock(
consumerGroupId,
- () -> {
- if (consumerGroupMetaFromCoordinator.isEmpty()) {
- handleDropConsumerGroupInternal(consumerGroupId);
- } else {
-
handleSingleConsumerGroupMetaChangesInternal(consumerGroupMetaFromCoordinator);
- }
- });
+ () ->
handleSingleConsumerGroupMetaChangesInternal(consumerGroupMetaFromCoordinator));
return null;
} catch (final Exception e) {
LOGGER.warn(
@@ -104,6 +98,22 @@ public class SubscriptionConsumerAgent {
final String consumerGroupId = metaFromCoordinator.getConsumerGroupId();
final ConsumerGroupMeta metaInAgent =
getConsumerGroupMeta(consumerGroupId);
+ if (metaFromCoordinator.isEmpty()) {
+ if (Objects.nonNull(metaInAgent)) {
+ cleanupConsumerGroupSubscriptions(consumerGroupId, metaInAgent, true);
+ }
+ final boolean retainProgress =
+
ConsensusSubscriptionSetupHandler.retainConfiguredProgressForConsumerGroup(
+ consumerGroupId, metaFromCoordinator.visibleUnder(true));
+ dropConsumerGroupBroker(consumerGroupId);
+ if (retainProgress) {
+ replaceConsumerGroupMeta(consumerGroupId, metaFromCoordinator);
+ } else {
+ removeConsumerGroupMeta(consumerGroupId);
+ }
+ return;
+ }
+
// if consumer group meta does not exist on local agent
if (Objects.isNull(metaInAgent)) {
SubscriptionAgent.broker().createPipeBrokerIfNotExist(consumerGroupId);
@@ -249,6 +259,11 @@ public class SubscriptionConsumerAgent {
cleanupConsumerGroupSubscriptions(consumerGroupId, consumerGroupMeta);
}
+ dropConsumerGroupBroker(consumerGroupId);
+ removeConsumerGroupMeta(consumerGroupId);
+ }
+
+ private void dropConsumerGroupBroker(final String consumerGroupId) {
if (SubscriptionAgent.broker().isBrokerExist(consumerGroupId)) {
if (!SubscriptionAgent.broker().dropBroker(consumerGroupId)) {
final String exceptionMessage =
@@ -262,8 +277,6 @@ public class SubscriptionConsumerAgent {
.PIPE_LOG_SUBSCRIPTION_BROKER_BOUND_TO_CONSUMER_GROUP_DOES_NOT_EXISTED_9F09E4DE,
consumerGroupId);
}
-
- removeConsumerGroupMeta(consumerGroupId);
}
private ConsumerGroupMeta getConsumerGroupMeta(final String consumerGroupId)
{
@@ -329,6 +342,13 @@ public class SubscriptionConsumerAgent {
private void cleanupConsumerGroupSubscriptions(
final String consumerGroupId, final ConsumerGroupMeta consumerGroupMeta)
{
+ cleanupConsumerGroupSubscriptions(consumerGroupId, consumerGroupMeta,
false);
+ }
+
+ private void cleanupConsumerGroupSubscriptions(
+ final String consumerGroupId,
+ final ConsumerGroupMeta consumerGroupMeta,
+ final boolean retainConfiguredProgress) {
final Set<String> consensusTopics = new LinkedHashSet<>();
for (final String topicName : consumerGroupMeta.getSubscribedTopicNames())
{
if (ConsensusSubscriptionSetupHandler.isConsensusBasedTopic(
@@ -339,8 +359,10 @@ public class SubscriptionConsumerAgent {
}
}
ConsensusSubscriptionSetupHandler.teardownConsensusSubscriptions(
- consumerGroupId, consensusTopics, false);
-
ConsensusSubscriptionSetupHandler.cleanupRetainedProgressForConsumerGroup(consumerGroupId);
+ consumerGroupId, consensusTopics, retainConfiguredProgress);
+ if (!retainConfiguredProgress) {
+
ConsensusSubscriptionSetupHandler.cleanupRetainedProgressForConsumerGroup(consumerGroupId);
+ }
}
public boolean isConsumerExisted(final String consumerGroupId, final String
consumerId) {
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 4e71f2e2fe9..9143e40796c 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
@@ -690,7 +690,11 @@ public class ConsensusPrefetchingQueue {
this::getRequiredRetainedMinVersionId);
} else {
serverImpl.registerSubscriptionQueue(
- pendingEntries, retentionPolicy,
this::getRequiredRetainedMinVersionId);
+ pendingEntries,
+ retentionPolicy,
+ this::getRequiredRetainedMinVersionId,
+ ConsensusSubscriptionWalRetention.generateRetentionId(
+ consumerGroupId, topicName, consensusGroupId));
}
LOGGER.info(
@@ -4474,17 +4478,25 @@ public class ConsensusPrefetchingQueue {
final boolean retainProgressAfterUnsubscribe) {
try {
if (retainProgressAfterUnsubscribe) {
+ // Detached retention outlives this queue. Capture only the reader and
progress manager
+ // required for WAL retention calculation instead of capturing the
queue through a field
+ // reference in either supplier.
+ final ConsensusReqReader retainedConsensusReqReader =
consensusReqReader;
+ final ConsensusSubscriptionCommitManager retainedCommitManager =
commitManager;
+ final String retainedConsumerGroupId = consumerGroupId;
+ final String retainedTopicName = topicName;
+ final ConsensusGroupId retainedConsensusGroupId = consensusGroupId;
serverImpl.replaceSubscriptionQueueWithDetachedRetention(
pendingEntries,
ConsensusSubscriptionWalRetention.generateRetentionId(
consumerGroupId, topicName, consensusGroupId),
retentionPolicy,
ConsensusSubscriptionWalRetention.createCommittedRetainedMinVersionIdSupplier(
- consensusGroupId,
- () -> consensusReqReader,
+ retainedConsensusGroupId,
+ () -> retainedConsensusReqReader,
() ->
- commitManager.getCommittedRegionProgress(
- consumerGroupId, topicName, consensusGroupId)));
+ retainedCommitManager.getCommittedRegionProgress(
+ retainedConsumerGroupId, retainedTopicName,
retainedConsensusGroupId)));
} else {
serverImpl.deregisterSubscriptionQueue(pendingEntries);
}
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 92cfade03c2..964bbcb2259 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
@@ -952,6 +952,34 @@ public class ConsensusSubscriptionSetupHandler {
}
public static void cleanupRetainedProgressForConsumerGroup(final String
consumerGroupId) {
+ cleanupConsumerGroupProgressExceptTopics(consumerGroupId,
Collections.emptySet());
+ }
+
+ public static boolean retainConfiguredProgressForConsumerGroup(
+ final String consumerGroupId, final boolean isTableModel) {
+ final Set<String> topicNames = new LinkedHashSet<>();
+ for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
+
ConsensusSubscriptionCommitManager.getInstance().getPersistedStateKeys()) {
+ if (consumerGroupId.equals(stateKey.getConsumerGroupId())) {
+ topicNames.add(stateKey.getTopicName());
+ }
+ }
+ final Set<String> retainedTopicNames = new LinkedHashSet<>();
+ SubscriptionAgent.topic()
+ .getTopicConfigs(topicNames, isTableModel)
+ .forEach(
+ (topicName, topicConfig) -> {
+ if (topicConfig.isIncrementalMode()
+ && topicConfig.isProgressRetainedAfterUnsubscribe()) {
+ retainedTopicNames.add(topicName);
+ }
+ });
+ cleanupConsumerGroupProgressExceptTopics(consumerGroupId,
retainedTopicNames);
+ return !retainedTopicNames.isEmpty();
+ }
+
+ private static void cleanupConsumerGroupProgressExceptTopics(
+ final String consumerGroupId, final Set<String> retainedTopicNames) {
final IConsensus dataRegionConsensus =
DataRegionConsensusImpl.getInstance();
final IoTConsensus ioTConsensus =
dataRegionConsensus instanceof IoTConsensus ? (IoTConsensus)
dataRegionConsensus : null;
@@ -959,7 +987,8 @@ public class ConsensusSubscriptionSetupHandler {
ConsensusSubscriptionCommitManager.getInstance();
for (final ConsensusSubscriptionCommitManager.PersistedStateKey stateKey :
commitManager.getPersistedStateKeys()) {
- if (!consumerGroupId.equals(stateKey.getConsumerGroupId())) {
+ if (!consumerGroupId.equals(stateKey.getConsumerGroupId())
+ || retainedTopicNames.contains(stateKey.getTopicName())) {
continue;
}
final ConsensusGroupId groupId =
@@ -971,8 +1000,11 @@ public class ConsensusSubscriptionSetupHandler {
consumerGroupId, stateKey.getTopicName(), groupId, serverImpl);
}
}
+ commitManager.removeState(consumerGroupId, stateKey.getTopicName(),
groupId);
+ }
+ if (retainedTopicNames.isEmpty()) {
+ commitManager.removeAllStatesForConsumerGroup(consumerGroupId);
}
- commitManager.removeAllStatesForConsumerGroup(consumerGroupId);
}
public static void refreshDetachedRetentionsForTopic(
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 cc30193647a..a0696342501 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
@@ -33,6 +33,7 @@ import
org.apache.iotdb.consensus.iot.WriterSafeFrontierTracker;
import org.apache.iotdb.consensus.iot.log.ConsensusReqReader;
import org.apache.iotdb.consensus.iot.logdispatcher.IoTConsensusMemoryManager;
import org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueAdmission;
+import org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueRegistry;
import
org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueRejectionReason;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
@@ -280,6 +281,76 @@ public class ConsensusPrefetchingQueueTest {
assertCloseDetachesBlockedConsensusRegistration(true);
}
+ @Test
+ public void testRetainingAsyncCloseDoesNotRecreateCancelledWalRetention()
throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final SubscriptionQueueRegistry registry = new
SubscriptionQueueRegistry("DataRegion[1]");
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(new
FakeConsensusReqReader());
+ final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
+ when(converter.isTableModel()).thenReturn(true);
+ doAnswer(
+ invocation -> {
+ registry.register(
+ invocation.getArgument(0),
+ invocation.getArgument(1),
+ invocation.getArgument(2),
+ invocation.getArgument(3));
+ return null;
+ })
+ .when(serverImpl)
+ .registerSubscriptionQueue(any(), any(), any(), any());
+ doAnswer(
+ invocation -> {
+ registry.replaceQueueWithDetachedRetention(
+ invocation.getArgument(0),
+ invocation.getArgument(1),
+ invocation.getArgument(2),
+ invocation.getArgument(3));
+ return null;
+ })
+ .when(serverImpl)
+ .replaceSubscriptionQueueWithDetachedRetention(any(), any(), any(),
any());
+ ConsensusPrefetchingQueue queue = null;
+ ReentrantReadWriteLock queueLock = null;
+ try {
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy("topic", -1L, -1L),
+ converter,
+
newCommitManager(temporaryFolder.newFolder("cancelled-retention")),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+ final Field lockField =
ConsensusPrefetchingQueue.class.getDeclaredField("lock");
+ lockField.setAccessible(true);
+ queueLock = (ReentrantReadWriteLock) lockField.get(queue);
+ queueLock.readLock().lock();
+ queue.closeAsync(false, true);
+ registry.unregisterDetachedRetention(
+ ConsensusSubscriptionWalRetention.generateRetentionId(
+ "consumerGroup", "topic", new DataRegionId(1)));
+ queueLock.readLock().unlock();
+ awaitCloseFinished(queue);
+ assertTrue(registry.isEmpty());
+ } finally {
+ if (queueLock != null && queueLock.getReadHoldCount() > 0) {
+ queueLock.readLock().unlock();
+ }
+ if (queue != null) {
+ queue.close();
+ awaitCloseFinished(queue);
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
private void assertCloseDetachesBlockedConsensusRegistration(
final boolean retainProgressAfterUnsubscribe) throws Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
@@ -610,7 +681,7 @@ public class ConsensusPrefetchingQueueTest {
return null;
})
.when(serverImpl)
- .registerSubscriptionQueue(any(), any(), any());
+ .registerSubscriptionQueue(any(), any(), any(), any());
final ConsensusSubscriptionCommitManager commitManager =
mock(ConsensusSubscriptionCommitManager.class);
@@ -702,7 +773,7 @@ public class ConsensusPrefetchingQueueTest {
return null;
})
.when(serverImpl)
- .registerSubscriptionQueue(any(), any(), any());
+ .registerSubscriptionQueue(any(), any(), any(), any());
final ConsensusSubscriptionCommitManager commitManager =
mock(ConsensusSubscriptionCommitManager.class);
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 a0b16d9b7c1..934ef58dd20 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
@@ -30,9 +30,12 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class CommitProgressKeeper {
@@ -97,38 +100,75 @@ public class CommitProgressKeeper {
public synchronized void removeTopicProgress(
final String consumerGroupId, final String topicName) {
+ final Set<String> prefixes = getTopicProgressKeyPrefixes(consumerGroupId,
topicName);
+ regionProgressMap
+ .keySet()
+ .removeIf(
+ key ->
+ prefixes.stream()
+ .anyMatch(prefix ->
isValidRegionAndDataNodeProgressKey(key, prefix)));
+ }
+
+ public synchronized boolean hasTopicProgress(
+ final String consumerGroupId, final String topicName) {
+ final Set<String> prefixes = getTopicProgressKeyPrefixes(consumerGroupId,
topicName);
+ return regionProgressMap.keySet().stream()
+ .anyMatch(
+ key ->
+ prefixes.stream()
+ .anyMatch(prefix ->
isValidRegionAndDataNodeProgressKey(key, prefix)));
+ }
+
+ private static Set<String> getTopicProgressKeyPrefixes(
+ final String consumerGroupId, final String topicName) {
+ final Set<String> prefixes = new HashSet<>();
final String versionedTopicKeyPrefix =
VERSIONED_KEY_PREFIX
+ encodeKeyComponent(consumerGroupId)
+ KEY_COMPONENT_SEPARATOR
+ encodeKeyComponent(topicName)
+ KEY_COMPONENT_SEPARATOR;
- final String legacyTopicKeyPrefix =
- String.valueOf(consumerGroupId) + KEY_SEPARATOR +
String.valueOf(topicName) + KEY_SEPARATOR;
- final boolean legacyTopicKeyIsUnambiguous =
- isLegacyKeyUnambiguous(consumerGroupId, topicName, "");
- regionProgressMap
- .keySet()
- .removeIf(
- key ->
- isValidRegionAndDataNodeProgressKey(key,
versionedTopicKeyPrefix)
- || (legacyTopicKeyIsUnambiguous
- && isValidRegionAndDataNodeProgressKey(key,
legacyTopicKeyPrefix)));
+ prefixes.add(versionedTopicKeyPrefix);
+ if (isLegacyKeyUnambiguous(consumerGroupId, topicName, "")) {
+ prefixes.add(consumerGroupId + KEY_SEPARATOR + topicName +
KEY_SEPARATOR);
+ }
+ return prefixes;
}
public synchronized void removeConsumerGroupProgress(final String
consumerGroupId) {
+ removeConsumerGroupProgressExceptTopics(consumerGroupId,
Collections.emptySet());
+ }
+
+ public synchronized void removeConsumerGroupProgressExceptTopics(
+ final String consumerGroupId, final Set<String> retainedTopicNames) {
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;
+ final Set<String> retainedPrefixes = new HashSet<>();
+ retainedTopicNames.forEach(
+ topic ->
retainedPrefixes.addAll(getTopicProgressKeyPrefixes(consumerGroupId, topic)));
regionProgressMap
.keySet()
.removeIf(
key ->
- key.startsWith(versionedConsumerGroupKeyPrefix)
- || (legacyConsumerGroupKeyPrefix != null
- && key.startsWith(legacyConsumerGroupKeyPrefix)));
+ (key.startsWith(versionedConsumerGroupKeyPrefix)
+ || (legacyConsumerGroupKeyPrefix != null
+ && key.startsWith(legacyConsumerGroupKeyPrefix)))
+ && retainedPrefixes.stream()
+ .noneMatch(prefix ->
isValidRegionAndDataNodeProgressKey(key, prefix)));
+ }
+
+ public synchronized boolean hasConsumerGroupProgress(final String
consumerGroupId) {
+ final String versionedPrefix =
+ VERSIONED_KEY_PREFIX + encodeKeyComponent(consumerGroupId) +
KEY_COMPONENT_SEPARATOR;
+ final String legacyPrefix = consumerGroupId + KEY_SEPARATOR;
+ return regionProgressMap.keySet().stream()
+ .anyMatch(
+ key ->
+ key.startsWith(versionedPrefix)
+ || (!consumerGroupId.contains(KEY_SEPARATOR) &&
key.startsWith(legacyPrefix)));
}
private static boolean isValidRegionAndDataNodeProgressKey(
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java
index 2efffb89182..a129c5ae70e 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/subscription/meta/consumer/ConsumerGroupMeta.java
@@ -52,6 +52,7 @@ public class ConsumerGroupMeta {
private long creationTime;
private Map<String, Set<String>> topicNameToSubscribedConsumerIdSet;
private Map<String, ConsumerMeta> consumerIdToConsumerMeta;
+ private boolean tableModel;
private Map<String, Long> topicNameToSubscriptionCreationTime; // used when
creationTime < 0
public ConsumerGroupMeta() {
@@ -68,6 +69,7 @@ public class ConsumerGroupMeta {
this.creationTime = -creationTime;
consumerIdToConsumerMeta.put(firstConsumerMeta.getConsumerId(),
firstConsumerMeta);
+ tableModel = isTableConsumer(firstConsumerMeta);
}
public ConsumerGroupMeta deepCopy() {
@@ -80,6 +82,7 @@ public class ConsumerGroupMeta {
copied.topicNameToSubscribedConsumerIdSet.put(
topicName, new HashSet<>(subscribedConsumerIds)));
copied.consumerIdToConsumerMeta = new
ConcurrentHashMap<>(consumerIdToConsumerMeta);
+ copied.tableModel = tableModel;
copied.topicNameToSubscriptionCreationTime =
new ConcurrentHashMap<>(topicNameToSubscriptionCreationTime);
return copied;
@@ -145,11 +148,10 @@ public class ConsumerGroupMeta {
public void checkAuthorityBeforeJoinConsumerGroup(final ConsumerMeta
consumerMeta)
throws SubscriptionException {
- if (isEmpty()) {
- return;
- }
- final ConsumerMeta existedConsumerMeta =
consumerIdToConsumerMeta.values().iterator().next();
- final String expectedSqlDialect =
existedConsumerMeta.getConfig().getSqlDialect();
+ final String expectedSqlDialect =
+ visibleUnder(true)
+ ? SystemConstant.SQL_DIALECT_TABLE_VALUE
+ : SystemConstant.SQL_DIALECT_TREE_VALUE;
final String actualSqlDialect = consumerMeta.getConfig().getSqlDialect();
final boolean isExpectedTableModel =
SystemConstant.SQL_DIALECT_TABLE_VALUE.equalsIgnoreCase(expectedSqlDialect);
@@ -169,6 +171,7 @@ public class ConsumerGroupMeta {
}
public void addConsumer(final ConsumerMeta consumerMeta) {
+ tableModel = isTableConsumer(consumerMeta);
consumerIdToConsumerMeta.put(consumerMeta.getConsumerId(), consumerMeta);
}
@@ -200,12 +203,12 @@ public class ConsumerGroupMeta {
}
public boolean visibleUnder(final boolean isTableModel) {
- if (consumerIdToConsumerMeta.isEmpty()) {
- return false;
- }
- final String sqlDialect =
-
consumerIdToConsumerMeta.values().iterator().next().getConfig().getSqlDialect();
- return isTableModel ==
SystemConstant.SQL_DIALECT_TABLE_VALUE.equalsIgnoreCase(sqlDialect);
+ return isTableModel == tableModel;
+ }
+
+ private static boolean isTableConsumer(final ConsumerMeta consumerMeta) {
+ return SystemConstant.SQL_DIALECT_TABLE_VALUE.equalsIgnoreCase(
+ consumerMeta.getConfig().getSqlDialect());
}
////////////////////////// subscription //////////////////////////
@@ -345,7 +348,11 @@ public class ConsumerGroupMeta {
}
}
- ReadWriteIOUtils.write(consumerIdToConsumerMeta.size(), outputStream);
+ // A negative empty-consumer count preserves the dialect of a retained
table group. Older
+ // readers also treat this count as an empty map, without changing the
following fields.
+ ReadWriteIOUtils.write(
+ consumerIdToConsumerMeta.isEmpty() && tableModel ? -1 :
consumerIdToConsumerMeta.size(),
+ outputStream);
for (final Map.Entry<String, ConsumerMeta> entry :
consumerIdToConsumerMeta.entrySet()) {
ReadWriteIOUtils.write(entry.getKey(), outputStream);
entry.getValue().serialize(outputStream);
@@ -382,10 +389,12 @@ public class ConsumerGroupMeta {
consumerGroupMeta.consumerIdToConsumerMeta = new ConcurrentHashMap<>();
size = ReadWriteIOUtils.readInt(inputStream);
+ consumerGroupMeta.tableModel = size < 0;
for (int i = 0; i < size; ++i) {
final String key = ReadWriteIOUtils.readString(inputStream);
final ConsumerMeta value = ConsumerMeta.deserialize(inputStream);
consumerGroupMeta.consumerIdToConsumerMeta.put(key, value);
+ consumerGroupMeta.tableModel = isTableConsumer(value);
}
consumerGroupMeta.topicNameToSubscriptionCreationTime = new
ConcurrentHashMap<>();
@@ -423,10 +432,12 @@ public class ConsumerGroupMeta {
consumerGroupMeta.consumerIdToConsumerMeta = new ConcurrentHashMap<>();
size = ReadWriteIOUtils.readInt(byteBuffer);
+ consumerGroupMeta.tableModel = size < 0;
for (int i = 0; i < size; ++i) {
final String key = ReadWriteIOUtils.readString(byteBuffer);
final ConsumerMeta value = ConsumerMeta.deserialize(byteBuffer);
consumerGroupMeta.consumerIdToConsumerMeta.put(key, value);
+ consumerGroupMeta.tableModel = isTableConsumer(value);
}
consumerGroupMeta.topicNameToSubscriptionCreationTime = new
ConcurrentHashMap<>();
@@ -455,6 +466,7 @@ public class ConsumerGroupMeta {
final ConsumerGroupMeta that = (ConsumerGroupMeta) obj;
return Objects.equals(this.consumerGroupId, that.consumerGroupId)
&& this.creationTime == that.creationTime
+ && this.tableModel == that.tableModel
&& Objects.equals(
this.topicNameToSubscribedConsumerIdSet,
that.topicNameToSubscribedConsumerIdSet)
&& Objects.equals(this.consumerIdToConsumerMeta,
that.consumerIdToConsumerMeta)
@@ -467,6 +479,7 @@ public class ConsumerGroupMeta {
return Objects.hash(
consumerGroupId,
creationTime,
+ tableModel,
topicNameToSubscribedConsumerIdSet,
consumerIdToConsumerMeta,
topicNameToSubscriptionCreationTime);
diff --git
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java
index 589571ab2b2..2d2014d7e97 100644
---
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java
+++
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/subscription/consumer/ConsumerGroupDeSerTest.java
@@ -28,6 +28,7 @@ import
org.apache.iotdb.rpc.subscription.exception.SubscriptionException;
import org.junit.Assert;
import org.junit.Test;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
@@ -38,6 +39,44 @@ import static org.junit.Assert.fail;
public class ConsumerGroupDeSerTest {
+ @Test
+ public void testEmptyGroupPreservesDialectAcrossSerializationAndRejoin()
throws Exception {
+ for (final String dialect : new String[] {"tree", "table"}) {
+ final Map<String, String> attributes =
+ Collections.singletonMap(ConsumerConstant.SQL_DIALECT_KEY, dialect);
+ final ConsumerGroupMeta group =
+ new ConsumerGroupMeta("retained", 1L, new ConsumerMeta("consumer",
1L, attributes));
+ group.removeConsumer("consumer");
+ final ByteBuffer buffer = group.serialize();
+ final byte[] bytes = new byte[buffer.remaining()];
+ buffer.duplicate().get(bytes);
+ final ConsumerGroupMeta[] restoredGroups = {
+ group.deepCopy(),
+ ConsumerGroupMeta.deserialize(buffer),
+ ConsumerGroupMeta.deserialize(new ByteArrayInputStream(bytes))
+ };
+ for (final ConsumerGroupMeta restored : restoredGroups) {
+ Assert.assertEquals(group, restored);
+ Assert.assertTrue(restored.isEmpty());
+ Assert.assertEquals("table".equals(dialect),
restored.visibleUnder(true));
+ final ConsumerMeta replacement = new ConsumerMeta("replacement", 2L,
attributes);
+ restored.checkAuthorityBeforeJoinConsumerGroup(replacement);
+ Assert.assertThrows(
+ SubscriptionException.class,
+ () ->
+ restored.checkAuthorityBeforeJoinConsumerGroup(
+ new ConsumerMeta(
+ "other",
+ 2L,
+ Collections.singletonMap(
+ ConsumerConstant.SQL_DIALECT_KEY,
+ "table".equals(dialect) ? "tree" : "table"))));
+ restored.addConsumer(replacement);
+ Assert.assertEquals("table".equals(dialect),
restored.visibleUnder(true));
+ }
+ }
+ }
+
@Test
public void test() throws IOException {
Map<String, String> consumerAttributes = new HashMap<>();
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 7f18e4875d1..2ca78d29264 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
@@ -35,6 +35,7 @@ import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -47,6 +48,31 @@ import static org.junit.Assert.assertTrue;
public class CommitProgressKeeperTest {
+ @Test
+ public void testConsumerGroupCleanupRetainsOnlyConfiguredTopics() {
+ final CommitProgressKeeper keeper = new CommitProgressKeeper();
+ final String retained = CommitProgressKeeper.generateKey("group",
"kept##topic", "1_1", 1);
+ final String retainedLegacy =
CommitProgressKeeper.generateLegacyKey("group", "kept", "1_2", 2);
+ final String discarded = CommitProgressKeeper.generateKey("group",
"discarded", "1_1", 1);
+ final String otherGroup = CommitProgressKeeper.generateKey("other",
"kept", "1_1", 1);
+ for (final String key : new String[] {retained, retainedLegacy, discarded,
otherGroup}) {
+ keeper.updateRegionProgress(key, ByteBuffer.wrap(new byte[] {1}));
+ }
+ keeper.removeConsumerGroupProgressExceptTopics(
+ "group", new java.util.HashSet<>(java.util.Arrays.asList("kept",
"kept##topic")));
+ assertTrue(keeper.hasTopicProgress("group", "kept##topic"));
+ assertTrue(keeper.hasTopicProgress("group", "kept"));
+ assertFalse(keeper.hasTopicProgress("group", "discarded"));
+ assertTrue(keeper.hasConsumerGroupProgress("group"));
+ assertNotNull(keeper.getRegionProgress(retained));
+ assertNotNull(keeper.getRegionProgress(retainedLegacy));
+ assertNull(keeper.getRegionProgress(discarded));
+ assertNotNull(keeper.getRegionProgress(otherGroup));
+ keeper.removeConsumerGroupProgressExceptTopics("group",
Collections.emptySet());
+ assertFalse(keeper.hasConsumerGroupProgress("group"));
+ assertNotNull(keeper.getRegionProgress(otherGroup));
+ }
+
@Test
public void testStateAccessUsesOneInstanceMonitor() throws Exception {
assertSynchronized("removeTopicProgress", String.class, String.class);