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 1b290ae320c [Bugfix] Reclaim RPC buffers by evicting idle clients 
(#18853)
1b290ae320c is described below

commit 1b290ae320c56ee7b2693564dd9cb98bdef80c74
Author: Caideyipi <[email protected]>
AuthorDate: Thu Oct 15 11:06:31 2026 +0800

    [Bugfix] Reclaim RPC buffers by evicting idle clients (#18853)
    
    * [Bugfix] Reclaim idle RPC buffers when the shared pool is exhausted
    
    * [Bugfix] Evict idle RPC clients to release buffers
---
 .../client/property/ClientPoolProperty.java        |   5 +-
 .../iotdb/rpc/ClientPoolIdleEvictionTest.java      | 326 +++++++++++++++++++++
 2 files changed, 330 insertions(+), 1 deletion(-)

diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/property/ClientPoolProperty.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/property/ClientPoolProperty.java
index 1f818afe543..a299cc056a3 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/property/ClientPoolProperty.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/client/property/ClientPoolProperty.java
@@ -101,6 +101,9 @@ public class ClientPoolProperty<V> {
       poolConfig.setMaxIdlePerKey(maxIdleClientNumForEachNode);
       
poolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(timeBetweenEvictionRuns));
       
poolConfig.setMinEvictableIdleTime(Duration.ofMillis(minIdleTimeForClient));
+      // -1 checks all idle clients across all nodes in each run. The default 
of 3 can leave
+      // expired connections (and their RPC buffers) in a large pool for hours.
+      poolConfig.setNumTestsPerEvictionRun(-1);
       poolConfig.setMaxWait(Duration.ofMillis(waitClientTimeoutMs));
       poolConfig.setTestOnReturn(true);
       poolConfig.setTestOnBorrow(true);
@@ -114,7 +117,7 @@ public class ClientPoolProperty<V> {
 
     public static final long WAIT_CLIENT_TIMEOUT_MS = 
TimeUnit.SECONDS.toMillis(30);
     public static final long MIN_IDLE_TIME_FOR_CLIENT_MS = 
TimeUnit.MINUTES.toMillis(1);
-    public static final long TIME_BETWEEN_EVICTION_RUNS_MS = 
TimeUnit.MINUTES.toMillis(1);
+    public static final long TIME_BETWEEN_EVICTION_RUNS_MS = 
TimeUnit.SECONDS.toMillis(30);
     public static final int MAX_CLIENT_NUM_FOR_EACH_NODE = 1000;
     public static final int MAX_IDLE_CLIENT_NUM_FOR_EACH_NODE = 1000;
   }
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/rpc/ClientPoolIdleEvictionTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/rpc/ClientPoolIdleEvictionTest.java
new file mode 100644
index 00000000000..dca5c7eb441
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/rpc/ClientPoolIdleEvictionTest.java
@@ -0,0 +1,326 @@
+/*
+ * 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.iotdb.commons.client.property.ClientPoolProperty;
+
+import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.protocol.TProtocol;
+import org.apache.thrift.server.TServer;
+import org.apache.thrift.server.TThreadPoolServer;
+import org.apache.thrift.transport.TMemoryBuffer;
+import org.apache.thrift.transport.TServerSocket;
+import org.apache.thrift.transport.TSocket;
+import org.apache.thrift.transport.TTransport;
+import org.apache.thrift.transport.TTransportException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class ClientPoolIdleEvictionTest {
+
+  private static final int INITIAL_BUFFER_SIZE = 32;
+  private static final byte[] PAYLOAD = new byte[4096];
+
+  private final boolean compression;
+  private final TestMemoryControl memoryControl = new TestMemoryControl();
+
+  public ClientPoolIdleEvictionTest(boolean compression) {
+    this.compression = compression;
+  }
+
+  @Parameterized.Parameters(name = "compression={0}")
+  public static Iterable<Object[]> parameters() {
+    return Arrays.asList(new Object[][] {{false}, {true}});
+  }
+
+  @Before
+  public void setUp() {
+    AutoResizingBufferMemoryManager.resetMemoryControl();
+    AutoResizingBufferMemoryManager.setMemoryControl(memoryControl);
+    AutoResizingBufferMemoryManager.setMemoryAllocateRetryIntervalInMs(1);
+    Arrays.fill(PAYLOAD, (byte) 13);
+  }
+
+  @After
+  public void tearDown() {
+    AutoResizingBufferMemoryManager.resetMemoryControl();
+  }
+
+  @Test
+  public void testOneRunReclaimsAllExpiredClientsAcrossNodes() throws 
Exception {
+    try (GenericKeyedObjectPool<Integer, TElasticFramedTransport> pool =
+        createPool(100, -1, null)) {
+      TElasticFramedTransport active = pool.borrowObject(0);
+      long activeMemory = memoryControl.used.get();
+      try {
+        List<TElasticFramedTransport> idleClients = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+          idleClients.add(pool.borrowObject(i % 2));
+        }
+        for (int i = 0; i < idleClients.size(); i++) {
+          pool.returnObject(i % 2, idleClients.get(i));
+        }
+
+        long retainedMemory = memoryControl.used.get();
+        memoryControl.limit = retainedMemory;
+        assertThrows(TTransportException.class, () -> pool.borrowObject(2));
+        assertEquals(retainedMemory, memoryControl.used.get());
+
+        // Wait for the idle timeout with the background evictor disabled, 
then run one sweep.
+        waitForIdleTimeout();
+        pool.evict();
+
+        assertEquals(0, pool.getNumIdle());
+        assertEquals(10, pool.getDestroyedCount());
+        assertEquals(1, pool.getNumActive());
+        assertEquals(activeMemory, memoryControl.used.get());
+
+        // Eviction must preserve a borrowed client's frame, and free quota 
for a new client.
+        byte[] actual = new byte[PAYLOAD.length];
+        active.readAll(actual, 0, actual.length);
+        assertArrayEquals(PAYLOAD, actual);
+        TElasticFramedTransport replacement = pool.borrowObject(2);
+        pool.returnObject(2, replacement);
+      } finally {
+        pool.returnObject(0, active);
+      }
+    }
+    assertEquals(0, memoryControl.used.get());
+  }
+
+  @Test
+  public void testBackgroundEvictionReleasesIdleBufferMemory() throws 
Exception {
+    try (GenericKeyedObjectPool<Integer, TElasticFramedTransport> pool =
+        createPool(100, 20, null)) {
+      TElasticFramedTransport client = pool.borrowObject(0);
+      pool.returnObject(0, client);
+
+      await()
+          .atMost(5, TimeUnit.SECONDS)
+          .pollInterval(Duration.ofMillis(10))
+          .until(() -> pool.getNumIdle() == 0 && memoryControl.used.get() == 
0);
+      assertEquals(1, pool.getDestroyedCount());
+    }
+    assertEquals(0, memoryControl.used.get());
+  }
+
+  @Test
+  public void testUnexpiredClientRemainsReusable() throws Exception {
+    try (GenericKeyedObjectPool<Integer, TElasticFramedTransport> pool =
+        createPool(TimeUnit.MINUTES.toMillis(1), -1, null)) {
+      TElasticFramedTransport client = pool.borrowObject(0);
+      pool.returnObject(0, client);
+      long retainedMemory = memoryControl.used.get();
+
+      pool.evict();
+      assertEquals(1, pool.getNumIdle());
+      assertEquals(0, pool.getDestroyedCount());
+      assertEquals(retainedMemory, memoryControl.used.get());
+
+      TElasticFramedTransport reused = pool.borrowObject(0);
+      try {
+        assertSame(client, reused);
+        byte[] actual = new byte[PAYLOAD.length];
+        reused.readAll(actual, 0, actual.length);
+        assertArrayEquals(PAYLOAD, actual);
+      } finally {
+        pool.returnObject(0, reused);
+      }
+    }
+    assertEquals(0, memoryControl.used.get());
+  }
+
+  @Test(timeout = 20000)
+  public void testEvictionReleasesServerBuffersForNewConnections() throws 
Exception {
+    try (ServerSocket socket = new ServerSocket()) {
+      socket.bind(new InetSocketAddress("127.0.0.1", 0));
+      ExecutorService workers = Executors.newFixedThreadPool(2);
+      TServer server =
+          new TThreadPoolServer(
+              new TThreadPoolServer.Args(new TServerSocket(socket))
+                  .executorService(workers)
+                  .transportFactory(
+                      compression
+                          ? new TSnappyElasticFramedTransport.Factory(
+                              INITIAL_BUFFER_SIZE, 65536, true)
+                          : new 
TElasticFramedTransport.Factory(INITIAL_BUFFER_SIZE, 65536, true))
+                  .protocolFactory(new TBinaryProtocol.Factory())
+                  .processor(
+                      (input, output) -> {
+                        output.writeBinary(input.readBinary());
+                        output.getTransport().flush();
+                      }));
+      Thread serverThread = new Thread(server::serve, 
"client-pool-idle-eviction-test-server");
+      serverThread.setDaemon(true);
+      serverThread.start();
+
+      try (GenericKeyedObjectPool<Integer, TElasticFramedTransport> pool =
+          createPool(100, -1, socket.getLocalPort())) {
+        TElasticFramedTransport first = pool.borrowObject(0);
+        try {
+          echo(first, PAYLOAD);
+        } finally {
+          pool.returnObject(0, first);
+        }
+        memoryControl.limit = memoryControl.used.get();
+        assertThrows(TTransportException.class, () -> pool.borrowObject(1));
+
+        waitForIdleTimeout();
+        pool.evict();
+        assertFalse(first.isOpen());
+        // The server must observe EOF and close its own read/write and 
compression buffers.
+        await()
+            .atMost(5, TimeUnit.SECONDS)
+            .pollInterval(Duration.ofMillis(10))
+            .until(() -> memoryControl.used.get() == 0);
+
+        TElasticFramedTransport replacement = pool.borrowObject(1);
+        try {
+          echo(replacement, new byte[] {1, 2, 3, 4});
+        } finally {
+          pool.returnObject(1, replacement);
+        }
+      } finally {
+        server.stop();
+        workers.shutdownNow();
+        assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
+        serverThread.join(5000);
+        assertFalse(serverThread.isAlive());
+      }
+    }
+    assertEquals(0, memoryControl.used.get());
+  }
+
+  private static void echo(TElasticFramedTransport transport, byte[] payload) 
throws Exception {
+    TProtocol protocol = new TBinaryProtocol(transport);
+    protocol.writeBinary(ByteBuffer.wrap(payload));
+    transport.flush();
+    assertEquals(ByteBuffer.wrap(payload), protocol.readBinary());
+  }
+
+  private static void waitForIdleTimeout() {
+    long returnedAt = System.nanoTime();
+    await()
+        .atMost(5, TimeUnit.SECONDS)
+        .pollInterval(Duration.ofMillis(10))
+        .until(() -> System.nanoTime() - returnedAt > 
TimeUnit.MILLISECONDS.toNanos(200));
+  }
+
+  private GenericKeyedObjectPool<Integer, TElasticFramedTransport> createPool(
+      long idleTimeoutMs, long evictionIntervalMs, Integer serverPort) {
+    return new GenericKeyedObjectPool<>(
+        new BaseKeyedPooledObjectFactory<Integer, TElasticFramedTransport>() {
+          @Override
+          public TElasticFramedTransport create(Integer node) throws Exception 
{
+            TElasticFramedTransport transport =
+                createTransport(
+                    serverPort == null
+                        ? new TMemoryBuffer(0)
+                        : new TSocket("127.0.0.1", serverPort, 5000));
+            try {
+              if (serverPort == null) {
+                transport.write(PAYLOAD, 0, PAYLOAD.length);
+                transport.flush();
+              } else {
+                transport.open();
+              }
+              return transport;
+            } catch (Exception e) {
+              transport.close();
+              throw e;
+            }
+          }
+
+          @Override
+          public PooledObject<TElasticFramedTransport> 
wrap(TElasticFramedTransport transport) {
+            return new DefaultPooledObject<>(transport);
+          }
+
+          @Override
+          public void destroyObject(Integer node, 
PooledObject<TElasticFramedTransport> transport) {
+            transport.getObject().close();
+          }
+        },
+        new ClientPoolProperty.Builder<TElasticFramedTransport>()
+            .setMaxClientNumForEachNode(16)
+            .setMaxIdleClientNumForEachNode(16)
+            .setMinIdleTimeForClient(idleTimeoutMs)
+            .setTimeBetweenEvictionRuns(evictionIntervalMs)
+            .build()
+            .getConfig());
+  }
+
+  private TElasticFramedTransport createTransport(TTransport underlying) 
throws Exception {
+    return compression
+        ? new TSnappyElasticFramedTransport(underlying, INITIAL_BUFFER_SIZE, 
65536, true)
+        : new TElasticFramedTransport(underlying, INITIAL_BUFFER_SIZE, 65536, 
true);
+  }
+
+  private static class TestMemoryControl implements 
AutoResizingBufferMemoryControl {
+    private final AtomicLong used = new AtomicLong();
+    private volatile long limit = Long.MAX_VALUE;
+
+    @Override
+    public boolean allocate(long bytes) {
+      while (true) {
+        long current = used.get();
+        if (current + bytes > limit) {
+          return false;
+        }
+        if (used.compareAndSet(current, current + bytes)) {
+          return true;
+        }
+      }
+    }
+
+    @Override
+    public void release(long bytes) {
+      used.addAndGet(-bytes);
+    }
+  }
+}

Reply via email to