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 83d419796fb Fix receiver write admission stalls and RPC buffer 
allocation failures (#18918)
83d419796fb is described below

commit 83d419796fb91a6cccb874414820d3865ab34b98
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 21 12:21:08 2026 +0800

    Fix receiver write admission stalls and RPC buffer allocation failures 
(#18918)
    
    * Fix receiver write admission and RPC buffer memory pressure
    
    * Preserve RPC frame buffers and avoid duplicate flush submissions
    
    * Revert fallback flush trigger in memory admission
---
 .../org/apache/iotdb/rpc/AutoResizingBuffer.java   |  47 +--
 .../iotdb/rpc/AutoResizingBufferMemoryManager.java |  12 +
 .../iotdb/rpc/AutoScalingBufferWriteTransport.java |   4 +-
 .../rpc/TCompressedElasticFramedTransport.java     |  10 +-
 .../apache/iotdb/rpc/TElasticFramedTransport.java  |  10 +-
 .../apache/iotdb/rpc/AutoResizingBufferTest.java   | 347 +++++++++++++++++++++
 .../db/storageengine/rescon/memory/SystemInfo.java |   8 +-
 .../rescon/memory/SystemInfoTest.java              | 232 ++++++++++++++
 8 files changed, 636 insertions(+), 34 deletions(-)

diff --git 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBuffer.java
 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBuffer.java
index aa0ea40541c..d9772985dc6 100644
--- 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBuffer.java
+++ 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBuffer.java
@@ -23,14 +23,15 @@ import java.io.IOException;
 import java.util.Arrays;
 
 /**
- * Helper class that wraps a byte[] so that it can expand and be reused. Users 
should call
- * resizeIfNecessary to make sure the buffer has suitable capacity, and then 
use the array as
- * needed.
+ * Helper class that wraps a byte[] so that it can expand and be reused. 
Incremental writes use
+ * ensureCapacity; resizeIfNecessary also considers shrinking and must receive 
a complete frame's
+ * size.
  *
  * <p>Resizing policies: Expanding: If the required size > current capacity * 
1.5, expand to the
- * required size, otherwise expand to current capacity * 1.5. Shrinking: If 
initial size < the
- * required size < current capacity * 0.6, and such small requests last for 
more than 5 times,
- * shrink to the middle of the required size and current capacity.
+ * required size, otherwise expand to current capacity * 1.5 when the memory 
budget allows it.
+ * Shrinking: If the required size < current capacity * 0.6, and such small 
requests last for more
+ * than 5 times, shrink to the middle of the required size and current 
capacity. The required size
+ * is bounded below by the initial capacity.
  */
 class AutoResizingBuffer implements AutoCloseable {
 
@@ -56,23 +57,28 @@ class AutoResizingBuffer implements AutoCloseable {
   }
 
   public void resizeIfNecessary(int size) throws IOException {
-    reserveCurrentCapacityIfReleased();
+    ensureCapacity(size);
+    size = Math.max(size, initialCapacity);
     final int currentCapacity = this.array.length;
     final double loadFactor = 0.6;
+    if (currentCapacity * loadFactor > size
+        && bufTooLargeCounter-- <= 0
+        && System.currentTimeMillis() - lastShrinkTime > 
RpcUtils.MIN_SHRINK_INTERVAL) {
+      resize(size + (currentCapacity - size) / 2, size);
+      bufTooLargeCounter = RpcUtils.MAX_BUFFER_OVERSIZE_TIME;
+      lastShrinkTime = System.currentTimeMillis();
+    }
+  }
+
+  void ensureCapacity(int size) throws IOException {
+    reserveCurrentCapacityIfReleased();
+    final int currentCapacity = this.array.length;
     if (currentCapacity < size) {
       // Increase by a factor of 1.5x
       int growCapacity = currentCapacity + (currentCapacity >> 1);
       int newCapacity = Math.max(growCapacity, size);
-      resize(newCapacity);
+      resize(newCapacity, size);
       bufTooLargeCounter = RpcUtils.MAX_BUFFER_OVERSIZE_TIME;
-    } else if (size > initialCapacity
-        && currentCapacity * loadFactor > size
-        && bufTooLargeCounter-- <= 0
-        && System.currentTimeMillis() - lastShrinkTime > 
RpcUtils.MIN_SHRINK_INTERVAL) {
-      // do not resize if it is reading the request size and do not shrink too 
often
-      resize(size + (currentCapacity - size) / 2);
-      bufTooLargeCounter = RpcUtils.MAX_BUFFER_OVERSIZE_TIME;
-      lastShrinkTime = System.currentTimeMillis();
     }
   }
 
@@ -89,11 +95,14 @@ class AutoResizingBuffer implements AutoCloseable {
     }
   }
 
-  private void resize(int newCapacity) throws IOException {
+  private void resize(int newCapacity, int requiredCapacity) throws 
IOException {
     final int currentCapacity = array.length;
     if (newCapacity > currentCapacity) {
-      final int delta = newCapacity - currentCapacity;
-      memoryAccount.allocate(delta);
+      final int delta =
+          (int)
+              memoryAccount.allocate(
+                  newCapacity - currentCapacity, requiredCapacity - 
currentCapacity);
+      newCapacity = currentCapacity + delta;
       try {
         array = Arrays.copyOf(array, newCapacity);
         accountedCapacity += delta;
diff --git 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBufferMemoryManager.java
 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBufferMemoryManager.java
index d33a15b44cb..bbdbc8bda06 100644
--- 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBufferMemoryManager.java
+++ 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoResizingBufferMemoryManager.java
@@ -124,6 +124,18 @@ public final class AutoResizingBufferMemoryManager {
       AutoResizingBufferMemoryManager.allocate(memoryControl, sizeInBytes);
     }
 
+    long allocate(long preferredSizeInBytes, long requiredSizeInBytes) throws 
IOException {
+      // Growth headroom is optional. If it does not fit, only wait for the 
bytes actually needed
+      // by the frame instead of rejecting a request that still fits within 
the memory budget.
+      if (preferredSizeInBytes > requiredSizeInBytes
+          && memoryControl.allocate(preferredSizeInBytes)) {
+        memoryAllocationCount.incrementAndGet();
+        return preferredSizeInBytes;
+      }
+      allocate(requiredSizeInBytes);
+      return requiredSizeInBytes;
+    }
+
     void release(long sizeInBytes) {
       AutoResizingBufferMemoryManager.release(memoryControl, sizeInBytes);
     }
diff --git 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoScalingBufferWriteTransport.java
 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoScalingBufferWriteTransport.java
index 24278067fcd..eab45f3da48 100644
--- 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoScalingBufferWriteTransport.java
+++ 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/AutoScalingBufferWriteTransport.java
@@ -47,7 +47,9 @@ public class AutoScalingBufferWriteTransport extends 
NonOpenTransport {
   @Override
   public void write(byte[] toWrite, int off, int len) throws 
TTransportException {
     try {
-      buf.resizeIfNecessary(pos + len);
+      // A small prefix during serialization does not indicate a small frame. 
Only the complete
+      // frame size, supplied after flushing, may trigger shrinking.
+      buf.ensureCapacity(pos + len);
     } catch (IOException e) {
       throw new TTransportException(e);
     }
diff --git 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TCompressedElasticFramedTransport.java
 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TCompressedElasticFramedTransport.java
index b4c6c0994f5..36cca36b9b1 100644
--- 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TCompressedElasticFramedTransport.java
+++ 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TCompressedElasticFramedTransport.java
@@ -100,12 +100,10 @@ public abstract class TCompressedElasticFramedTransport 
extends TElasticFramedTr
     }
 
     writeBuffer.reset();
-    if (thriftDefaultBufferSize < length) {
-      try {
-        writeBuffer.resizeIfNecessary(thriftDefaultBufferSize);
-      } catch (IOException e) {
-        throw new TTransportException(e);
-      }
+    try {
+      writeBuffer.resizeIfNecessary(length);
+    } catch (IOException e) {
+      throw new TTransportException(e);
     }
     underlying.flush();
   }
diff --git 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
index 339dee595e7..e15c9f59424 100644
--- 
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
+++ 
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/TElasticFramedTransport.java
@@ -317,12 +317,10 @@ public class TElasticFramedTransport extends TTransport {
     underlying.write(i32buf, 0, 4);
     underlying.write(writeBuffer.getBuffer(), 0, length);
     writeBuffer.reset();
-    if (length > thriftDefaultBufferSize) {
-      try {
-        writeBuffer.resizeIfNecessary(thriftDefaultBufferSize);
-      } catch (IOException e) {
-        throw new TTransportException(e);
-      }
+    try {
+      writeBuffer.resizeIfNecessary(length);
+    } catch (IOException e) {
+      throw new TTransportException(e);
     }
     underlying.flush();
   }
diff --git 
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/AutoResizingBufferTest.java
 
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/AutoResizingBufferTest.java
index 164d0f9e287..18aed44eec3 100644
--- 
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/AutoResizingBufferTest.java
+++ 
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/AutoResizingBufferTest.java
@@ -27,6 +27,8 @@ import org.junit.Test;
 
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Random;
 import java.util.concurrent.atomic.AtomicLong;
 
 public class AutoResizingBufferTest {
@@ -60,6 +62,336 @@ public class AutoResizingBufferTest {
     Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
   }
 
+  @Test
+  public void testGrowToRequiredCapacityWhenPreferredCapacityExceedsBudget() 
throws Exception {
+    memoryControl.setLimit(120);
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+    AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
+
+    try (AutoResizingBuffer buffer = new AutoResizingBuffer(100)) {
+      buffer.array()[99] = 42;
+      buffer.resizeIfNecessary(110);
+
+      Assert.assertEquals(110, buffer.array().length);
+      Assert.assertEquals(110, memoryControl.getUsedMemoryInBytes());
+      Assert.assertEquals(42, buffer.array()[99]);
+      Assert.assertEquals(2, 
AutoResizingBufferMemoryManager.getMemoryAllocationCount());
+      Assert.assertEquals(0, 
AutoResizingBufferMemoryManager.getMemoryAllocationFailureCount());
+    }
+    Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+  }
+
+  @Test
+  public void testKeepPreferredGrowthWhenBudgetAllows() throws Exception {
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+
+    try (AutoResizingBuffer buffer = new AutoResizingBuffer(100)) {
+      buffer.resizeIfNecessary(110);
+      Assert.assertEquals(150, buffer.array().length);
+      Assert.assertEquals(150, memoryControl.getUsedMemoryInBytes());
+      Assert.assertEquals(2, 
AutoResizingBufferMemoryManager.getMemoryAllocationCount());
+    }
+    Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+  }
+
+  @Test
+  public void testShrinkForRequestsAtOrBelowInitialCapacity() throws Exception 
{
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+
+    for (int requestSize : new int[] {0, 1, 100}) {
+      try (AutoResizingBuffer buffer = new AutoResizingBuffer(100)) {
+        buffer.resizeIfNecessary(1000);
+        buffer.array()[99] = 42;
+        for (int i = 0; i <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+          buffer.resizeIfNecessary(requestSize);
+        }
+
+        Assert.assertEquals(550, buffer.array().length);
+        Assert.assertEquals(550, memoryControl.getUsedMemoryInBytes());
+        Assert.assertEquals(42, buffer.array()[99]);
+      }
+      Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+    }
+  }
+
+  @Test
+  public void testSmallFramesReleaseCapacityForAnotherConnection() throws 
Exception {
+    memoryControl.setLimit(800);
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+    AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
+
+    AutoScalingBufferReadTransport first = new 
AutoScalingBufferReadTransport(100);
+    AutoScalingBufferReadTransport second = new 
AutoScalingBufferReadTransport(100);
+    try {
+      TMemoryBuffer input = new TMemoryBuffer(600);
+      byte[] payload = new byte[600];
+      payload[0] = 42;
+      input.write(payload);
+      first.fill(input, payload.length);
+      Assert.assertEquals(700, memoryControl.getUsedMemoryInBytes());
+      first.consumeBuffer(payload.length);
+
+      for (int i = 0; i <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+        input.write(new byte[] {42});
+        first.fill(input, 1);
+        first.consumeBuffer(1);
+      }
+      Assert.assertEquals(450, memoryControl.getUsedMemoryInBytes());
+
+      input.write(payload, 0, 400);
+      second.fill(input, 400);
+      Assert.assertEquals(750, memoryControl.getUsedMemoryInBytes());
+      Assert.assertEquals(42, first.getBuffer()[0]);
+      Assert.assertEquals(42, second.getBuffer()[0]);
+      Assert.assertEquals(400, second.getBytesRemainingInBuffer());
+    } finally {
+      first.close();
+      second.close();
+    }
+    Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+  }
+
+  @Test
+  public void testFramedTransportsShrinkAfterLargeFrames() throws Exception {
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+
+    for (boolean compressed : new boolean[] {false, true}) {
+      TMemoryBuffer underlying = new TMemoryBuffer(0);
+      try (TElasticFramedTransport transport =
+          compressed
+              ? new TSnappyElasticFramedTransport(underlying, 100, 2048, true)
+              : new TElasticFramedTransport(underlying, 100, 2048, true)) {
+        byte[] payload = new byte[1000];
+        payload[999] = 42;
+        transport.write(payload);
+        transport.flush();
+        byte[] actual = new byte[payload.length];
+        transport.readAll(actual, 0, actual.length);
+        Assert.assertArrayEquals(payload, actual);
+        long largeFrameMemory = memoryControl.getUsedMemoryInBytes();
+        byte[] largeWriteBuffer = transport.writeBuffer.getBuffer();
+        byte[] largeReadBuffer = transport.readBuffer.getBuffer();
+        long cooldown = System.currentTimeMillis() + 
RpcUtils.MIN_SHRINK_INTERVAL;
+        setLastShrinkTime(transport.writeBuffer, cooldown);
+        setLastShrinkTime(transport.readBuffer, cooldown);
+
+        for (int i = 0; i <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+          transport.write(new byte[] {42});
+          transport.flush();
+          byte[] smallFrame = new byte[1];
+          transport.readAll(smallFrame, 0, smallFrame.length);
+          Assert.assertEquals(42, smallFrame[0]);
+        }
+        Assert.assertSame(largeWriteBuffer, transport.writeBuffer.getBuffer());
+        Assert.assertSame(largeReadBuffer, transport.readBuffer.getBuffer());
+
+        setLastShrinkTime(transport.writeBuffer, 0);
+        setLastShrinkTime(transport.readBuffer, 0);
+        transport.write(new byte[] {42});
+        transport.flush();
+        byte[] smallFrame = new byte[1];
+        transport.readAll(smallFrame, 0, 1);
+        Assert.assertEquals(42, smallFrame[0]);
+        Assert.assertTrue(transport.writeBuffer.getBuffer().length < 
payload.length);
+        Assert.assertTrue(transport.readBuffer.getBuffer().length < 
payload.length);
+        Assert.assertTrue(transport.writeBuffer.getBuffer().length >= 100);
+        Assert.assertTrue(transport.readBuffer.getBuffer().length >= 100);
+        Assert.assertTrue(memoryControl.getUsedMemoryInBytes() < 
largeFrameMemory);
+
+        byte[] shrunkBuffer = transport.writeBuffer.getBuffer();
+        for (int i = 0; i <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+          transport.write(new byte[] {42});
+          transport.flush();
+          transport.readAll(smallFrame, 0, 1);
+          Assert.assertEquals(42, smallFrame[0]);
+        }
+        Assert.assertSame(shrunkBuffer, transport.writeBuffer.getBuffer());
+      }
+      Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+    }
+  }
+
+  @Test
+  public void testRepeatedLargeFramesKeepWriteCapacity() throws Exception {
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+
+    for (boolean compressed : new boolean[] {false, true}) {
+      try (TElasticFramedTransport transport = newFramedTransport(compressed)) 
{
+        byte[] payload = new byte[1000];
+        new Random(0).nextBytes(payload);
+        transport.write(payload);
+        transport.flush();
+        byte[] actual = new byte[payload.length];
+        transport.readAll(actual, 0, actual.length);
+        Assert.assertArrayEquals(payload, actual);
+        byte[] writeBuffer = transport.writeBuffer.getBuffer();
+        for (int i = 0; i < 3 * (RpcUtils.MAX_BUFFER_OVERSIZE_TIME + 1); i++) {
+          setLastShrinkTime(transport.writeBuffer, 0);
+          long allocationCount = 
AutoResizingBufferMemoryManager.getMemoryAllocationCount();
+          transport.write(payload);
+          transport.flush();
+          Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+          Assert.assertEquals(
+              allocationCount, 
AutoResizingBufferMemoryManager.getMemoryAllocationCount());
+          transport.readAll(actual, 0, actual.length);
+          Assert.assertArrayEquals(payload, actual);
+        }
+      }
+      Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+    }
+  }
+
+  @Test
+  public void testSmallWritesDoNotShrinkAnUnfinishedFrame() throws Exception {
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+
+    for (boolean compressed : new boolean[] {false, true}) {
+      try (TElasticFramedTransport transport = newFramedTransport(compressed)) 
{
+        byte[] payload = new byte[1000];
+        new Random(0).nextBytes(payload);
+        transport.write(payload);
+        transport.flush();
+        byte[] actual = new byte[payload.length];
+        transport.readAll(actual, 0, actual.length);
+        Assert.assertArrayEquals(payload, actual);
+        byte[] writeBuffer = transport.writeBuffer.getBuffer();
+        setLastShrinkTime(transport.writeBuffer, 0);
+
+        int prefixLength = RpcUtils.MAX_BUFFER_OVERSIZE_TIME + 1;
+        for (int i = 0; i < prefixLength; i++) {
+          transport.write(payload, i, 1);
+          Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+          Assert.assertEquals(i + 1, transport.writeBuffer.getPos());
+        }
+        transport.write(payload, prefixLength, payload.length - prefixLength);
+        Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+        transport.flush();
+        transport.readAll(actual, 0, actual.length);
+        Assert.assertArrayEquals(payload, actual);
+      }
+      Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+    }
+  }
+
+  @Test
+  public void testLargeFrameReuseDoesNotStarveAnotherConnection() throws 
Exception {
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+    AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
+
+    for (boolean compressed : new boolean[] {false, true}) {
+      memoryControl.setLimit(Long.MAX_VALUE);
+      try (TElasticFramedTransport transport = newFramedTransport(compressed);
+          AutoScalingBufferReadTransport anotherConnection =
+              new AutoScalingBufferReadTransport(100)) {
+        byte[] payload = new byte[1000];
+        new Random(0).nextBytes(payload);
+        transport.write(payload);
+        transport.flush();
+        byte[] writeBuffer = transport.writeBuffer.getBuffer();
+        long retainedMemory = memoryControl.getUsedMemoryInBytes();
+        // Only leave enough quota for the other connection to grow from 100 
to 200 bytes.
+        memoryControl.setLimit(retainedMemory + 100);
+        for (int i = 0; i < 3 * (RpcUtils.MAX_BUFFER_OVERSIZE_TIME + 1); i++) {
+          setLastShrinkTime(transport.writeBuffer, 0);
+          transport.write(payload, 0, 600);
+          transport.write(payload, 600, 400);
+          transport.flush();
+          Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+        }
+        transport.write(payload, 0, 600);
+        transport.write(payload, 600, 300);
+        Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+        TMemoryBuffer input = new TMemoryBuffer(0);
+        input.write(payload, 0, 200);
+        anotherConnection.fill(input, 200);
+
+        Assert.assertEquals(0, 
AutoResizingBufferMemoryManager.getMemoryAllocationFailureCount());
+        Assert.assertEquals(retainedMemory + 100, 
memoryControl.getUsedMemoryInBytes());
+        Assert.assertArrayEquals(
+            Arrays.copyOf(payload, 200), 
Arrays.copyOf(anotherConnection.getBuffer(), 200));
+        Assert.assertArrayEquals(
+            Arrays.copyOf(payload, 900), 
Arrays.copyOf(transport.writeBuffer.getBuffer(), 900));
+      }
+      Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+    }
+  }
+
+  @Test
+  public void testFailedIncrementalGrowthPreservesFrameAndAccounting() throws 
Exception {
+    memoryControl.setLimit(120);
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+    AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
+
+    try (AutoScalingBufferWriteTransport transport = new 
AutoScalingBufferWriteTransport(100)) {
+      byte[] payload = new byte[130];
+      new Random(0).nextBytes(payload);
+      transport.write(payload, 0, 90);
+      byte[] originalBuffer = transport.getBuffer();
+      try {
+        transport.write(payload, 90, 40);
+        Assert.fail("Expected TTransportException");
+      } catch (TTransportException expected) {
+        Assert.assertSame(originalBuffer, transport.getBuffer());
+        Assert.assertEquals(90, transport.getPos());
+        Assert.assertEquals(100, memoryControl.getUsedMemoryInBytes());
+        Assert.assertArrayEquals(
+            Arrays.copyOf(payload, 90), Arrays.copyOf(transport.getBuffer(), 
90));
+      }
+
+      memoryControl.setLimit(130);
+      transport.write(payload, 90, 40);
+      Assert.assertEquals(130, transport.getBuffer().length);
+      Assert.assertEquals(130, transport.getPos());
+      Assert.assertEquals(130, memoryControl.getUsedMemoryInBytes());
+      Assert.assertArrayEquals(payload, transport.getBuffer());
+      Assert.assertEquals(1, 
AutoResizingBufferMemoryManager.getMemoryAllocationFailureCount());
+      transport.close();
+      Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+    }
+    Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+  }
+
+  @Test
+  public void testConcurrentReadBuffersEnforceSharedBudget() throws Exception {
+    memoryControl.setLimit(1000);
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+    AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
+
+    AutoScalingBufferReadTransport first = new 
AutoScalingBufferReadTransport(100);
+    AutoScalingBufferReadTransport second = new 
AutoScalingBufferReadTransport(100);
+    AutoScalingBufferReadTransport third = new 
AutoScalingBufferReadTransport(100);
+    try {
+      byte[] payload = new byte[400];
+      payload[399] = 42;
+      TMemoryBuffer input = new TMemoryBuffer(0);
+      input.write(payload);
+      first.fill(input, payload.length);
+      input.write(payload);
+      second.fill(input, payload.length);
+      Assert.assertEquals(900, memoryControl.getUsedMemoryInBytes());
+
+      input.write(payload);
+      try {
+        third.fill(input, payload.length);
+        Assert.fail("Expected TTransportException");
+      } catch (TTransportException expected) {
+        Assert.assertEquals(900, memoryControl.getUsedMemoryInBytes());
+        Assert.assertEquals(1, 
AutoResizingBufferMemoryManager.getMemoryAllocationFailureCount());
+      }
+
+      first.close();
+      third.fill(input, payload.length);
+      Assert.assertEquals(800, memoryControl.getUsedMemoryInBytes());
+      Assert.assertEquals(42, second.getBuffer()[399]);
+      Assert.assertEquals(42, third.getBuffer()[399]);
+    } finally {
+      first.close();
+      second.close();
+      third.close();
+    }
+    Assert.assertEquals(0, memoryControl.getUsedMemoryInBytes());
+  }
+
   @Test
   public void testThrowIOExceptionWhenMemoryIsInsufficient() throws Exception {
     memoryControl.setLimit(120);
@@ -67,6 +399,7 @@ public class AutoResizingBufferTest {
     AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
 
     AutoResizingBuffer buffer = new AutoResizingBuffer(100);
+    buffer.array()[99] = 42;
     try {
       buffer.resizeIfNecessary(200);
       Assert.fail("Expected IOException");
@@ -74,6 +407,8 @@ public class AutoResizingBufferTest {
       Assert.assertTrue(e.getMessage().contains("100"));
       Assert.assertTrue(e.getMessage().contains("5"));
     } finally {
+      Assert.assertEquals(100, buffer.array().length);
+      Assert.assertEquals(42, buffer.array()[99]);
       Assert.assertEquals(100, memoryControl.getUsedMemoryInBytes());
       buffer.close();
     }
@@ -195,6 +530,18 @@ public class AutoResizingBufferTest {
     field.set(buffer, lastShrinkTime);
   }
 
+  private void setLastShrinkTime(Object transport, long lastShrinkTime) throws 
Exception {
+    Field field = transport.getClass().getDeclaredField("buf");
+    field.setAccessible(true);
+    setLastShrinkTime((AutoResizingBuffer) field.get(transport), 
lastShrinkTime);
+  }
+
+  private TElasticFramedTransport newFramedTransport(boolean compressed) 
throws Exception {
+    return compressed
+        ? new TSnappyElasticFramedTransport(new TMemoryBuffer(0), 100, 2048, 
true)
+        : new TElasticFramedTransport(new TMemoryBuffer(0), 100, 2048, true);
+  }
+
   private static class TestMemoryControl implements 
AutoResizingBufferMemoryControl {
 
     private final AtomicLong usedMemoryInBytes = new AtomicLong();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfo.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfo.java
index 5491f113ba7..75c7a73a42b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfo.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfo.java
@@ -448,8 +448,12 @@ public class SystemInfo {
       }
       TsFileProcessor selectedTsFileProcessor = allTsFileProcessors.peek();
       memCost += selectedTsFileProcessor.getWorkMemTableRamCost();
-      selectedTsFileProcessor.setWorkMemTableShouldFlush();
-      
flushTaskSubmitThreadPool.submit(selectedTsFileProcessor::submitAFlushTask);
+      // A marked memtable already has a flush pending. Count its memory 
towards the target,
+      // but do not enqueue another task on every report while the region 
write lock is held.
+      if (!selectedTsFileProcessor.getWorkMemTable().shouldFlush()) {
+        selectedTsFileProcessor.setWorkMemTableShouldFlush();
+        
flushTaskSubmitThreadPool.submit(selectedTsFileProcessor::submitAFlushTask);
+      }
       if (selectedTsFileProcessor == currentTsFileProcessor) {
         isCurrentTsFileProcessorSelected = true;
       }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
new file mode 100644
index 00000000000..8c5e3e362d5
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.storageengine.rescon.memory;
+
+import org.apache.iotdb.db.exception.WriteProcessRejectException;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegionInfo;
+import org.apache.iotdb.db.storageengine.dataregion.memtable.IMemTable;
+import org.apache.iotdb.db.storageengine.dataregion.memtable.TsFileProcessor;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SystemInfoTest {
+
+  private SystemInfo systemInfo;
+  private DataRegionInfo dataRegionInfo;
+  private TsFileProcessor currentProcessor;
+  private TsFileProcessor largerProcessor;
+  private ExecutorService originalFlushSubmitter;
+  private ExecutorService flushSubmitter;
+
+  @Before
+  public void setUp() throws Exception {
+    systemInfo = SystemInfo.getInstance();
+    systemInfo.close();
+    setMemoryThreshold("memorySizeForMemtable", 1000L);
+    setMemoryThreshold("FLUSH_THRESHOLD", 500.0);
+    setMemoryThreshold("REJECT_THRESHOLD", 800.0);
+    Field field = 
SystemInfo.class.getDeclaredField("flushTaskSubmitThreadPool");
+    field.setAccessible(true);
+    originalFlushSubmitter = (ExecutorService) field.get(systemInfo);
+    flushSubmitter = mock(ExecutorService.class);
+    field.set(systemInfo, flushSubmitter);
+
+    DataRegion dataRegion = mock(DataRegion.class);
+    when(dataRegion.getDatabaseName()).thenReturn("root.test");
+    dataRegionInfo = new DataRegionInfo(dataRegion);
+    currentProcessor = mock(TsFileProcessor.class);
+    largerProcessor = mock(TsFileProcessor.class);
+    mockWorkMemTable(currentProcessor);
+    mockWorkMemTable(largerProcessor);
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(100L);
+    when(largerProcessor.getWorkMemTableRamCost()).thenReturn(600L);
+    dataRegionInfo.initTsFileProcessorInfo(currentProcessor);
+    dataRegionInfo.initTsFileProcessorInfo(largerProcessor);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    setMemoryThreshold("flushTaskSubmitThreadPool", originalFlushSubmitter);
+    systemInfo.close();
+    systemInfo.loadWriteMemory();
+  }
+
+  @Test
+  public void testRejectedWriterCannotFlushSmallerCurrentProcessor() throws 
Exception {
+    dataRegionInfo.addStorageGroupMemCost(900);
+
+    Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    Assert.assertTrue(systemInfo.isRejected());
+    verify(currentProcessor, never()).setWorkMemTableShouldFlush();
+    verify(largerProcessor).setWorkMemTableShouldFlush();
+  }
+
+  @Test
+  public void testRejectedWriterFirstTabletCannotBypassSelection() throws 
Exception {
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(0L);
+    dataRegionInfo.addStorageGroupMemCost(900);
+
+    Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    verify(currentProcessor, never()).setWorkMemTableShouldFlush();
+    verify(largerProcessor).setWorkMemTableShouldFlush();
+  }
+
+  @Test
+  public void testRepeatedReportsSubmitEachMemtableOnce() throws Exception {
+    dataRegionInfo.addStorageGroupMemCost(900);
+
+    for (int i = 0; i < 10; i++) {
+      Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    }
+    Assert.assertTrue(systemInfo.isRejected());
+    verify(currentProcessor, never()).setWorkMemTableShouldFlush();
+    verify(largerProcessor).setWorkMemTableShouldFlush();
+    verify(flushSubmitter).submit(any(Runnable.class));
+  }
+
+  @Test
+  public void testRepeatedReportsForFirstTabletSubmitOnce() throws Exception {
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(0L);
+    dataRegionInfo.addStorageGroupMemCost(900);
+
+    for (int i = 0; i < 10; i++) {
+      Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    }
+    verify(currentProcessor, never()).setWorkMemTableShouldFlush();
+    verify(largerProcessor).setWorkMemTableShouldFlush();
+    verify(flushSubmitter).submit(any(Runnable.class));
+  }
+
+  @Test
+  public void testNewMemtableCanBeScheduledAfterPreviousFlush() throws 
Exception {
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(700L);
+    when(largerProcessor.getWorkMemTableRamCost()).thenReturn(100L);
+    dataRegionInfo.addStorageGroupMemCost(900);
+    Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+
+    mockWorkMemTable(currentProcessor);
+    Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    verify(currentProcessor, times(2)).setWorkMemTableShouldFlush();
+    verify(largerProcessor, never()).setWorkMemTableShouldFlush();
+    verify(flushSubmitter, times(2)).submit(any(Runnable.class));
+  }
+
+  @Test
+  public void testMarkedCurrentProcessorCannotReachHardMemoryLimit() throws 
Exception {
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(700L);
+    when(largerProcessor.getWorkMemTableRamCost()).thenReturn(100L);
+    dataRegionInfo.addStorageGroupMemCost(900);
+    Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    dataRegionInfo.addStorageGroupMemCost(100);
+
+    try {
+      systemInfo.reportStorageGroupStatus(dataRegionInfo, currentProcessor);
+      Assert.fail("Expected WriteProcessRejectException");
+    } catch (WriteProcessRejectException expected) {
+      Assert.assertTrue(systemInfo.isRejected());
+      verify(currentProcessor).setWorkMemTableShouldFlush();
+      verify(flushSubmitter).submit(any(Runnable.class));
+    }
+  }
+
+  @Test
+  public void testNoCurrentMemtableCannotBypassRejection() throws Exception {
+    when(currentProcessor.getWorkMemTable()).thenReturn(null);
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(0L);
+    dataRegionInfo.addStorageGroupMemCost(900);
+
+    Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    Assert.assertTrue(systemInfo.isRejected());
+    verify(currentProcessor, never()).setWorkMemTableShouldFlush();
+    verify(largerProcessor).setWorkMemTableShouldFlush();
+  }
+
+  @Test
+  public void testRejectionClearsAfterMemoryIsReleased() throws Exception {
+    dataRegionInfo.addStorageGroupMemCost(900);
+    Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+
+    dataRegionInfo.releaseStorageGroupMemCost(500);
+    systemInfo.resetStorageGroupStatus(dataRegionInfo);
+    Assert.assertFalse(systemInfo.isRejected());
+    Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    verify(flushSubmitter).submit(any(Runnable.class));
+  }
+
+  @Test
+  public void testCurrentProcessorCannotExceedHardMemoryLimit() throws 
Exception {
+    when(currentProcessor.getWorkMemTableRamCost()).thenReturn(700L);
+    when(largerProcessor.getWorkMemTableRamCost()).thenReturn(100L);
+    dataRegionInfo.addStorageGroupMemCost(1100);
+
+    try {
+      systemInfo.reportStorageGroupStatus(dataRegionInfo, currentProcessor);
+      Assert.fail("Expected WriteProcessRejectException");
+    } catch (WriteProcessRejectException expected) {
+      Assert.assertTrue(systemInfo.isRejected());
+    }
+  }
+
+  @Test
+  public void testNormalFlushStillSelectsLargestMemtable() throws Exception {
+    dataRegionInfo.addStorageGroupMemCost(700);
+
+    Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
+    Assert.assertFalse(systemInfo.isRejected());
+    verify(largerProcessor).setWorkMemTableShouldFlush();
+    verify(currentProcessor, never()).setWorkMemTableShouldFlush();
+  }
+
+  private void setMemoryThreshold(String fieldName, Object value) throws 
Exception {
+    Field field = SystemInfo.class.getDeclaredField(fieldName);
+    field.setAccessible(true);
+    field.set(systemInfo, value);
+  }
+
+  private void mockWorkMemTable(TsFileProcessor processor) {
+    IMemTable memTable = mock(IMemTable.class);
+    AtomicBoolean marked = new AtomicBoolean();
+    when(memTable.shouldFlush()).thenAnswer(invocation -> marked.get());
+    when(processor.getWorkMemTable()).thenReturn(memTable);
+    doAnswer(
+            invocation -> {
+              marked.set(true);
+              return null;
+            })
+        .when(processor)
+        .setWorkMemTableShouldFlush();
+  }
+}

Reply via email to