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

commit 4df9f9961a9600a2e146d6866ec232f18625f1a9
Author: Caideyipi <[email protected]>
AuthorDate: Fri Jul 10 10:20:43 2026 +0800

    Add pipe receiver memory protection (#18090) (#18133)
    
    * Add pipe receiver memory protection
    
    * Add pipe receiver memory protection tests
    
    * Address receiver memory review comments
    
    (cherry picked from commit 71696f333af3f71e2bea946754436b90247b0d4e)
---
 .../protocol/thrift/IoTDBDataNodeReceiver.java     | 117 +++++++++++++---
 .../commons/pipe/receiver/IoTDBFileReceiver.java   |  39 +++++-
 .../thrift/common/PipeTransferSliceReqHandler.java |  37 +++++
 .../thrift/request/PipeTransferCompressedReq.java  |  21 +++
 .../pipe/receiver/IoTDBFileReceiverTest.java       |  89 ++++++++++++
 .../common/PipeTransferSliceReqBuilderTest.java    |  53 ++++++++
 .../request/PipeTransferCompressedReqTest.java     | 149 +++++++++++++++++++++
 7 files changed, 489 insertions(+), 16 deletions(-)

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 761c0183c9b..945b4a55209 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
@@ -151,6 +151,7 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
   private static final SessionManager SESSION_MANAGER = 
SessionManager.getInstance();
 
   private PipeMemoryBlock allocatedMemoryBlock;
+  private final List<PipeMemoryBlock> allocatedSliceMemoryBlocks = new 
ArrayList<>();
 
   static {
     try {
@@ -172,7 +173,7 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
       if (PipeRequestType.isValidatedRequestType(rawRequestType)) {
         final PipeRequestType requestType = 
PipeRequestType.valueOf(rawRequestType);
         if (requestType != PipeRequestType.TRANSFER_SLICE) {
-          sliceReqHandler.clear();
+          clearSliceReqHandler();
         }
         final TPipeTransferResp authResp = 
checkPipeTransferAuthenticated(requestType);
         if (Objects.nonNull(authResp)) {
@@ -350,8 +351,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);
@@ -698,22 +709,97 @@ 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(
+                "Temporarily out of memory when %s. Requested memory: %d 
bytes, used memory: %d "
+                    + "bytes, free memory: %d bytes, total non-floating 
memory: %d bytes",
+                action,
+                requestedMemorySizeInBytes,
+                
PipeDataNodeResourceManager.memory().getUsedMemorySizeInBytes(),
+                
PipeDataNodeResourceManager.memory().getFreeMemorySizeInBytes(),
+                
PipeDataNodeResourceManager.memory().getTotalNonFloatingMemorySizeInBytes()));
   }
 
   /**
@@ -945,6 +1031,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/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 2375f0d4072..3223305dc80 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
@@ -23,6 +23,7 @@ import org.apache.iotdb.common.rpc.thrift.TSStatus;
 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.pipe.config.PipeConfig;
 import org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant;
 import org.apache.iotdb.commons.pipe.resource.log.PipeLogger;
@@ -425,7 +426,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
@@ -460,6 +461,22 @@ 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,
+          "Receiver id = %s: Failed to write file piece from req %s.",
+          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,
@@ -480,6 +497,26 @@ 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(
+                "Temporarily out of memory when %s. Requested memory: %d 
bytes. Root cause: %s",
+                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 09edb4bbe4a..ead9ea277d6 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
@@ -42,8 +42,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
@@ -58,6 +64,7 @@ public class PipeTransferSliceReqHandler {
         originReqType = req.getOriginReqType();
         originBodySize = req.getOriginBodySize();
         sliceCount = req.getSliceCount();
+        receivedBodySize = 0;
       } else {
         LOGGER.warn(
             "Invalid state: orderId={}, originReqType={}, originBodySize={}, 
sliceCount={}, sliceBodies.size={}",
@@ -104,10 +111,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();
@@ -131,5 +167,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 73bac4c05b4..7aa639140be 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
@@ -112,6 +112,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 b0180e5b124..a03f8b8e61a 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,12 +22,15 @@ 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.common.PipeTransferHandshakeConstant;
 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.request.PipeTransferHandshakeV2Req;
+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;
@@ -200,10 +203,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;
     private int loginCallCount = 0;
 
     DummyFileReceiver(final File baseDir) {
@@ -233,6 +281,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);
+    }
+
     void setHasPipeHandshakeCredential(final boolean 
hasPipeHandshakeCredential) {
       this.hasPipeHandshakeCredential = hasPipeHandshakeCredential;
     }
@@ -303,6 +368,14 @@ public class IoTDBFileReceiverTest {
       return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
     }
 
+    @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) {
@@ -327,6 +400,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 290ce397980..857a62ac75c 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
@@ -30,7 +30,9 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.Optional;
 
 public class PipeTransferSliceReqBuilderTest {
 
@@ -91,6 +93,57 @@ public class PipeTransferSliceReqBuilderTest {
             createReq(IoTDBSinkRequestVersion.VERSION_1.getVersion(), 4), 
bodySizeLimit));
   }
 
+  @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
new file mode 100644
index 00000000000..a00530b8c68
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/pipe/sink/payload/thrift/request/PipeTransferCompressedReqTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.commons.pipe.sink.payload.thrift.request;
+
+import org.apache.iotdb.commons.pipe.sink.compressor.PipeCompressor;
+import org.apache.iotdb.commons.pipe.sink.compressor.PipeCompressorFactory;
+import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
+
+import org.apache.tsfile.utils.BytesUtils;
+import org.apache.tsfile.utils.PublicBAOS;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class PipeTransferCompressedReqTest {
+
+  @Test
+  public void testPipeTransferCompressedReq() throws IOException {
+    final TPipeTransferReq originalReq = new TPipeTransferReq();
+    originalReq.version = IoTDBSinkRequestVersion.VERSION_1.getVersion();
+    originalReq.type = PipeRequestType.TRANSFER_TABLET_BINARY.getType();
+    originalReq.body = ByteBuffer.wrap(new byte[] {1, 2, 3, 4});
+
+    final TPipeTransferReq compressedReq =
+        PipeTransferCompressedReq.toTPipeTransferReq(
+            originalReq,
+            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);
+
+    Assert.assertEquals(IoTDBSinkRequestVersion.VERSION_1.getVersion(), 
compressedReq.version);
+    Assert.assertEquals(PipeRequestType.TRANSFER_COMPRESSED.getType(), 
compressedReq.type);
+    Assert.assertEquals(originalReq.version, decompressedReq.version);
+    Assert.assertEquals(originalReq.type, decompressedReq.type);
+    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();
+    originalReq.version = IoTDBSinkRequestVersion.VERSION_1.getVersion();
+    originalReq.type = PipeRequestType.TRANSFER_TABLET_BINARY.getType();
+    originalReq.body = ByteBuffer.wrap(new byte[] {1, 2, 3, 4});
+
+    final TPipeTransferReq compressedReq = new TPipeTransferReq();
+    compressedReq.version = IoTDBSinkRequestVersion.VERSION_1.getVersion();
+    compressedReq.type = PipeRequestType.TRANSFER_COMPRESSED.getType();
+    compressedReq.body =
+        serializeLegacyCompressedBody(
+            originalReq,
+            Collections.singletonList(
+                PipeCompressorFactory.getCompressor(
+                    PipeCompressor.PipeCompressionType.GZIP.getIndex())));
+
+    final TPipeTransferReq decompressedReq =
+        PipeTransferCompressedReq.fromTPipeTransferReq(compressedReq);
+
+    Assert.assertEquals(originalReq.version, decompressedReq.version);
+    Assert.assertEquals(originalReq.type, decompressedReq.type);
+    Assert.assertArrayEquals(originalReq.getBody(), decompressedReq.getBody());
+  }
+
+  private static ByteBuffer serializeLegacyCompressedBody(
+      final TPipeTransferReq originalReq, final List<PipeCompressor> 
compressors)
+      throws IOException {
+    try (final PublicBAOS byteArrayOutputStream = new PublicBAOS();
+        final DataOutputStream outputStream = new 
DataOutputStream(byteArrayOutputStream)) {
+      byte[] body =
+          BytesUtils.concatByteArrayList(
+              Arrays.asList(
+                  new byte[] {originalReq.version},
+                  BytesUtils.shortToBytes(originalReq.type),
+                  originalReq.getBody()));
+
+      ReadWriteIOUtils.write((byte) compressors.size(), outputStream);
+      for (final PipeCompressor compressor : compressors) {
+        ReadWriteIOUtils.write(compressor.serialize(), outputStream);
+        ReadWriteIOUtils.write(body.length, outputStream);
+        body = compressor.compress(body);
+      }
+      outputStream.write(body);
+
+      return ByteBuffer.wrap(byteArrayOutputStream.getBuf(), 0, 
byteArrayOutputStream.size());
+    }
+  }
+}


Reply via email to