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 71696f333af Add pipe receiver memory protection (#18090)
71696f333af is described below

commit 71696f333af3f71e2bea946754436b90247b0d4e
Author: Caideyipi <[email protected]>
AuthorDate: Tue Jul 7 10:31:11 2026 +0800

    Add pipe receiver memory protection (#18090)
    
    * Add pipe receiver memory protection
    
    * Add pipe receiver memory protection tests
    
    * Address receiver memory review comments
---
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   3 +
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   3 +
 .../protocol/thrift/IoTDBDataNodeReceiver.java     | 116 ++++++++++++++++++---
 .../apache/iotdb/commons/i18n/PipeMessages.java    |   2 +
 .../apache/iotdb/commons/i18n/PipeMessages.java    |   2 +
 .../commons/pipe/receiver/IoTDBFileReceiver.java   |  37 ++++++-
 .../thrift/common/PipeTransferSliceReqHandler.java |  37 +++++++
 .../thrift/request/PipeTransferCompressedReq.java  |  21 ++++
 .../pipe/receiver/IoTDBFileReceiverTest.java       |  89 ++++++++++++++++
 .../common/PipeTransferSliceReqBuilderTest.java    |  52 +++++++++
 .../request/PipeTransferCompressedReqTest.java     |  37 +++++++
 11 files changed, 383 insertions(+), 16 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index dad8dcbc7f4..2f3fdd5ff64 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -1243,6 +1243,9 @@ public final class DataNodePipeMessages {
   public static final String 
RECEIVER_ID_UNSUPPORTED_STATEMENT_TYPE_FOR_REDIRECTION =
       "Receiver id = {}: Unsupported statement type {} for redirection.";
   public static final String RECEIVER_IS_READY = "Receiver-{} is ready";
+  public static final String RECEIVER_TEMPORARILY_OUT_OF_MEMORY_FORMAT =
+      "Temporarily out of memory when %s. Requested memory: %d bytes, used 
memory: %d bytes, "
+          + "free memory: %d bytes, total non-floating memory: %d bytes";
   public static final String REGISTER_WITH_INTERVAL_IN_SECONDS_SUCCESSFULLY =
       "Register {} with interval in seconds {} successfully.";
   public static final String SOCKET_CLOSED_WHEN_EXECUTING_READTILLFULL =
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index b9b042aff52..b2b67f4d4b5 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -1172,6 +1172,9 @@ public final class DataNodePipeMessages {
   public static final String 
RECEIVER_ID_UNSUPPORTED_STATEMENT_TYPE_FOR_REDIRECTION =
       "Receiver id = {}:不支持的 statement type {} 用于 redirection。";
   public static final String RECEIVER_IS_READY = "Receiver-{} 已就绪";
+  public static final String RECEIVER_TEMPORARILY_OUT_OF_MEMORY_FORMAT =
+      "执行 %s 时暂时内存不足。请求内存:%d bytes,已用内存:%d bytes,可用内存:%d bytes,"
+          + "总非浮动内存:%d bytes";
   public static final String REGISTER_WITH_INTERVAL_IN_SECONDS_SUCCESSFULLY =
       "成功注册 {},间隔时间(秒){}。";
   public static final String SOCKET_CLOSED_WHEN_EXECUTING_READTILLFULL =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
index 8944466847d..f4a4baed694 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
@@ -192,6 +192,7 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
   private static final PipeConfig PIPE_CONFIG = PipeConfig.getInstance();
 
   private PipeMemoryBlock allocatedMemoryBlock;
+  private final List<PipeMemoryBlock> allocatedSliceMemoryBlocks = new 
ArrayList<>();
   private final Set<String> autoCreatedTreeDatabases = 
ConcurrentHashMap.newKeySet();
   private final Set<String> conflictedTreeDatabases = 
ConcurrentHashMap.newKeySet();
 
@@ -219,7 +220,7 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
       if (PipeRequestType.isValidatedRequestType(rawRequestType)) {
         final PipeRequestType requestType = 
PipeRequestType.valueOf(rawRequestType);
         if (requestType != PipeRequestType.TRANSFER_SLICE) {
-          sliceReqHandler.clear();
+          clearSliceReqHandler();
         }
         switch (requestType) {
           case HANDSHAKE_DATANODE_V1:
@@ -442,8 +443,18 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
             }
           case TRANSFER_COMPRESSED:
             {
+              long requestedMemorySizeInBytes = 0;
               try {
-                return 
receive(PipeTransferCompressedReq.fromTPipeTransferReq(req));
+                requestedMemorySizeInBytes =
+                    
PipeTransferCompressedReq.getMaxAdditionalDecompressedLengthInBytes(req);
+                try (final PipeMemoryBlock ignored =
+                    tryAllocateReceiverMemory(requestedMemorySizeInBytes)) {
+                  return 
receive(PipeTransferCompressedReq.fromTPipeTransferReq(req));
+                }
+              } catch (final PipeRuntimeOutOfMemoryCriticalException e) {
+                return new TPipeTransferResp(
+                    getReceiverTemporaryUnavailableStatus(
+                        "decompressing pipe transfer request", 
requestedMemorySizeInBytes, e));
               } finally {
                 PipeDataNodeReceiverMetrics.getInstance()
                     .recordTransferCompressedTimer(System.nanoTime() - 
startTime);
@@ -823,22 +834,96 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
   }
 
   private TPipeTransferResp handleTransferSlice(final PipeTransferSliceReq 
pipeTransferSliceReq) {
-    final boolean isInorder = 
sliceReqHandler.receiveSlice(pipeTransferSliceReq);
-    if (!isInorder) {
+    final long sliceBodySizeInBytes = 
getSliceBodySizeInBytes(pipeTransferSliceReq);
+    long requestedMemorySizeInBytes = sliceBodySizeInBytes;
+    String memoryAction = "buffering sliced pipe transfer request";
+    PipeMemoryBlock sliceMemoryBlock = null;
+    try {
+      sliceMemoryBlock = tryAllocateReceiverMemory(sliceBodySizeInBytes);
+
+      final boolean isInorder = 
sliceReqHandler.receiveSlice(pipeTransferSliceReq);
+      if (!isInorder) {
+        closeMemoryBlock(sliceMemoryBlock);
+        clearSliceReqHandler();
+        return new TPipeTransferResp(
+            RpcUtils.getStatus(
+                TSStatusCode.PIPE_TRANSFER_SLICE_OUT_OF_ORDER,
+                "Slice request is out of order, please check the request 
sequence."));
+      }
+
+      allocatedSliceMemoryBlocks.add(sliceMemoryBlock);
+      sliceMemoryBlock = null;
+
+      if (pipeTransferSliceReq.getSliceIndex() + 1 < 
pipeTransferSliceReq.getSliceCount()) {
+        return new TPipeTransferResp(
+            RpcUtils.getStatus(
+                TSStatusCode.SUCCESS_STATUS,
+                "Slice received, waiting for more slices to complete the 
request."));
+      }
+
+      memoryAction = "assembling sliced pipe transfer request";
+      requestedMemorySizeInBytes = pipeTransferSliceReq.getOriginBodySize();
+      try (final PipeMemoryBlock ignored = 
tryAllocateReceiverMemory(requestedMemorySizeInBytes)) {
+        final Optional<TPipeTransferReq> req = 
sliceReqHandler.makeReqIfComplete();
+        if (!req.isPresent()) {
+          return new TPipeTransferResp(
+              RpcUtils.getStatus(
+                  TSStatusCode.SUCCESS_STATUS,
+                  "Slice received, waiting for more slices to complete the 
request."));
+        }
+        clearSliceReqHandler();
+        return receive(req.get());
+      }
+    } catch (final PipeRuntimeOutOfMemoryCriticalException e) {
+      closeMemoryBlock(sliceMemoryBlock);
+      clearSliceReqHandler();
       return new TPipeTransferResp(
-          RpcUtils.getStatus(
-              TSStatusCode.PIPE_TRANSFER_SLICE_OUT_OF_ORDER,
-              "Slice request is out of order, please check the request 
sequence."));
+          getReceiverTemporaryUnavailableStatus(memoryAction, 
requestedMemorySizeInBytes, e));
+    } catch (final RuntimeException e) {
+      closeMemoryBlock(sliceMemoryBlock);
+      clearSliceReqHandler();
+      throw e;
     }
-    final Optional<TPipeTransferReq> req = sliceReqHandler.makeReqIfComplete();
-    if (!req.isPresent()) {
-      return new TPipeTransferResp(
-          RpcUtils.getStatus(
-              TSStatusCode.SUCCESS_STATUS,
-              "Slice received, waiting for more slices to complete the 
request."));
+  }
+
+  private long getSliceBodySizeInBytes(final PipeTransferSliceReq 
pipeTransferSliceReq) {
+    return pipeTransferSliceReq.getSliceBody() == null
+        ? 0
+        : pipeTransferSliceReq.getSliceBody().length;
+  }
+
+  private void clearSliceReqHandler() {
+    sliceReqHandler.clear();
+    allocatedSliceMemoryBlocks.forEach(this::closeMemoryBlock);
+    allocatedSliceMemoryBlocks.clear();
+  }
+
+  private void closeMemoryBlock(final PipeMemoryBlock memoryBlock) {
+    if (Objects.nonNull(memoryBlock)) {
+      memoryBlock.close();
     }
-    // sliceReqHandler will be cleared in the receive(req) method
-    return receive(req.get());
+  }
+
+  private PipeMemoryBlock tryAllocateReceiverMemory(final long 
requestedMemorySizeInBytes)
+      throws PipeRuntimeOutOfMemoryCriticalException {
+    return PipeDataNodeResourceManager.memory()
+        .forceAllocate(Math.max(requestedMemorySizeInBytes, 0));
+  }
+
+  @Override
+  protected TSStatus getReceiverTemporaryUnavailableStatus(
+      final String action,
+      final long requestedMemorySizeInBytes,
+      final PipeRuntimeOutOfMemoryCriticalException e) {
+    return new 
TSStatus(TSStatusCode.PIPE_RECEIVER_TEMPORARY_UNAVAILABLE_EXCEPTION.getStatusCode())
+        .setMessage(
+            String.format(
+                DataNodePipeMessages.RECEIVER_TEMPORARILY_OUT_OF_MEMORY_FORMAT,
+                action,
+                requestedMemorySizeInBytes,
+                
PipeDataNodeResourceManager.memory().getUsedMemorySizeInBytes(),
+                
PipeDataNodeResourceManager.memory().getFreeMemorySizeInBytes(),
+                
PipeDataNodeResourceManager.memory().getTotalNonFloatingMemorySizeInBytes()));
   }
 
   /**
@@ -1395,6 +1480,7 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
 
   @Override
   public synchronized void handleExit() {
+    clearSliceReqHandler();
     if (Objects.nonNull(configReceiverId.get())) {
       try {
         
ClusterConfigTaskExecutor.getInstance().handlePipeConfigClientExit(configReceiverId.get());
diff --git 
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
 
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
index 08e12488b03..afb977f85a2 100644
--- 
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
+++ 
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
@@ -595,6 +595,8 @@ public final class PipeMessages {
       "Receiver id = %s: Handshake failed, response status = %s.";
   public static final String RECEIVER_HANDSHAKE_FAILED_LOGIN =
       "Receiver id = %s: Handshake failed because login failed, response 
status = %s.";
+  public static final String RECEIVER_TEMPORARILY_OUT_OF_MEMORY_FORMAT =
+      "Temporarily out of memory when %s. Requested memory: %d bytes. Root 
cause: %s";
   public static final String RECEIVER_USER_LOGIN_SUCCESS =
       "Receiver id = {}: User {} login successfully.";
   public static final String RECEIVER_EXITED =
diff --git 
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
 
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
index 0c855952e99..87f1601a754 100644
--- 
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
+++ 
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
@@ -577,6 +577,8 @@ public final class PipeMessages {
       "接收器 id = %s:握手失败,响应状态 = %s。";
   public static final String RECEIVER_HANDSHAKE_FAILED_LOGIN =
       "接收器 id = %s:因登录失败导致握手失败,响应状态 = %s。";
+  public static final String RECEIVER_TEMPORARILY_OUT_OF_MEMORY_FORMAT =
+      "执行 %s 时暂时内存不足。请求内存:%d bytes。根因:%s";
   public static final String RECEIVER_USER_LOGIN_SUCCESS =
       "接收器 id = {}:用户 {} 登录成功。";
   public static final String RECEIVER_EXITED =
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
index 8304161c9d0..f05e5069eed 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiver.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.commons.audit.IAuditEntity;
 import org.apache.iotdb.commons.audit.UserEntity;
 import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.exception.IllegalPathException;
+import 
org.apache.iotdb.commons.exception.pipe.PipeRuntimeOutOfMemoryCriticalException;
 import org.apache.iotdb.commons.i18n.PipeMessages;
 import org.apache.iotdb.commons.pipe.config.PipeConfig;
 import org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant;
@@ -386,7 +387,7 @@ public abstract class IoTDBFileReceiver implements 
IoTDBReceiver {
       final PipeTransferFilePieceReq req,
       final boolean isRequestThroughAirGap,
       final boolean isSingleFile) {
-    try {
+    try (final AutoCloseable ignored = tryAllocateMemoryForFilePiece(req)) {
       updateWritingFileIfNeeded(req.getFileName(), isSingleFile);
 
       // If the request is through air gap, the sender will resend the file 
piece from the beginning
@@ -419,6 +420,18 @@ public abstract class IoTDBFileReceiver implements 
IoTDBReceiver {
       writingFileWriter.write(req.getFilePiece());
       return PipeTransferFilePieceResp.toTPipeTransferResp(
           RpcUtils.SUCCESS_STATUS, writingFileWriter.length());
+    } catch (final PipeRuntimeOutOfMemoryCriticalException e) {
+      final TSStatus status =
+          getReceiverTemporaryUnavailableStatus(
+              "receiving pipe file piece", getFilePieceSizeInBytes(req), e);
+      PipeLogger.log(
+          LOGGER::warn, e, PipeMessages.RECEIVER_FAILED_WRITE_FILE_PIECE, 
receiverId.get(), req);
+      try {
+        return PipeTransferFilePieceResp.toTPipeTransferResp(
+            status, PipeTransferFilePieceResp.ERROR_END_OFFSET);
+      } catch (Exception ex) {
+        return PipeTransferFilePieceResp.toTPipeTransferResp(status);
+      }
     } catch (final Exception e) {
       PipeLogger.log(
           LOGGER::warn, e, PipeMessages.RECEIVER_FAILED_WRITE_FILE_PIECE, 
receiverId.get(), req);
@@ -435,6 +448,28 @@ public abstract class IoTDBFileReceiver implements 
IoTDBReceiver {
     }
   }
 
+  protected AutoCloseable tryAllocateMemoryForFilePiece(final 
PipeTransferFilePieceReq req)
+      throws PipeRuntimeOutOfMemoryCriticalException {
+    return () -> {};
+  }
+
+  protected TSStatus getReceiverTemporaryUnavailableStatus(
+      final String action,
+      final long requestedMemorySizeInBytes,
+      final PipeRuntimeOutOfMemoryCriticalException e) {
+    return new 
TSStatus(TSStatusCode.PIPE_RECEIVER_TEMPORARY_UNAVAILABLE_EXCEPTION.getStatusCode())
+        .setMessage(
+            String.format(
+                PipeMessages.RECEIVER_TEMPORARILY_OUT_OF_MEMORY_FORMAT,
+                action,
+                requestedMemorySizeInBytes,
+                e.getMessage()));
+  }
+
+  private static long getFilePieceSizeInBytes(final PipeTransferFilePieceReq 
req) {
+    return req.getFilePiece() == null ? 0 : req.getFilePiece().length;
+  }
+
   protected final void updateWritingFileIfNeeded(final String fileName, final 
boolean isSingleFile)
       throws IOException {
     if (isFileExistedAndNameCorrect(fileName)) {
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqHandler.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqHandler.java
index c95d4ec4306..1e219dbfd27 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqHandler.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqHandler.java
@@ -43,8 +43,14 @@ public class PipeTransferSliceReqHandler {
 
   private int sliceCount = -1;
   private final List<byte[]> sliceBodies = new ArrayList<>();
+  private long receivedBodySize = 0;
 
   public boolean receiveSlice(final PipeTransferSliceReq req) {
+    if (!isValidSliceReq(req)) {
+      clear();
+      return false;
+    }
+
     if (orderId == -1
         || originReqType == -1
         || originBodySize == -1
@@ -59,6 +65,7 @@ public class PipeTransferSliceReqHandler {
         originReqType = req.getOriginReqType();
         originBodySize = req.getOriginBodySize();
         sliceCount = req.getSliceCount();
+        receivedBodySize = 0;
       } else {
         LOGGER.warn(
             PipeMessages.INVALID_STATE_SLICE,
@@ -107,10 +114,39 @@ public class PipeTransferSliceReqHandler {
       return false;
     }
 
+    if (receivedBodySize + req.getSliceBody().length > originBodySize) {
+      LOGGER.warn(
+          "Received slice body size {} exceeds origin body size {}",
+          receivedBodySize + req.getSliceBody().length,
+          originBodySize);
+      clear();
+      return false;
+    }
+
     sliceBodies.add(req.getSliceBody());
+    receivedBodySize += req.getSliceBody().length;
+
+    if (sliceBodies.size() == sliceCount && receivedBodySize != 
originBodySize) {
+      LOGGER.warn(
+          "Received slice body size {} is not equal to origin body size {}",
+          receivedBodySize,
+          originBodySize);
+      clear();
+      return false;
+    }
+
     return true;
   }
 
+  private boolean isValidSliceReq(final PipeTransferSliceReq req) {
+    return req.getOriginBodySize() >= 0
+        && req.getSliceCount() > 0
+        && req.getSliceIndex() >= 0
+        && req.getSliceIndex() < req.getSliceCount()
+        && req.getSliceBody() != null
+        && req.getSliceBody().length <= req.getOriginBodySize();
+  }
+
   public Optional<TPipeTransferReq> makeReqIfComplete() {
     if (sliceBodies.size() != sliceCount) {
       return Optional.empty();
@@ -134,5 +170,6 @@ public class PipeTransferSliceReqHandler {
     originBodySize = -1;
     sliceCount = -1;
     sliceBodies.clear();
+    receivedBodySize = 0;
   }
 }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReq.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReq.java
index 8f6f36edf1b..5dc1f1f6661 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReq.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReq.java
@@ -113,6 +113,27 @@ public class PipeTransferCompressedReq extends 
TPipeTransferReq {
     return decompressedReq;
   }
 
+  /** Get the largest intermediate decompressed body size without consuming 
the request body. */
+  public static int getMaxDecompressedLengthInBytes(final TPipeTransferReq 
transferReq) {
+    final ByteBuffer compressedBuffer = transferReq.body.duplicate();
+
+    int maxDecompressedLength = compressedBuffer.remaining();
+    final int compressorsSize = ReadWriteIOUtils.readByte(compressedBuffer);
+    for (int i = 0; i < compressorsSize; ++i) {
+      ReadWriteIOUtils.readByte(compressedBuffer);
+      final int decompressedLength = 
ReadWriteIOUtils.readInt(compressedBuffer);
+      checkDecompressedLength(decompressedLength);
+      maxDecompressedLength = Math.max(maxDecompressedLength, 
decompressedLength);
+    }
+    return maxDecompressedLength;
+  }
+
+  /** Get the largest additional decompressed body size beyond the current 
transfer frame. */
+  public static int getMaxAdditionalDecompressedLengthInBytes(final 
TPipeTransferReq transferReq) {
+    final int transferFrameBodySize = transferReq.body.duplicate().remaining();
+    return Math.max(0, getMaxDecompressedLengthInBytes(transferReq) - 
transferFrameBodySize);
+  }
+
   /** This method is used to prevent decompression bomb attacks. */
   private static void checkDecompressedLength(final int decompressedLength)
       throws IllegalArgumentException {
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
index 5a1ed3e653a..f774d725bae 100644
--- 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/receiver/IoTDBFileReceiverTest.java
@@ -22,10 +22,13 @@ package org.apache.iotdb.commons.pipe.receiver;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.exception.IllegalPathException;
+import 
org.apache.iotdb.commons.exception.pipe.PipeRuntimeOutOfMemoryCriticalException;
 import 
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeRequestType;
+import 
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFilePieceReq;
 import 
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFileSealReqV1;
 import 
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferFileSealReqV2;
 import 
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeTransferHandshakeV1Req;
+import 
org.apache.iotdb.commons.pipe.sink.payload.thrift.response.PipeTransferFilePieceResp;
 import org.apache.iotdb.rpc.TSStatusCode;
 import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
 import org.apache.iotdb.service.rpc.thrift.TPipeTransferResp;
@@ -138,10 +141,55 @@ public class IoTDBFileReceiverTest {
     }
   }
 
+  @Test
+  public void 
testFilePieceMemoryAllocationFailureReturnsTemporaryUnavailable() throws 
Exception {
+    final Path baseDir = Files.createTempDirectory("iotdb-file-receiver-test");
+    final DummyFileReceiver receiver = new DummyFileReceiver(baseDir.toFile());
+    try {
+      receiver.setFailFilePieceMemoryAllocation(true);
+
+      final TPipeTransferResp response =
+          receiver.writeFilePiece("normal.tsfile", 0, new byte[] {1, 2, 3});
+      final PipeTransferFilePieceResp filePieceResp =
+          PipeTransferFilePieceResp.fromTPipeTransferResp(response);
+
+      Assert.assertEquals(
+          
TSStatusCode.PIPE_RECEIVER_TEMPORARY_UNAVAILABLE_EXCEPTION.getStatusCode(),
+          response.getStatus().getCode());
+      Assert.assertTrue(response.getStatus().getMessage().contains("no memory 
for file piece"));
+      Assert.assertEquals(
+          PipeTransferFilePieceResp.ERROR_END_OFFSET, 
filePieceResp.getEndWritingOffset());
+      
Assert.assertFalse(receiver.getWritingFileInBaseDir("normal.tsfile").exists());
+    } finally {
+      receiver.handleExit();
+    }
+  }
+
+  @Test
+  public void testFilePieceMemoryAllocationIsClosedAfterWrite() throws 
Exception {
+    final Path baseDir = Files.createTempDirectory("iotdb-file-receiver-test");
+    final DummyFileReceiver receiver = new DummyFileReceiver(baseDir.toFile());
+    try {
+      final TPipeTransferResp response =
+          receiver.writeFilePiece("normal.tsfile", 0, new byte[] {1, 2, 3});
+      final PipeTransferFilePieceResp filePieceResp =
+          PipeTransferFilePieceResp.fromTPipeTransferResp(response);
+
+      Assert.assertEquals(
+          TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
response.getStatus().getCode());
+      Assert.assertEquals(3, filePieceResp.getEndWritingOffset());
+      Assert.assertEquals(1, receiver.getFilePieceMemoryCloseCount());
+    } finally {
+      receiver.handleExit();
+    }
+  }
+
   private static class DummyFileReceiver extends IoTDBFileReceiver {
 
     private final File receiverFileBaseDir;
     private TSStatus loadFileV1Status = new 
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+    private boolean failFilePieceMemoryAllocation = false;
+    private int filePieceMemoryCloseCount = 0;
 
     DummyFileReceiver(final File baseDir) {
       receiverFileBaseDir = baseDir;
@@ -166,6 +214,23 @@ public class IoTDBFileReceiverTest {
       loadFileV1Status = status;
     }
 
+    void setFailFilePieceMemoryAllocation(final boolean 
failFilePieceMemoryAllocation) {
+      this.failFilePieceMemoryAllocation = failFilePieceMemoryAllocation;
+    }
+
+    int getFilePieceMemoryCloseCount() {
+      return filePieceMemoryCloseCount;
+    }
+
+    TPipeTransferResp writeFilePiece(
+        final String fileName, final long startWritingOffset, final byte[] 
filePiece)
+        throws IOException {
+      return handleTransferFilePiece(
+          DummyFilePieceReq.toTPipeTransferReq(fileName, startWritingOffset, 
filePiece),
+          false,
+          true);
+    }
+
     TPipeTransferResp sealFileV1(final String fileName, final long fileLength) 
throws IOException {
       return 
handleTransferFileSealV1(DummyFileSealReqV1.toTPipeTransferReq(fileName, 
fileLength));
     }
@@ -228,6 +293,14 @@ public class IoTDBFileReceiverTest {
       return new TSStatus(200);
     }
 
+    @Override
+    protected AutoCloseable tryAllocateMemoryForFilePiece(final 
PipeTransferFilePieceReq req) {
+      if (failFilePieceMemoryAllocation) {
+        throw new PipeRuntimeOutOfMemoryCriticalException("no memory for file 
piece");
+      }
+      return () -> filePieceMemoryCloseCount++;
+    }
+
     @Override
     protected TSStatus loadFileV1(
         final PipeTransferFileSealReqV1 req, final String fileAbsolutePath) {
@@ -252,6 +325,22 @@ public class IoTDBFileReceiverTest {
     }
   }
 
+  private static class DummyFilePieceReq extends PipeTransferFilePieceReq {
+
+    static DummyFilePieceReq toTPipeTransferReq(
+        final String fileName, final long startWritingOffset, final byte[] 
filePiece)
+        throws IOException {
+      return (DummyFilePieceReq)
+          new DummyFilePieceReq()
+              .convertToTPipeTransferReq(fileName, startWritingOffset, 
filePiece);
+    }
+
+    @Override
+    protected PipeRequestType getPlanType() {
+      return PipeRequestType.TRANSFER_TS_FILE_PIECE;
+    }
+  }
+
   private static class DummyHandshakeReq extends PipeTransferHandshakeV1Req {
 
     static DummyHandshakeReq toTPipeTransferReq(final String 
timestampPrecision)
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqBuilderTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqBuilderTest.java
index cc836cae945..40ec6d2d62c 100644
--- 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqBuilderTest.java
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/common/PipeTransferSliceReqBuilderTest.java
@@ -37,6 +37,7 @@ import org.junit.Test;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.Optional;
 
 public class PipeTransferSliceReqBuilderTest {
 
@@ -123,6 +124,57 @@ public class PipeTransferSliceReqBuilderTest {
     Assert.assertEquals(2, sliceReq.getSliceCount());
   }
 
+  @Test
+  public void testSliceReqHandlerRejectsOversizedSlices() throws IOException {
+    final TPipeTransferReq req = 
createReq(IoTDBSinkRequestVersion.VERSION_1.getVersion(), 6);
+    final PipeTransferSliceReq firstSlice =
+        PipeTransferSliceReq.toTPipeTransferReq(7, req.getType(), 0, 2, 
req.body.duplicate(), 0, 4);
+    final PipeTransferSliceReq oversizedSecondSlice =
+        PipeTransferSliceReq.toTPipeTransferReq(7, req.getType(), 1, 2, 
req.body.duplicate(), 2, 6);
+
+    final PipeTransferSliceReqHandler handler = new 
PipeTransferSliceReqHandler();
+    Assert.assertTrue(handler.receiveSlice(firstSlice));
+    Assert.assertFalse(handler.receiveSlice(oversizedSecondSlice));
+    Assert.assertFalse(handler.makeReqIfComplete().isPresent());
+  }
+
+  @Test
+  public void testSliceReqHandlerAssemblesCompleteRequest() throws IOException 
{
+    final TPipeTransferReq req = 
createReq(IoTDBSinkRequestVersion.VERSION_1.getVersion(), 6);
+    final PipeTransferSliceReq firstSlice =
+        PipeTransferSliceReq.toTPipeTransferReq(7, req.getType(), 0, 2, 
req.body.duplicate(), 0, 4);
+    final PipeTransferSliceReq secondSlice =
+        PipeTransferSliceReq.toTPipeTransferReq(7, req.getType(), 1, 2, 
req.body.duplicate(), 4, 6);
+
+    final PipeTransferSliceReqHandler handler = new 
PipeTransferSliceReqHandler();
+    Assert.assertTrue(handler.receiveSlice(firstSlice));
+    Assert.assertFalse(handler.makeReqIfComplete().isPresent());
+    Assert.assertTrue(handler.receiveSlice(secondSlice));
+
+    final Optional<TPipeTransferReq> completedReq = 
handler.makeReqIfComplete();
+    Assert.assertTrue(completedReq.isPresent());
+
+    final TPipeTransferReq assembledReq = completedReq.get();
+    Assert.assertEquals(IoTDBSinkRequestVersion.VERSION_1.getVersion(), 
assembledReq.getVersion());
+    Assert.assertEquals(req.getType(), assembledReq.getType());
+
+    final byte[] assembledBody = new byte[assembledReq.body.remaining()];
+    assembledReq.body.get(assembledBody);
+    Assert.assertArrayEquals(new byte[] {0, 1, 2, 3, 4, 5}, assembledBody);
+  }
+
+  @Test
+  public void testSliceReqHandlerRejectsInvalidMetadata() throws IOException {
+    final TPipeTransferReq req = 
createReq(IoTDBSinkRequestVersion.VERSION_1.getVersion(), 2);
+    final PipeTransferSliceReq invalidSlice =
+        PipeTransferSliceReq.toTPipeTransferReq(7, req.getType(), 0, 0, 
req.body.duplicate(), 0, 1);
+
+    final PipeTransferSliceReqHandler handler = new 
PipeTransferSliceReqHandler();
+
+    Assert.assertFalse(handler.receiveSlice(invalidSlice));
+    Assert.assertFalse(handler.makeReqIfComplete().isPresent());
+  }
+
   private static TPipeTransferReq createReq(final byte version, final int 
bodySize) {
     final byte[] body = new byte[bodySize];
     for (int i = 0; i < body.length; ++i) {
diff --git 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReqTest.java
 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReqTest.java
index ccdbc5086a5..a00530b8c68 100644
--- 
a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReqTest.java
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReqTest.java
@@ -51,6 +51,17 @@ public class PipeTransferCompressedReqTest {
             Collections.singletonList(
                 PipeCompressorFactory.getCompressor(
                     PipeCompressor.PipeCompressionType.GZIP.getIndex())));
+    final int compressedBodySize = compressedReq.body.remaining();
+    final int maxDecompressedLength =
+        Math.max(compressedBodySize, originalReq.body.remaining() + 3);
+    Assert.assertEquals(
+        maxDecompressedLength,
+        
PipeTransferCompressedReq.getMaxDecompressedLengthInBytes(compressedReq));
+    Assert.assertEquals(
+        maxDecompressedLength - compressedBodySize,
+        
PipeTransferCompressedReq.getMaxAdditionalDecompressedLengthInBytes(compressedReq));
+    Assert.assertEquals(0, compressedReq.body.position());
+
     final TPipeTransferReq decompressedReq =
         PipeTransferCompressedReq.fromTPipeTransferReq(compressedReq);
 
@@ -61,6 +72,32 @@ public class PipeTransferCompressedReqTest {
     Assert.assertArrayEquals(originalReq.getBody(), decompressedReq.getBody());
   }
 
+  @Test
+  public void testAdditionalDecompressedLengthExcludesTransferFrameBody() 
throws IOException {
+    final TPipeTransferReq originalReq = new TPipeTransferReq();
+    originalReq.version = IoTDBSinkRequestVersion.VERSION_1.getVersion();
+    originalReq.type = PipeRequestType.TRANSFER_TABLET_BINARY.getType();
+    final byte[] highlyCompressibleBody = new byte[16 * 1024];
+    Arrays.fill(highlyCompressibleBody, (byte) 1);
+    originalReq.body = ByteBuffer.wrap(highlyCompressibleBody);
+
+    final TPipeTransferReq compressedReq =
+        PipeTransferCompressedReq.toTPipeTransferReq(
+            originalReq,
+            Collections.singletonList(
+                PipeCompressorFactory.getCompressor(
+                    PipeCompressor.PipeCompressionType.GZIP.getIndex())));
+    final int compressedBodySize = compressedReq.body.remaining();
+    final int maxDecompressedLength =
+        
PipeTransferCompressedReq.getMaxDecompressedLengthInBytes(compressedReq);
+
+    Assert.assertTrue(maxDecompressedLength > compressedBodySize);
+    Assert.assertEquals(
+        maxDecompressedLength - compressedBodySize,
+        
PipeTransferCompressedReq.getMaxAdditionalDecompressedLengthInBytes(compressedReq));
+    Assert.assertEquals(0, compressedReq.body.position());
+  }
+
   @Test
   public void testPipeTransferCompressedReqFromLegacyV13Body() throws 
IOException {
     final TPipeTransferReq originalReq = new TPipeTransferReq();


Reply via email to