This is an automated email from the ASF dual-hosted git repository.

jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/dev/1.3 by this push:
     new 42eae6a643d [To dev/1.3] Fix SyncStatus batch memory reservation retry 
(#18557)
42eae6a643d is described below

commit 42eae6a643d3895545913f984743689baa232442
Author: Jiang Tian <[email protected]>
AuthorDate: Tue Sep 1 11:23:40 2026 +0800

    [To dev/1.3] Fix SyncStatus batch memory reservation retry (#18557)
    
    * fix: retry SyncStatus batch memory reservation (#18453)
    
    * test: adapt SyncStatusTest to dev/1.3 memory manager
    
    * test: cover concurrent large writes with two replicas
---
 .../consensus/iot/logdispatcher/SyncStatus.java    |  13 +-
 .../consensus/iot/ConcurrentLargeWriteTest.java    | 248 +++++++++++++++++++++
 .../iot/logdispatcher/SyncStatusTest.java          |  55 +++++
 3 files changed, 312 insertions(+), 4 deletions(-)

diff --git 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatus.java
 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatus.java
index a96abfb1a5a..9e6af375828 100644
--- 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatus.java
+++ 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatus.java
@@ -53,10 +53,15 @@ public class SyncStatus {
    * @throws InterruptedException
    */
   public synchronized void addNextBatch(Batch batch) throws 
InterruptedException {
-    while ((pendingBatches.size() >= 
config.getReplication().getMaxPendingBatchesNum()
-            || !iotConsensusMemoryManager.reserve(batch))
-        && !Thread.interrupted()) {
-      wait();
+    while (true) {
+      while (pendingBatches.size() >= 
config.getReplication().getMaxPendingBatchesNum()) {
+        wait();
+      }
+      if (iotConsensusMemoryManager.reserve(batch)) {
+        break;
+      }
+      // Memory may be freed by another SyncStatus, which cannot notify this 
monitor.
+      wait(Math.max(1, config.getReplication().getBasicRetryWaitTimeMs()));
     }
     if (LOGGER.isDebugEnabled()) {
       LOGGER.debug(
diff --git 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ConcurrentLargeWriteTest.java
 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ConcurrentLargeWriteTest.java
new file mode 100644
index 00000000000..4d28d610095
--- /dev/null
+++ 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/ConcurrentLargeWriteTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.consensus.iot;
+
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.consensus.ConsensusGroupId;
+import org.apache.iotdb.commons.consensus.DataRegionId;
+import org.apache.iotdb.commons.exception.StartupException;
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.consensus.common.ConsensusGroup;
+import org.apache.iotdb.consensus.common.Peer;
+import org.apache.iotdb.consensus.common.request.IConsensusRequest;
+import org.apache.iotdb.consensus.config.ConsensusConfig;
+import org.apache.iotdb.consensus.config.IoTConsensusConfig;
+import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.consensus.iot.logdispatcher.IoTConsensusMemoryManager;
+import org.apache.iotdb.consensus.iot.util.TestStateMachine;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.ratis.util.FileUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+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 ConcurrentLargeWriteTest {
+
+  private static final ConsensusGroupId GROUP_ID = new DataRegionId(1);
+  private static final int FIRST_PORT = 10000;
+  private static final int LAST_PORT = 20000;
+  private static final int REQUEST_COUNT = 4;
+  private static final int REQUEST_PAYLOAD_SIZE = 8 * 1024 * 1024;
+  private static final long REPLICATION_TIMEOUT_SECONDS = 60;
+
+  private List<Peer> peers;
+  private final List<File> storageDirs =
+      Arrays.asList(
+          new File("target" + File.separator + "concurrent-large-write-1"),
+          new File("target" + File.separator + "concurrent-large-write-2"));
+  private final List<IoTConsensus> servers = new ArrayList<>();
+  private final List<TestStateMachine> stateMachines = new ArrayList<>();
+  private ConsensusGroup group;
+  private final IoTConsensusMemoryManager memoryManager = 
IoTConsensusMemoryManager.getInstance();
+
+  private long previousMaxMemory;
+  private long previousMaxQueueMemory;
+
+  @Before
+  public void setUp() throws Exception {
+    int basePort = findAvailablePortPair();
+    Assume.assumeTrue(basePort > 0);
+    peers =
+        Arrays.asList(
+            new Peer(GROUP_ID, 1, new TEndPoint("127.0.0.1", basePort)),
+            new Peer(GROUP_ID, 2, new TEndPoint("127.0.0.1", basePort + 1)));
+    group = new ConsensusGroup(GROUP_ID, peers);
+    previousMaxMemory = memoryManager.getMaxMemorySizeInByte();
+    previousMaxQueueMemory = memoryManager.getMaxMemorySizeForQueueInByte();
+    for (File storageDir : storageDirs) {
+      FileUtils.deleteFully(storageDir);
+      FileUtils.createDirectories(storageDir);
+    }
+
+    IoTConsensusConfig consensusConfig =
+        IoTConsensusConfig.newBuilder()
+            .setReplication(
+                IoTConsensusConfig.Replication.newBuilder()
+                    .setMaxLogEntriesNumPerBatch(2)
+                    .setMaxSizePerBatch(REQUEST_PAYLOAD_SIZE * 2)
+                    .setMaxPendingBatchesNum(5)
+                    .setBasicRetryWaitTimeMs(10)
+                    // Only one large batch fits, forcing competing 
dispatchers to retry
+                    // reservation.
+                    .setAllocateMemoryForConsensus(REQUEST_PAYLOAD_SIZE * 2L)
+                    .setMaxMemoryRatioForQueue(1.0)
+                    .build())
+            .build();
+    for (int i = 0; i < peers.size(); i++) {
+      int nodeIndex = i;
+      stateMachines.add(new TestStateMachine());
+      servers.add(
+          (IoTConsensus)
+              ConsensusFactory.getConsensusImpl(
+                      ConsensusFactory.IOT_CONSENSUS,
+                      ConsensusConfig.newBuilder()
+                          .setThisNodeId(peers.get(i).getNodeId())
+                          .setThisNode(peers.get(i).getEndpoint())
+                          .setStorageDir(storageDirs.get(i).getAbsolutePath())
+                          
.setConsensusGroupType(TConsensusGroupType.DataRegion)
+                          .setIoTConsensusConfig(consensusConfig)
+                          .build(),
+                      groupId -> stateMachines.get(nodeIndex))
+                  .orElseThrow(
+                      () ->
+                          new IllegalArgumentException(
+                              String.format(
+                                  ConsensusFactory.CONSTRUCT_FAILED_MSG,
+                                  ConsensusFactory.IOT_CONSENSUS))));
+      
servers.get(i).recordCorrectPeerListBeforeStarting(Collections.singletonMap(GROUP_ID,
 peers));
+    }
+    try {
+      for (IoTConsensus server : servers) {
+        server.start();
+      }
+    } catch (IOException e) {
+      if (e.getCause() instanceof StartupException) {
+        Assume.assumeTrue(false);
+      }
+      throw e;
+    }
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    servers.forEach(IoTConsensus::stop);
+    servers.clear();
+    memoryManager.init(previousMaxMemory, previousMaxQueueMemory);
+    for (File storageDir : storageDirs) {
+      FileUtils.deleteFully(storageDir);
+    }
+  }
+
+  @Test
+  public void testConcurrentLargeWritesWithTwoReplicas() throws Exception {
+    for (IoTConsensus server : servers) {
+      createLocalPeer(server);
+    }
+
+    CountDownLatch start = new CountDownLatch(1);
+    ExecutorService executor = Executors.newFixedThreadPool(peers.size() * 
REQUEST_COUNT);
+    List<Future<TSStatus>> futures = new ArrayList<>();
+    try {
+      for (int nodeIndex = 0; nodeIndex < peers.size(); nodeIndex++) {
+        int finalNodeIndex = nodeIndex;
+        for (int requestIndex = 0; requestIndex < REQUEST_COUNT; 
requestIndex++) {
+          int finalRequestIndex = requestIndex;
+          futures.add(
+              executor.submit(
+                  () -> {
+                    start.await();
+                    return servers
+                        .get(finalNodeIndex)
+                        .write(
+                            GROUP_ID,
+                            new LargeTestEntry(
+                                finalRequestIndex,
+                                peers.get(finalNodeIndex),
+                                REQUEST_PAYLOAD_SIZE));
+                  }));
+        }
+      }
+      start.countDown();
+      for (Future<TSStatus> future : futures) {
+        Assert.assertEquals(
+            TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+            future.get(30, TimeUnit.SECONDS).getCode());
+      }
+    } finally {
+      executor.shutdownNow();
+      executor.awaitTermination(30, TimeUnit.SECONDS);
+    }
+
+    long deadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(REPLICATION_TIMEOUT_SECONDS);
+    while (System.nanoTime() < deadline
+        && (servers.get(0).getImpl(GROUP_ID).getMinSyncIndex() < REQUEST_COUNT
+            || servers.get(1).getImpl(GROUP_ID).getMinSyncIndex() < 
REQUEST_COUNT)) {
+      Thread.sleep(100);
+    }
+
+    Assert.assertEquals(REQUEST_COUNT, 
servers.get(0).getImpl(GROUP_ID).getMinSyncIndex());
+    Assert.assertEquals(REQUEST_COUNT, 
servers.get(1).getImpl(GROUP_ID).getMinSyncIndex());
+    Assert.assertEquals(REQUEST_COUNT * peers.size(), 
stateMachines.get(0).getRequestSet().size());
+    Assert.assertEquals(REQUEST_COUNT * peers.size(), 
stateMachines.get(1).getRequestSet().size());
+  }
+
+  private void createLocalPeer(IoTConsensus server) throws ConsensusException {
+    server.createLocalPeer(GROUP_ID, group.getPeers());
+  }
+
+  private int findAvailablePortPair() {
+    for (int basePort = FIRST_PORT; basePort < LAST_PORT; basePort++) {
+      try (ServerSocket first = new ServerSocket(basePort);
+          ServerSocket second = new ServerSocket(basePort + 1)) {
+        // Keep the test independent from services using a fixed development 
port.
+        return basePort;
+      } catch (IOException e) {
+        // Try the next pair when either port is already in use.
+      }
+    }
+    return -1;
+  }
+
+  private static class LargeTestEntry implements IConsensusRequest {
+    private final ByteBuffer serialized;
+
+    private LargeTestEntry(int num, Peer peer, int payloadSize) throws 
IOException {
+      try (ByteArrayOutputStream output = new 
ByteArrayOutputStream(payloadSize + 64);
+          DataOutputStream dataOutput = new DataOutputStream(output)) {
+        dataOutput.writeInt(num);
+        peer.serialize(dataOutput);
+        dataOutput.write(new byte[payloadSize]);
+        dataOutput.flush();
+        serialized = ByteBuffer.wrap(output.toByteArray());
+      }
+    }
+
+    @Override
+    public ByteBuffer serializeToByteBuffer() {
+      return serialized.duplicate();
+    }
+  }
+}
diff --git 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatusTest.java
 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatusTest.java
index be81c69f7f7..dd1ee21c1a9 100644
--- 
a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatusTest.java
+++ 
b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/logdispatcher/SyncStatusTest.java
@@ -36,7 +36,13 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 public class SyncStatusTest {
 
@@ -242,4 +248,53 @@ public class SyncStatusTest {
     Assert.assertEquals(
         config.getReplication().getMaxPendingBatchesNum() + 1, 
status.getNextSendingIndex());
   }
+
+  @Test
+  public void testFirstBatchRetriesMemoryReservation()
+      throws InterruptedException, ExecutionException, TimeoutException {
+    IndexController controller =
+        new IndexController(storageDir.getAbsolutePath(), peer, 0, 
CHECK_POINT_GAP);
+    IoTConsensusConfig retryConfig =
+        IoTConsensusConfig.newBuilder()
+            .setReplication(
+                
IoTConsensusConfig.Replication.newBuilder().setBasicRetryWaitTimeMs(10).build())
+            .build();
+    SyncStatus status = new SyncStatus(controller, retryConfig);
+    TLogEntry logEntry = new TLogEntry().setSearchIndex(1).setMemorySize(1);
+    Batch batch = new Batch(retryConfig);
+    batch.addTLogEntry(logEntry);
+    batch.buildIndex();
+
+    IoTConsensusMemoryManager memoryManager = 
IoTConsensusMemoryManager.getInstance();
+    long previousMaxMemory = memoryManager.getMaxMemorySizeInByte();
+    long previousMaxQueueMemory = 
memoryManager.getMaxMemorySizeForQueueInByte();
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    CountDownLatch taskStarted = new CountDownLatch(1);
+    memoryManager.init(0, 0);
+    try {
+      Future<?> future =
+          executor.submit(
+              () -> {
+                taskStarted.countDown();
+                status.addNextBatch(batch);
+                return null;
+              });
+
+      Assert.assertTrue(taskStarted.await(5, TimeUnit.SECONDS));
+      // The zero limit makes the first reservation fail before the retry 
limit is raised.
+      Thread.sleep(100);
+      Assert.assertFalse(future.isDone());
+      memoryManager.init(batch.getMemorySize() + 1, batch.getMemorySize() + 1);
+      future.get(5, TimeUnit.SECONDS);
+
+      Assert.assertEquals(1, status.getPendingBatches().size());
+      status.removeBatch(batch);
+      Assert.assertEquals(0, status.getPendingBatches().size());
+    } finally {
+      executor.shutdownNow();
+      executor.awaitTermination(5, TimeUnit.SECONDS);
+      status.free();
+      memoryManager.init(previousMaxMemory, previousMaxQueueMemory);
+    }
+  }
 }

Reply via email to