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 a321fa34d5d Subscription: preserve topic updates during owner transfer 
(#18353)
a321fa34d5d is described below

commit a321fa34d5db3b64d42010c983563e6fc6c2e5e9
Author: Caideyipi <[email protected]>
AuthorDate: Fri Jul 31 10:57:07 2026 +0800

    Subscription: preserve topic updates during owner transfer (#18353)
    
    * Subscription: preserve topic updates during owner transfer
    
    * Subscription: serialize concurrent topic alterations
---
 .../iotdb/confignode/manager/ProcedureManager.java |  40 ++++--
 .../subscription/SubscriptionCoordinator.java      |  69 +++++++++-
 .../subscription/topic/AlterTopicProcedure.java    |  66 ++++++++-
 .../procedure/store/ProcedureFactory.java          |   7 +-
 .../confignode/procedure/store/ProcedureType.java  |   1 +
 .../subscription/SubscriptionCoordinatorTest.java  | 152 +++++++++++++++++++++
 .../topic/AlterTopicProcedureTest.java             |  82 ++++++++---
 7 files changed, 374 insertions(+), 43 deletions(-)

diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java
index e6a8476500f..bd987cd42a6 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ProcedureManager.java
@@ -59,6 +59,7 @@ import 
org.apache.iotdb.confignode.consensus.request.write.datanode.RemoveDataNo
 import 
org.apache.iotdb.confignode.consensus.request.write.procedure.UpdateProcedurePlan;
 import 
org.apache.iotdb.confignode.consensus.request.write.region.CreateRegionGroupsPlan;
 import org.apache.iotdb.confignode.i18n.ManagerMessages;
+import 
org.apache.iotdb.confignode.manager.subscription.SubscriptionCoordinator;
 import org.apache.iotdb.confignode.persistence.ProcedureInfo;
 import org.apache.iotdb.confignode.procedure.PartitionTableAutoCleaner;
 import org.apache.iotdb.confignode.procedure.Procedure;
@@ -1915,21 +1916,18 @@ public class ProcedureManager {
   }
 
   public TSStatus alterTopic(TAlterTopicReq req) {
+    final SubscriptionCoordinator subscriptionCoordinator =
+        configManager.getSubscriptionManager().getSubscriptionCoordinator();
+    subscriptionCoordinator.lockTopicAlteration(req.getTopicName());
     boolean isOwnerLeaseRenewalBlocked = false;
     try {
       isOwnerLeaseRenewalBlocked =
-          configManager
-              .getSubscriptionManager()
-              .getSubscriptionCoordinator()
-              .blockOwnerLeaseRenewalIfOwnerTransfer(req);
+          subscriptionCoordinator.blockOwnerLeaseRenewalIfOwnerTransfer(req);
       // Owner transfers wait for the previous owner's lease to drain (lease 
duration + one
       // heartbeat interval, measured on the ConfigNode clock) inside the call 
below before the
       // updated meta is built; epoch fencing on DataNodes guarantees 
correctness in the meantime.
       final TopicMeta updatedTopicMeta =
-          configManager
-              .getSubscriptionManager()
-              .getSubscriptionCoordinator()
-              .buildAlteredTopicMetaAfterOwnerLeaseExpired(req);
+          
subscriptionCoordinator.buildAlteredTopicMetaAfterOwnerLeaseExpired(req);
       if (updatedTopicMeta == null) {
         return new TSStatus(TSStatusCode.ALTER_TOPIC_ERROR.getStatusCode())
             .setMessage(
@@ -1938,9 +1936,27 @@ public class ProcedureManager {
                     req.getTopicName()));
       }
 
+      final Map<String, String> attributesBeforeInjection =
+          new HashMap<>(updatedTopicMeta.getConfig().getAttribute());
       
injectTreeViewSourceAttributes(updatedTopicMeta.getConfig().getAttribute());
 
-      AlterTopicProcedure procedure = new 
AlterTopicProcedure(updatedTopicMeta);
+      final Map<String, String> updatedTopicAttributes = new HashMap<>();
+      if (Objects.nonNull(req.getTopicAttributes())) {
+        updatedTopicAttributes.putAll(req.getTopicAttributes());
+      }
+      updatedTopicMeta
+          .getConfig()
+          .getAttribute()
+          .forEach(
+              (key, value) -> {
+                if (!attributesBeforeInjection.containsKey(key)
+                    || !Objects.equals(attributesBeforeInjection.get(key), 
value)) {
+                  updatedTopicAttributes.put(key, value);
+                }
+              });
+
+      AlterTopicProcedure procedure =
+          new AlterTopicProcedure(updatedTopicMeta, updatedTopicAttributes);
       executor.submitProcedure(procedure);
       TSStatus status = waitingProcedureFinished(procedure);
       if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
@@ -1953,11 +1969,9 @@ public class ProcedureManager {
           .setMessage(e.getMessage());
     } finally {
       if (isOwnerLeaseRenewalBlocked) {
-        configManager
-            .getSubscriptionManager()
-            .getSubscriptionCoordinator()
-            .unblockOwnerLeaseRenewal(req.getTopicName());
+        subscriptionCoordinator.unblockOwnerLeaseRenewal(req.getTopicName());
       }
+      subscriptionCoordinator.unlockTopicAlteration(req.getTopicName());
     }
   }
 
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java
index 6fbda89257b..1d988eabc93 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinator.java
@@ -65,6 +65,7 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
 
 public class SubscriptionCoordinator {
 
@@ -80,11 +81,23 @@ public class SubscriptionCoordinator {
 
   private final SubscriptionMetaSyncer subscriptionMetaSyncer;
   private final SubscriptionOwnerLeaseSyncer subscriptionOwnerLeaseSyncer;
+
+  // Serialize client ALTER TOPIC requests per topic. Besides preventing 
ordinary partial updates
+  // from being built from the same snapshot, this also ensures that only one 
owner-transfer
+  // request can own the renewal-block entry for a topic at a time.
+  private final Map<String, TopicAlterationLock> topicAlterationLocks = new 
HashMap<>();
+
   // topicName -> blockSinceMs (ConfigNode local clock when owner-lease 
renewal was stopped for an
   // in-flight owner transfer). Used to skip renewal and to bound the 
admission wait.
   private final Map<String, Long> blockedOwnerLeaseRenewalTopics =
       Collections.synchronizedMap(new HashMap<>());
 
+  private static class TopicAlterationLock {
+
+    private final ReentrantLock lock = new ReentrantLock(true);
+    private int referenceCount;
+  }
+
   public SubscriptionCoordinator(ConfigManager configManager, SubscriptionInfo 
subscriptionInfo) {
     this.configManager = configManager;
     this.subscriptionInfo = subscriptionInfo;
@@ -134,6 +147,30 @@ public class SubscriptionCoordinator {
     return coordinatorLock.isLocked();
   }
 
+  public void lockTopicAlteration(final String topicName) {
+    final TopicAlterationLock topicAlterationLock;
+    synchronized (topicAlterationLocks) {
+      topicAlterationLock =
+          topicAlterationLocks.computeIfAbsent(topicName, ignored -> new 
TopicAlterationLock());
+      topicAlterationLock.referenceCount++;
+    }
+    topicAlterationLock.lock.lock();
+  }
+
+  public void unlockTopicAlteration(final String topicName) {
+    final TopicAlterationLock topicAlterationLock;
+    synchronized (topicAlterationLocks) {
+      topicAlterationLock = topicAlterationLocks.get(topicName);
+    }
+
+    topicAlterationLock.lock.unlock();
+    synchronized (topicAlterationLocks) {
+      if (--topicAlterationLock.referenceCount == 0) {
+        topicAlterationLocks.remove(topicName, topicAlterationLock);
+      }
+    }
+  }
+
   /////////////////////////////// Meta sync ///////////////////////////////
 
   public void startSubscriptionMetaSync() {
@@ -185,7 +222,10 @@ public class SubscriptionCoordinator {
 
   public boolean blockOwnerLeaseRenewalIfOwnerTransfer(TAlterTopicReq req) {
     final TopicMeta currentTopicMeta = 
subscriptionInfo.deepCopyTopicMeta(req.getTopicName());
-    final TopicMeta updatedTopicMeta = buildAlteredTopicMeta(req);
+    final TopicMeta updatedTopicMeta =
+        Objects.isNull(currentTopicMeta)
+            ? null
+            : 
currentTopicMeta.deepCopyWithUpdatedAttributes(req.getTopicAttributes());
     if (Objects.isNull(currentTopicMeta)
         || Objects.isNull(updatedTopicMeta)
         || Objects.equals(currentTopicMeta.getOwnerId(), 
updatedTopicMeta.getOwnerId())) {
@@ -211,7 +251,10 @@ public class SubscriptionCoordinator {
   public TopicMeta buildAlteredTopicMetaAfterOwnerLeaseExpired(TAlterTopicReq 
req)
       throws InterruptedException {
     final TopicMeta currentTopicMeta = 
subscriptionInfo.deepCopyTopicMeta(req.getTopicName());
-    final TopicMeta updatedTopicMeta = buildAlteredTopicMeta(req);
+    final TopicMeta updatedTopicMeta =
+        Objects.isNull(currentTopicMeta)
+            ? null
+            : 
currentTopicMeta.deepCopyWithUpdatedAttributes(req.getTopicAttributes());
     if (Objects.isNull(currentTopicMeta)
         || Objects.isNull(updatedTopicMeta)
         || Objects.equals(currentTopicMeta.getOwnerId(), 
updatedTopicMeta.getOwnerId())) {
@@ -220,20 +263,32 @@ public class SubscriptionCoordinator {
     }
 
     final Long leaseDurationMs = currentTopicMeta.getOwnerLeaseDurationMs();
-    final Long blockSinceMs = 
blockedOwnerLeaseRenewalTopics.get(req.getTopicName());
-    if (Objects.isNull(leaseDurationMs) || Objects.isNull(blockSinceMs)) {
-      // No lease configured (no drain to wait for) or renewal not blocked: 
epoch fencing applies on
-      // reachable DataNodes; nothing further to wait on here.
+    if (Objects.isNull(leaseDurationMs)) {
+      // No lease configured, so there is nothing to drain.
       return updatedTopicMeta;
     }
 
+    waitForOwnerLeaseExpiration(req.getTopicName(), leaseDurationMs);
+
+    // Another alteration may have completed while this owner transfer was 
waiting for the old
+    // lease to drain. Rebuild from the latest TopicMeta so that this request 
only applies its own
+    // attributes instead of restoring the stale snapshot captured before the 
wait.
+    return buildAlteredTopicMeta(req);
+  }
+
+  void waitForOwnerLeaseExpiration(final String topicName, final long 
leaseDurationMs)
+      throws InterruptedException {
+    final Long blockSinceMs = blockedOwnerLeaseRenewalTopics.get(topicName);
+    if (Objects.isNull(blockSinceMs)) {
+      return;
+    }
+
     final long drainDeadlineMs =
         blockSinceMs + leaseDurationMs + 
SubscriptionOwnerLeaseSyncer.getHeartbeatIntervalMs();
     long remainingMs;
     while ((remainingMs = drainDeadlineMs - System.currentTimeMillis()) > 0) {
       Thread.sleep(Math.min(remainingMs, 1000L));
     }
-    return updatedTopicMeta;
   }
 
   public TopicMeta buildAlteredTopicMeta(TAlterTopicReq req) {
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java
index 4a85db0458a..6e9c94dcca4 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedure.java
@@ -41,7 +41,9 @@ import org.slf4j.LoggerFactory;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -53,15 +55,35 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
 
   private TopicMeta existedTopicMeta;
 
+  // Non-null only for client ALTER TOPIC requests. These are merged again 
after the procedure has
+  // acquired the subscription lock, so a request cannot overwrite attributes 
committed after its
+  // initial TopicMeta snapshot was built.
+  private Map<String, String> updatedTopicAttributes;
+
   public AlterTopicProcedure() {
     super();
   }
 
+  public AlterTopicProcedure(final boolean shouldMergeUpdatedTopicAttributes) {
+    super();
+    if (shouldMergeUpdatedTopicAttributes) {
+      updatedTopicAttributes = new HashMap<>();
+    }
+  }
+
   public AlterTopicProcedure(TopicMeta updatedTopicMeta) {
     super();
     this.updatedTopicMeta = updatedTopicMeta;
   }
 
+  public AlterTopicProcedure(
+      TopicMeta updatedTopicMeta, Map<String, String> updatedTopicAttributes) {
+    super();
+    this.updatedTopicMeta = updatedTopicMeta;
+    this.updatedTopicAttributes =
+        Objects.isNull(updatedTopicAttributes) ? null : new 
HashMap<>(updatedTopicAttributes);
+  }
+
   /** This is only used when the subscription info lock is held by another 
procedure. */
   public AlterTopicProcedure(
       TopicMeta updatedTopicMeta, AtomicReference<SubscriptionInfo> 
subscriptionInfo) {
@@ -70,6 +92,15 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
     this.subscriptionInfo = subscriptionInfo;
   }
 
+  /** This is only used when the subscription info lock is held by another 
procedure. */
+  public AlterTopicProcedure(
+      TopicMeta updatedTopicMeta,
+      Map<String, String> updatedTopicAttributes,
+      AtomicReference<SubscriptionInfo> subscriptionInfo) {
+    this(updatedTopicMeta, updatedTopicAttributes);
+    this.subscriptionInfo = subscriptionInfo;
+  }
+
   /** This should be called after {@link #executeFromValidate}. */
   public TopicMeta getExistedTopicMeta() {
     return existedTopicMeta;
@@ -80,6 +111,10 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
     return updatedTopicMeta;
   }
 
+  public boolean shouldMergeUpdatedTopicAttributes() {
+    return Objects.nonNull(updatedTopicAttributes);
+  }
+
   @Override
   protected SubscriptionOperation getOperation() {
     return SubscriptionOperation.ALTER_TOPIC;
@@ -89,9 +124,12 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
   public boolean executeFromValidate(ConfigNodeProcedureEnv env) throws 
SubscriptionException {
     LOGGER.info(ProcedureMessages.ALTERTOPICPROCEDURE_EXECUTEFROMVALIDATE);
 
-    subscriptionInfo.get().validateBeforeAlteringTopic(updatedTopicMeta);
+    existedTopicMeta = 
subscriptionInfo.get().deepCopyTopicMeta(updatedTopicMeta.getTopicName());
+    if (Objects.nonNull(updatedTopicAttributes) && 
Objects.nonNull(existedTopicMeta)) {
+      updatedTopicMeta = 
existedTopicMeta.deepCopyWithUpdatedAttributes(updatedTopicAttributes);
+    }
 
-    existedTopicMeta = 
subscriptionInfo.get().getTopicMeta(updatedTopicMeta.getTopicName());
+    subscriptionInfo.get().validateBeforeAlteringTopic(updatedTopicMeta);
 
     return true;
   }
@@ -196,7 +234,11 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
 
   @Override
   public void serialize(DataOutputStream stream) throws IOException {
-    stream.writeShort(ProcedureType.ALTER_TOPIC_PROCEDURE.getTypeCode());
+    stream.writeShort(
+        (shouldMergeUpdatedTopicAttributes()
+                ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE
+                : ProcedureType.ALTER_TOPIC_PROCEDURE)
+            .getTypeCode());
     super.serialize(stream);
 
     ReadWriteIOUtils.write(updatedTopicMeta != null, stream);
@@ -208,6 +250,10 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
     if (existedTopicMeta != null) {
       existedTopicMeta.serialize(stream);
     }
+
+    if (shouldMergeUpdatedTopicAttributes()) {
+      ReadWriteIOUtils.write(updatedTopicAttributes, stream);
+    }
   }
 
   @Override
@@ -221,6 +267,10 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
     if (ReadWriteIOUtils.readBool(byteBuffer)) {
       existedTopicMeta = TopicMeta.deserialize(byteBuffer);
     }
+
+    if (shouldMergeUpdatedTopicAttributes()) {
+      updatedTopicAttributes = ReadWriteIOUtils.readMap(byteBuffer);
+    }
   }
 
   @Override
@@ -236,12 +286,18 @@ public class AlterTopicProcedure extends 
AbstractOperateSubscriptionProcedure {
         && Objects.equals(getCurrentState(), that.getCurrentState())
         && getCycles() == that.getCycles()
         && Objects.equals(updatedTopicMeta, that.updatedTopicMeta)
-        && Objects.equals(existedTopicMeta, that.existedTopicMeta);
+        && Objects.equals(existedTopicMeta, that.existedTopicMeta)
+        && Objects.equals(updatedTopicAttributes, that.updatedTopicAttributes);
   }
 
   @Override
   public int hashCode() {
     return Objects.hash(
-        getProcId(), getCurrentState(), getCycles(), updatedTopicMeta, 
existedTopicMeta);
+        getProcId(),
+        getCurrentState(),
+        getCycles(),
+        updatedTopicMeta,
+        existedTopicMeta,
+        updatedTopicAttributes);
   }
 }
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java
index 4cc3af5480b..aadc40f310c 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureFactory.java
@@ -382,6 +382,9 @@ public class ProcedureFactory implements IProcedureFactory {
       case ALTER_TOPIC_PROCEDURE:
         procedure = new AlterTopicProcedure();
         break;
+      case ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE:
+        procedure = new AlterTopicProcedure(true);
+        break;
       case TOPIC_META_SYNC_PROCEDURE:
         procedure = new TopicMetaSyncProcedure();
         break;
@@ -546,7 +549,9 @@ public class ProcedureFactory implements IProcedureFactory {
     } else if (procedure instanceof DropTopicProcedure) {
       return ProcedureType.DROP_TOPIC_PROCEDURE;
     } else if (procedure instanceof AlterTopicProcedure) {
-      return ProcedureType.ALTER_TOPIC_PROCEDURE;
+      return ((AlterTopicProcedure) 
procedure).shouldMergeUpdatedTopicAttributes()
+          ? ProcedureType.ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE
+          : ProcedureType.ALTER_TOPIC_PROCEDURE;
     } else if (procedure instanceof TopicMetaSyncProcedure) {
       return ProcedureType.TOPIC_META_SYNC_PROCEDURE;
     } else if (procedure instanceof CreateSubscriptionProcedure) {
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java
index c6b885bfac2..c29d1afd672 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/store/ProcedureType.java
@@ -170,6 +170,7 @@ public enum ProcedureType {
   CONSUMER_GROUP_META_SYNC_PROCEDURE((short) 1509),
   COMMIT_PROGRESS_SYNC_PROCEDURE((short) 1510),
   SUBSCRIPTION_HANDLE_LEADER_CHANGE_PROCEDURE((short) 1511),
+  ALTER_TOPIC_WITH_ATTRIBUTES_PROCEDURE((short) 1512),
 
   /** Other */
   @TestOnly
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinatorTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinatorTest.java
new file mode 100644
index 00000000000..12b1b3dd2ae
--- /dev/null
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/subscription/SubscriptionCoordinatorTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.confignode.manager.subscription;
+
+import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta;
+import 
org.apache.iotdb.confignode.consensus.request.write.subscription.topic.AlterTopicPlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.persistence.subscription.SubscriptionInfo;
+import org.apache.iotdb.confignode.rpc.thrift.TAlterTopicReq;
+import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.subscription.config.TopicConstant;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class SubscriptionCoordinatorTest {
+
+  @Test
+  public void testTopicAlterationLockSerializesOnlyTheSameTopic() throws 
Exception {
+    final SubscriptionCoordinator coordinator =
+        new SubscriptionCoordinator(Mockito.mock(ConfigManager.class), new 
SubscriptionInfo());
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final CountDownLatch sameTopicAttemptStarted = new CountDownLatch(1);
+    final CountDownLatch sameTopicLockAcquired = new CountDownLatch(1);
+    boolean topic1LockHeld = true;
+
+    coordinator.lockTopicAlteration("topic1");
+    try {
+      final Future<?> sameTopicAlteration =
+          executor.submit(
+              () -> {
+                sameTopicAttemptStarted.countDown();
+                coordinator.lockTopicAlteration("topic1");
+                try {
+                  sameTopicLockAcquired.countDown();
+                } finally {
+                  coordinator.unlockTopicAlteration("topic1");
+                }
+              });
+
+      Assert.assertTrue(sameTopicAttemptStarted.await(5, TimeUnit.SECONDS));
+      Assert.assertFalse(sameTopicLockAcquired.await(100, 
TimeUnit.MILLISECONDS));
+
+      coordinator.lockTopicAlteration("topic2");
+      coordinator.unlockTopicAlteration("topic2");
+
+      coordinator.unlockTopicAlteration("topic1");
+      topic1LockHeld = false;
+      Assert.assertTrue(sameTopicLockAcquired.await(5, TimeUnit.SECONDS));
+      sameTopicAlteration.get(5, TimeUnit.SECONDS);
+    } finally {
+      if (topic1LockHeld) {
+        coordinator.unlockTopicAlteration("topic1");
+      }
+      executor.shutdownNow();
+    }
+  }
+
+  @Test
+  public void testOwnerTransferPreservesConcurrentAlterationDuringLeaseWait() 
throws Exception {
+    final String topicName = "test_topic";
+    final String initialColumnFilter = "old-column-filter";
+    final String latestColumnFilter = "latest-column-filter";
+    final long ownerLeaseDurationMs = 60_000L;
+    final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+
+    final Map<String, String> initialAttributes = new HashMap<>();
+    initialAttributes.put(TopicConstant.OWNER_ID_KEY, "owner1");
+    initialAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "1");
+    initialAttributes.put(
+        TopicConstant.OWNER_LEASE_DURATION_MS_KEY, 
String.valueOf(ownerLeaseDurationMs));
+    initialAttributes.put(TopicConstant.COLUMN_FILTER_KEY, 
initialColumnFilter);
+    Assert.assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        subscriptionInfo
+            .createTopic(new CreateTopicPlan(new TopicMeta(topicName, 1L, 
initialAttributes)))
+            .getCode());
+
+    final SubscriptionCoordinator coordinator =
+        new SubscriptionCoordinator(Mockito.mock(ConfigManager.class), 
subscriptionInfo) {
+          @Override
+          void waitForOwnerLeaseExpiration(
+              final String waitingTopicName, final long 
waitingLeaseDurationMs) {
+            Assert.assertEquals(topicName, waitingTopicName);
+            Assert.assertEquals(ownerLeaseDurationMs, waitingLeaseDurationMs);
+
+            final Map<String, String> updatedAttributes = new HashMap<>();
+            updatedAttributes.put(TopicConstant.COLUMN_FILTER_KEY, 
latestColumnFilter);
+            final TopicMeta concurrentlyUpdatedTopicMeta =
+                subscriptionInfo.deepCopyTopicMetaWithUpdatedAttributes(
+                    topicName, updatedAttributes);
+            Assert.assertEquals(
+                TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+                subscriptionInfo
+                    .alterTopic(new 
AlterTopicPlan(concurrentlyUpdatedTopicMeta))
+                    .getCode());
+          }
+        };
+
+    final Map<String, String> ownerTransferAttributes = new HashMap<>();
+    ownerTransferAttributes.put(TopicConstant.OWNER_ID_KEY, "owner2");
+    ownerTransferAttributes.put(TopicConstant.OWNER_EPOCH_KEY, "2");
+    ownerTransferAttributes.put(
+        TopicConstant.OWNER_LEASE_DURATION_MS_KEY, 
String.valueOf(ownerLeaseDurationMs));
+    final TAlterTopicReq ownerTransferRequest =
+        new TAlterTopicReq()
+            .setTopicName(topicName)
+            .setTopicAttributes(ownerTransferAttributes)
+            .setSubscribedConsumerGroupIds(Collections.emptySet());
+
+    
Assert.assertTrue(coordinator.blockOwnerLeaseRenewalIfOwnerTransfer(ownerTransferRequest));
+    try {
+      final TopicMeta result =
+          
coordinator.buildAlteredTopicMetaAfterOwnerLeaseExpired(ownerTransferRequest);
+
+      Assert.assertEquals("owner2", result.getOwnerId());
+      Assert.assertEquals(2L, result.getOwnerEpoch());
+      Assert.assertEquals(
+          latestColumnFilter, 
result.getConfig().getString(TopicConstant.COLUMN_FILTER_KEY));
+    } finally {
+      coordinator.unblockOwnerLeaseRenewal(topicName);
+    }
+  }
+}
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java
index b943e4edd97..3f553cb6013 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/subscription/topic/AlterTopicProcedureTest.java
@@ -20,7 +20,11 @@
 package org.apache.iotdb.confignode.procedure.impl.subscription.topic;
 
 import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta;
+import 
org.apache.iotdb.confignode.consensus.request.write.subscription.topic.AlterTopicPlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.subscription.topic.CreateTopicPlan;
+import org.apache.iotdb.confignode.persistence.subscription.SubscriptionInfo;
 import org.apache.iotdb.confignode.procedure.store.ProcedureFactory;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.utils.PublicBAOS;
 import org.junit.Test;
@@ -29,32 +33,76 @@ import java.io.DataOutputStream;
 import java.nio.ByteBuffer;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
 
 public class AlterTopicProcedureTest {
+
   @Test
-  public void serializeDeserializeTest() {
-    PublicBAOS byteArrayOutputStream = new PublicBAOS();
-    DataOutputStream outputStream = new 
DataOutputStream(byteArrayOutputStream);
+  public void serializeDeserializeTest() throws Exception {
+    final Map<String, String> topicAttributes = new HashMap<>();
+    topicAttributes.put("path", "root.db1.**");
+    assertSerializeDeserialize(
+        new AlterTopicProcedure(new TopicMeta("test_topic", 1, 
topicAttributes)));
+  }
 
-    Map<String, String> topicAttributes = new HashMap<>();
+  @Test
+  public void serializeDeserializeWithUpdatedAttributesTest() throws Exception 
{
+    final Map<String, String> topicAttributes = new HashMap<>();
     topicAttributes.put("path", "root.db1.**");
+    final Map<String, String> updatedTopicAttributes = new HashMap<>();
+    updatedTopicAttributes.put("processor", "processor1");
+    assertSerializeDeserialize(
+        new AlterTopicProcedure(
+            new TopicMeta("test_topic", 1, topicAttributes), 
updatedTopicAttributes));
+  }
+
+  private void assertSerializeDeserialize(final AlterTopicProcedure procedure) 
throws Exception {
+    final PublicBAOS byteArrayOutputStream = new PublicBAOS();
+    final DataOutputStream outputStream = new 
DataOutputStream(byteArrayOutputStream);
+    procedure.serialize(outputStream);
+    final ByteBuffer buffer =
+        ByteBuffer.wrap(byteArrayOutputStream.getBuf(), 0, 
byteArrayOutputStream.size());
+    final AlterTopicProcedure deserializedProcedure =
+        (AlterTopicProcedure) ProcedureFactory.getInstance().create(buffer);
+    assertEquals(procedure, deserializedProcedure);
+  }
+
+  @Test
+  public void testRebaseUpdatedAttributesDuringValidate() throws Exception {
+    final String topicName = "test_topic";
+    final SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
+    final Map<String, String> initialAttributes = new HashMap<>();
+    initialAttributes.put("path", "root.db1.**");
+    assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        subscriptionInfo
+            .createTopic(new CreateTopicPlan(new TopicMeta(topicName, 1, 
initialAttributes)))
+            .getCode());
+
+    final Map<String, String> requestAttributes = new HashMap<>();
+    requestAttributes.put("processor", "processor1");
+    final TopicMeta staleUpdatedTopicMeta =
+        subscriptionInfo.deepCopyTopicMetaWithUpdatedAttributes(topicName, 
requestAttributes);
 
-    AlterTopicProcedure proc =
-        new AlterTopicProcedure(new TopicMeta("test_topic", 1, 
topicAttributes));
+    final Map<String, String> concurrentAttributes = new HashMap<>();
+    concurrentAttributes.put("source", "source1");
+    assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+        subscriptionInfo
+            .alterTopic(
+                new AlterTopicPlan(
+                    subscriptionInfo.deepCopyTopicMetaWithUpdatedAttributes(
+                        topicName, concurrentAttributes)))
+            .getCode());
 
-    try {
-      proc.serialize(outputStream);
-      ByteBuffer buffer =
-          ByteBuffer.wrap(byteArrayOutputStream.getBuf(), 0, 
byteArrayOutputStream.size());
-      AlterTopicProcedure proc2 =
-          (AlterTopicProcedure) ProcedureFactory.getInstance().create(buffer);
+    final AlterTopicProcedure procedure =
+        new AlterTopicProcedure(
+            staleUpdatedTopicMeta, requestAttributes, new 
AtomicReference<>(subscriptionInfo));
+    procedure.executeFromValidate(null);
 
-      assertEquals(proc, proc2);
-    } catch (Exception e) {
-      fail();
-    }
+    assertEquals("processor1", 
procedure.getUpdatedTopicMeta().getConfig().getString("processor"));
+    assertEquals("source1", 
procedure.getUpdatedTopicMeta().getConfig().getString("source"));
   }
 }

Reply via email to