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 221f79c5fae [To dev/1.3] Fix RPC buffer resizing and duplicate flush 
submissions (#18918) (#18971)
221f79c5fae is described below

commit 221f79c5fae7b63d683783db80657a19b60fb6ea
Author: Caideyipi <[email protected]>
AuthorDate: Thu Oct 22 11:41:24 2026 +0800

    [To dev/1.3] Fix RPC buffer resizing and duplicate flush submissions 
(#18918) (#18971)
    
    Backport the applicable changes from #18918 to the dev/1.3 buffer and
    memory-admission implementations. Incremental serialization only grows
    buffers; complete frame sizes drive shrinking in ordinary and compressed
    transports. Count marked memtables without resubmitting pending flushes.
    
    Adapt the regression coverage to 1.3 and retain its existing admission
    decisions. The RPC memory-budget fallback is omitted because this branch
    has no AutoResizingBufferMemoryManager.
    
    (cherry picked from commit 83d419796fb91a6cccb874414820d3865ab34b98)
---
 .../org/apache/iotdb/rpc/AutoResizingBuffer.java   |  34 +--
 .../iotdb/rpc/AutoScalingBufferWriteTransport.java |   4 +-
 .../rpc/TCompressedElasticFramedTransport.java     |   4 +-
 .../apache/iotdb/rpc/TElasticFramedTransport.java  |   4 +-
 .../apache/iotdb/rpc/AutoResizingBufferTest.java   | 221 ++++++++++++++++++++
 .../db/storageengine/rescon/memory/SystemInfo.java |   8 +-
 .../rescon/memory/SystemInfoTest.java              | 232 +++++++++++++++++++++
 7 files changed, 484 insertions(+), 23 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 180d05b3d93..ca47d80acae 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
@@ -22,14 +22,15 @@ package org.apache.iotdb.rpc;
 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. 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 {
 
@@ -44,22 +45,27 @@ class AutoResizingBuffer {
   }
 
   public void resizeIfNecessary(int size) {
+    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) {
+      array = Arrays.copyOf(array, size + (currentCapacity - size) / 2);
+      bufTooLargeCounter = RpcUtils.MAX_BUFFER_OVERSIZE_TIME;
+      lastShrinkTime = System.currentTimeMillis();
+    }
+  }
+
+  void ensureCapacity(int size) {
+    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);
       this.array = Arrays.copyOf(array, newCapacity);
       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
-      array = Arrays.copyOf(array, size + (currentCapacity - size) / 2);
-      bufTooLargeCounter = RpcUtils.MAX_BUFFER_OVERSIZE_TIME;
-      lastShrinkTime = System.currentTimeMillis();
     }
   }
 
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 ae833fad239..9e57e53768f 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
@@ -44,7 +44,9 @@ public class AutoScalingBufferWriteTransport extends 
NonOpenTransport {
 
   @Override
   public void write(byte[] toWrite, int off, int len) {
-    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);
     System.arraycopy(toWrite, off, buf.array(), pos, len);
     pos += len;
   }
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 07768dd98bd..ab00ca2ac63 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
@@ -79,9 +79,7 @@ public abstract class TCompressedElasticFramedTransport 
extends TElasticFramedTr
     }
 
     writeBuffer.reset();
-    if (thriftDefaultBufferSize < length) {
-      writeBuffer.resizeIfNecessary(thriftDefaultBufferSize);
-    }
+    writeBuffer.resizeIfNecessary(length);
     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 40811a00f41..cbc74e22f42 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
@@ -260,9 +260,7 @@ public class TElasticFramedTransport extends TTransport {
     underlying.write(i32buf, 0, 4);
     underlying.write(writeBuffer.getBuffer(), 0, length);
     writeBuffer.reset();
-    if (length > thriftDefaultBufferSize) {
-      writeBuffer.resizeIfNecessary(thriftDefaultBufferSize);
-    }
+    writeBuffer.resizeIfNecessary(length);
     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
new file mode 100644
index 00000000000..d4ddaa00313
--- /dev/null
+++ 
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/AutoResizingBufferTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.rpc;
+
+import org.apache.thrift.transport.TMemoryBuffer;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.Random;
+
+public class AutoResizingBufferTest {
+
+  @Test
+  public void testGrowthPreservesDataAndHeadroom() {
+    AutoResizingBuffer buffer = new AutoResizingBuffer(100);
+    buffer.array()[99] = 42;
+    buffer.resizeIfNecessary(110);
+    Assert.assertEquals(150, buffer.array().length);
+    Assert.assertEquals(42, buffer.array()[99]);
+
+    buffer.resizeIfNecessary(1000);
+    Assert.assertEquals(1000, buffer.array().length);
+    Assert.assertEquals(42, buffer.array()[99]);
+  }
+
+  @Test
+  public void testShrinkForRequestsAtOrBelowInitialCapacity() throws Exception 
{
+    for (int requestSize : new int[] {0, 1, 100}) {
+      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(42, buffer.array()[99]);
+      for (int i = 0; i < 20; i++) {
+        setLastShrinkTime(buffer, 0);
+        for (int j = 0; j <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; j++) {
+          buffer.resizeIfNecessary(requestSize);
+        }
+        Assert.assertTrue(buffer.array().length >= 100);
+      }
+    }
+  }
+
+  @Test
+  public void testSmallFramesRespectShrinkCountAndCooldown() throws Exception {
+    AutoResizingBuffer buffer = new AutoResizingBuffer(100);
+    buffer.resizeIfNecessary(1000);
+    byte[] largeBuffer = buffer.array();
+    setLastShrinkTime(buffer, System.currentTimeMillis() + 
RpcUtils.MIN_SHRINK_INTERVAL);
+
+    for (int i = 0; i <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+      buffer.resizeIfNecessary(101);
+    }
+    Assert.assertSame(largeBuffer, buffer.array());
+
+    setLastShrinkTime(buffer, 0);
+    buffer.resizeIfNecessary(101);
+    Assert.assertEquals(550, buffer.array().length);
+    byte[] shrunkBuffer = buffer.array();
+    for (int i = 0; i <= RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+      buffer.resizeIfNecessary(101);
+    }
+    Assert.assertSame(shrunkBuffer, buffer.array());
+
+    AutoResizingBuffer countLimitedBuffer = new AutoResizingBuffer(100);
+    countLimitedBuffer.resizeIfNecessary(1000);
+    byte[] originalBuffer = countLimitedBuffer.array();
+    for (int i = 0; i < RpcUtils.MAX_BUFFER_OVERSIZE_TIME; i++) {
+      countLimitedBuffer.resizeIfNecessary(101);
+      Assert.assertSame(originalBuffer, countLimitedBuffer.array());
+    }
+    countLimitedBuffer.resizeIfNecessary(101);
+    Assert.assertEquals(550, countLimitedBuffer.array().length);
+  }
+
+  @Test
+  public void testFramedTransportsShrinkAfterLargeFrames() throws Exception {
+    for (boolean compressed : new boolean[] {false, true}) {
+      try (TElasticFramedTransport transport = newFramedTransport(compressed)) 
{
+        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);
+        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);
+
+        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());
+      }
+    }
+  }
+
+  @Test
+  public void testRepeatedLargeFramesKeepWriteCapacity() throws Exception {
+    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);
+          transport.write(payload, 0, 200);
+          transport.write(payload, 200, payload.length - 200);
+          transport.flush();
+          Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+          transport.readAll(actual, 0, actual.length);
+          Assert.assertArrayEquals(payload, actual);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testSmallWritesDoNotShrinkAnUnfinishedFrame() throws Exception {
+    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 fragmentLength = 32;
+        int prefixLength = fragmentLength * (RpcUtils.MAX_BUFFER_OVERSIZE_TIME 
+ 5);
+        for (int offset = 0; offset < prefixLength; offset += fragmentLength) {
+          transport.write(payload, offset, fragmentLength);
+          Assert.assertSame(writeBuffer, transport.writeBuffer.getBuffer());
+          Assert.assertEquals(offset + fragmentLength, 
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);
+      }
+    }
+  }
+
+  private void setLastShrinkTime(AutoResizingBuffer buffer, long 
lastShrinkTime) throws Exception {
+    Field field = AutoResizingBuffer.class.getDeclaredField("lastShrinkTime");
+    field.setAccessible(true);
+    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);
+  }
+}
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 e8cd62b6d16..38d0c75c073 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
@@ -493,8 +493,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..934ea252d5d
--- /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.allocateWriteMemory();
+  }
+
+  @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