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 4300930cbc2 [Pipe] Back off TsFile parser admission retries (#18262)
4300930cbc2 is described below

commit 4300930cbc23faa61f4d7fd9e44c444bebccfeaa
Author: Caideyipi <[email protected]>
AuthorDate: Wed Jul 22 15:14:08 2026 +0800

    [Pipe] Back off TsFile parser admission retries (#18262)
    
    * [Pipe] Back off TsFile parser admission retries
    
    * [Pipe] Document parser admission backoff config semantics
---
 .../common/tsfile/PipeTsFileInsertionEvent.java    |  65 ++++++---
 .../db/pipe/resource/memory/PipeMemoryManager.java |   5 +
 .../PipeTsFileInsertionEventAdmissionTest.java     | 162 +++++++++++++++++++++
 .../apache/iotdb/commons/conf/CommonConfig.java    |   2 +
 4 files changed, 217 insertions(+), 17 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
index 757411df9b9..db0d1b261ab 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
@@ -69,6 +69,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
@@ -879,10 +880,27 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
     final long startTime = System.currentTimeMillis();
     long lastRecordTime = startTime;
 
-    final long memoryCheckIntervalMs =
-        PipeConfig.getInstance().getPipeCheckMemoryEnoughIntervalMs();
-    while (!tryReserveTsFileParserMemory(memoryManager)) {
-      Thread.sleep(memoryCheckIntervalMs);
+    final long initialMemoryCheckIntervalMs =
+        Math.max(1, 
PipeConfig.getInstance().getPipeCheckMemoryEnoughIntervalMs());
+    final long maxMemoryCheckIntervalMs =
+        getMaxMemoryCheckIntervalMs(
+            initialMemoryCheckIntervalMs,
+            PipeConfig.getInstance().getPipeMemoryAllocateMaxRetries());
+    long memoryCheckIntervalMs = initialMemoryCheckIntervalMs;
+    while (true) {
+      final long elapsedTimeMs = Math.max(0, System.currentTimeMillis() - 
startTime);
+      if (elapsedTimeMs >= timeoutMs) {
+        // should contain 'TimeoutException' in exception message
+        throw new PipeRuntimeOutOfMemoryCriticalException(
+            String.format(
+                DataNodePipeMessages
+                    
.PIPE_EXCEPTION_TIMEOUTEXCEPTION_WAITED_S_SECONDS_FOR_MEMORY_TO_PARSE_TSFILE_0E4EF8FD,
+                elapsedTimeMs / 1000.0));
+      }
+
+      memoryManager.waitForTsFileParserMemory(
+          Math.min(
+              getMemoryCheckIntervalWithJitter(memoryCheckIntervalMs), 
timeoutMs - elapsedTimeMs));
 
       final long currentTime = System.currentTimeMillis();
       final double elapsedRecordTimeSeconds = (currentTime - lastRecordTime) / 
1000.0;
@@ -900,22 +918,35 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
             waitTimeSeconds);
       }
 
-      if (waitTimeSeconds * 1000 > timeoutMs) {
-        // should contain 'TimeoutException' in exception message
-        throw new PipeRuntimeOutOfMemoryCriticalException(
-            String.format(
-                DataNodePipeMessages
-                    
.PIPE_EXCEPTION_TIMEOUTEXCEPTION_WAITED_S_SECONDS_FOR_MEMORY_TO_PARSE_TSFILE_0E4EF8FD,
-                waitTimeSeconds));
+      if (tryReserveTsFileParserMemory(memoryManager)) {
+        LOGGER.info(
+            DataNodePipeMessages.WAIT_FOR_MEMORY_ENOUGH_FOR_PARSING_FOR,
+            resource != null ? resource.getTsFilePath() : "tsfile",
+            waitTimeSeconds);
+        return;
       }
+
+      memoryCheckIntervalMs =
+          getNextMemoryCheckIntervalMs(memoryCheckIntervalMs, 
maxMemoryCheckIntervalMs);
     }
+  }
+
+  static long getMaxMemoryCheckIntervalMs(final long initialIntervalMs, final 
int maxRetries) {
+    final long multiplier = Math.max(1, maxRetries);
+    return initialIntervalMs > Long.MAX_VALUE / multiplier
+        ? Long.MAX_VALUE
+        : initialIntervalMs * multiplier;
+  }
+
+  static long getNextMemoryCheckIntervalMs(final long currentIntervalMs, final 
long maxIntervalMs) {
+    return currentIntervalMs >= maxIntervalMs - currentIntervalMs
+        ? maxIntervalMs
+        : currentIntervalMs << 1;
+  }
 
-    final long currentTime = System.currentTimeMillis();
-    final double waitTimeSeconds = (currentTime - startTime) / 1000.0;
-    LOGGER.info(
-        DataNodePipeMessages.WAIT_FOR_MEMORY_ENOUGH_FOR_PARSING_FOR,
-        resource != null ? resource.getTsFilePath() : "tsfile",
-        waitTimeSeconds);
+  static long getMemoryCheckIntervalWithJitter(final long intervalMs) {
+    return Math.max(
+        1, (long) (intervalMs * (0.5 + 
ThreadLocalRandom.current().nextDouble() * 0.5)));
   }
 
   private boolean tryReserveTsFileParserMemory(final PipeMemoryManager 
memoryManager) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/PipeMemoryManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/PipeMemoryManager.java
index 6360efcdedd..98b820925c7 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/PipeMemoryManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/resource/memory/PipeMemoryManager.java
@@ -167,6 +167,11 @@ public class PipeMemoryManager {
     this.notifyAll();
   }
 
+  public synchronized void waitForTsFileParserMemory(final long timeoutInMs)
+      throws InterruptedException {
+    this.wait(Math.max(1, timeoutInMs));
+  }
+
   public boolean shouldReleaseTsFileParserOnOutOfMemory(
       final long firstOutOfMemoryTimeInMs, final int retryCount) {
     final long retryIntervalInMs = 
PIPE_CONFIG.getPipeMemoryAllocateRetryIntervalInMs();
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEventAdmissionTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEventAdmissionTest.java
new file mode 100644
index 00000000000..d3d7ae9cf8e
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEventAdmissionTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.pipe.event.common.tsfile;
+
+import org.apache.iotdb.commons.conf.CommonConfig;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.PrefixTreePattern;
+import org.apache.iotdb.db.pipe.resource.PipeDataNodeResourceManager;
+import org.apache.iotdb.db.pipe.resource.memory.PipeMemoryManager;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import 
org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResourceStatus;
+import org.apache.iotdb.pipe.api.event.dml.insertion.TabletInsertionEvent;
+
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.utils.TsFileGeneratorUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+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 PipeTsFileInsertionEventAdmissionTest {
+
+  @Test
+  public void testParserAdmissionBackoffIsBoundedAndJittered() {
+    Assert.assertEquals(100, 
PipeTsFileInsertionEvent.getMaxMemoryCheckIntervalMs(10, 10));
+    Assert.assertEquals(10, 
PipeTsFileInsertionEvent.getMaxMemoryCheckIntervalMs(10, 0));
+    Assert.assertEquals(
+        Long.MAX_VALUE, 
PipeTsFileInsertionEvent.getMaxMemoryCheckIntervalMs(Long.MAX_VALUE, 10));
+
+    Assert.assertEquals(20, 
PipeTsFileInsertionEvent.getNextMemoryCheckIntervalMs(10, 100));
+    Assert.assertEquals(100, 
PipeTsFileInsertionEvent.getNextMemoryCheckIntervalMs(80, 100));
+    Assert.assertEquals(100, 
PipeTsFileInsertionEvent.getNextMemoryCheckIntervalMs(100, 100));
+
+    for (int i = 0; i < 100; i++) {
+      final long intervalWithJitter =
+          PipeTsFileInsertionEvent.getMemoryCheckIntervalWithJitter(100);
+      Assert.assertTrue(intervalWithJitter >= 50);
+      Assert.assertTrue(intervalWithJitter <= 100);
+    }
+  }
+
+  @Test(timeout = 10000)
+  public void testParserAdmissionIsWokenWhenMemoryIsReleased() throws 
Exception {
+    final CommonConfig commonConfig = 
CommonDescriptor.getInstance().getConfig();
+    final PipeMemoryManager memoryManager = 
PipeDataNodeResourceManager.memory();
+    final long originalParserMemoryInBytes = 
commonConfig.getPipeTsFileParserMemory();
+    final long originalMemoryCheckIntervalMs = 
commonConfig.getPipeCheckMemoryEnoughIntervalMs();
+    final int originalMemoryAllocateMaxRetries = 
commonConfig.getPipeMemoryAllocateMaxRetries();
+
+    File tsFile = null;
+    PipeTsFileInsertionEvent event = null;
+    ExecutorService executor = null;
+    Future<Iterable<TabletInsertionEvent>> parsingFuture = null;
+    int blockerReservationCount = 0;
+    try {
+      commonConfig.setPipeTsFileParserMemory(
+          Math.max(1, memoryManager.getTotalNonFloatingMemorySizeInBytes() / 
8));
+      commonConfig.setPipeCheckMemoryEnoughIntervalMs(10000);
+      commonConfig.setPipeMemoryAllocateMaxRetries(10);
+
+      boolean parserMemoryExhausted = false;
+      for (int i = 0; i < 100; i++) {
+        if (!memoryManager.tryReserveTsFileParserMemory()) {
+          parserMemoryExhausted = true;
+          break;
+        }
+        blockerReservationCount++;
+      }
+      Assert.assertTrue(blockerReservationCount > 0);
+      Assert.assertTrue(parserMemoryExhausted);
+
+      tsFile =
+          TsFileGeneratorUtils.generateNonAlignedTsFile(
+              "parser-admission-backoff.tsfile", 1, 1, 10, 0, 100, 10, 10);
+      final TsFileResource resource = new TsFileResource(tsFile);
+      resource.setStatusForTest(TsFileResourceStatus.NORMAL);
+      final IDeviceID deviceID = 
IDeviceID.Factory.DEFAULT_FACTORY.create("root.testsg.d0");
+      resource.updateStartTime(deviceID, 0);
+      resource.updateEndTime(deviceID, 9);
+      event =
+          new PipeTsFileInsertionEvent(
+              false,
+              "root",
+              resource,
+              null,
+              false,
+              false,
+              false,
+              null,
+              null,
+              0,
+              null,
+              new PrefixTreePattern("root"),
+              null,
+              null,
+              null,
+              null,
+              true,
+              Long.MIN_VALUE,
+              Long.MAX_VALUE);
+
+      executor = Executors.newSingleThreadExecutor();
+      final PipeTsFileInsertionEvent eventToParse = event;
+      parsingFuture = executor.submit(() -> 
eventToParse.toTabletInsertionEvents(30000));
+
+      final Future<Iterable<TabletInsertionEvent>> blockedParsingFuture = 
parsingFuture;
+      Assert.assertThrows(
+          TimeoutException.class, () -> blockedParsingFuture.get(200, 
TimeUnit.MILLISECONDS));
+
+      final long releaseTimeInNanos = System.nanoTime();
+      memoryManager.releaseTsFileParserMemory();
+      blockerReservationCount--;
+
+      Assert.assertNotNull(parsingFuture.get(3, TimeUnit.SECONDS));
+      Assert.assertTrue(
+          TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 
releaseTimeInNanos) < 3000);
+    } finally {
+      if (parsingFuture != null) {
+        parsingFuture.cancel(true);
+      }
+      if (executor != null) {
+        executor.shutdownNow();
+        executor.awaitTermination(3, TimeUnit.SECONDS);
+      }
+      if (event != null) {
+        event.close();
+      }
+      while (blockerReservationCount > 0) {
+        memoryManager.releaseTsFileParserMemory();
+        blockerReservationCount--;
+      }
+      commonConfig.setPipeTsFileParserMemory(originalParserMemoryInBytes);
+      
commonConfig.setPipeCheckMemoryEnoughIntervalMs(originalMemoryCheckIntervalMs);
+      
commonConfig.setPipeMemoryAllocateMaxRetries(originalMemoryAllocateMaxRetries);
+      if (tsFile != null) {
+        tsFile.delete();
+      }
+    }
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
index 91393bf9fd2..14675b8b19e 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
@@ -341,6 +341,8 @@ public class CommonConfig {
 
   private volatile boolean pipeMemoryManagementEnabled = true;
   private volatile long pipeMemoryAllocateRetryIntervalMs = 50;
+  // Besides limiting allocation retries, this value also caps the TsFile 
parser memory admission
+  // backoff at pipeCheckMemoryEnoughIntervalMs * max(1, 
pipeMemoryAllocateMaxRetries).
   private volatile int pipeMemoryAllocateMaxRetries = 10;
   private volatile long pipeMemoryAllocateMinSizeInBytes = 32;
   private volatile long pipeMemoryAllocateForTsFileSequenceReaderInBytes =

Reply via email to