This is an automated email from the ASF dual-hosted git repository.

SteNicholas pushed a commit to branch branch-0.6
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/branch-0.6 by this push:
     new 0bf9c5a58 [CELEBORN-2253][0.6] Fix IndexOutOfBoundsException reading 
shuffle data from HDFS
0bf9c5a58 is described below

commit 0bf9c5a58598501a970c9fd5d05bf11e599c6e17
Author: Cheng Pan <[email protected]>
AuthorDate: Thu May 21 19:52:38 2026 +0800

    [CELEBORN-2253][0.6] Fix IndexOutOfBoundsException reading shuffle data 
from HDFS
    
    Backport CELEBORN-2253 (https://github.com/apache/celeborn/pull/3683) to 
branch-0.6, code and tests are tuned to adapt branch-0.6 due to miss 
CELEBORN-2211 (https://github.com/apache/celeborn/pull/3548)
    
    Assisted-by: OpenCode:mimo-v2.5-pro
    
    ---
    
    Original PR description.
    
    ### What changes were proposed in this pull request?
    
    `HdfsFlushTask.writeAndRecordMetrics` calls `hdfsStream.write(bytes)`, 
which writes the full `bytes.length`. When the provider passes a reusable 
`copyBytes` buffer (whose length is `>= size`), this leaks trailing bytes from 
previous flushes into the current partition file. Pass the actual readable size 
to write only `size` bytes.
    
    ### Why are the changes needed?
    
    The S3 and OSS flush paths had the same bug and were fixed in #3600 for 
CELEBORN-2263; the HDFS path was missed. Without the fix, shuffle data flushed 
to HDFS can be corrupted when `copyBytes` is reused across flushes, and readers 
later fail with `IndexOutOfBoundsException` in 
`CelebornInputStream.fillBuffer`, for example:
    
    ```
    IndexOutOfBoundsException: readerIndex(4154253) + length(808530018)
      exceeds writerIndex(12457470)
    ```
    
    ### Does this PR resolve a correctness bug?
    
    Yes.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New unit test in `FlushTaskSuite` mirrors the S3/OSS coverage added in 
#3600. It drives `HdfsFlushTask.flush` with `copyBytes` arrays of three sizes 
(equal, larger, smaller than the buffer payload), captures the 
`FSDataOutputStream.write` arguments via Mockito's `ArgumentCaptor`, and 
asserts the offset/length pair matches the buffer content. The test fails on 
master with `ArgumentsAreDifferent` at `FlushTask.scala:128` and passes with 
the fix.
    
    Closes #3697 from pan3793/CELEBORN-2253-0.6.
    
    Authored-by: Cheng Pan <[email protected]>
    Signed-off-by: SteNicholas <[email protected]>
---
 .../service/deploy/worker/storage/FlushTask.scala  |  2 +-
 .../deploy/worker/storage/FlushTaskSuite.scala     | 69 +++++++++++++++++++++-
 2 files changed, 69 insertions(+), 2 deletions(-)

diff --git 
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTask.scala
 
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTask.scala
index 35ac91715..1bee1cd7f 100644
--- 
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTask.scala
+++ 
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTask.scala
@@ -107,7 +107,7 @@ private[worker] class HdfsFlushTask(
     val hadoopFs = StorageManager.hadoopFs.get(Type.HDFS)
     val hdfsStream = hadoopFs.append(path, 256 * 1024)
     flush(hdfsStream) {
-      hdfsStream.write(convertBufferToBytes(buffer, copyBytes, readableBytes))
+      hdfsStream.write(convertBufferToBytes(buffer, copyBytes, readableBytes), 
0, readableBytes)
       source.incCounter(WorkerSource.HDFS_FLUSH_COUNT)
       source.incCounter(WorkerSource.HDFS_FLUSH_SIZE, readableBytes)
     }
diff --git 
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTaskSuite.scala
 
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTaskSuite.scala
index da7e2b456..d91280569 100644
--- 
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTaskSuite.scala
+++ 
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/FlushTaskSuite.scala
@@ -18,17 +18,20 @@
 package org.apache.celeborn.service.deploy.worker.storage
 
 import java.io.ByteArrayInputStream
+import java.util.{HashMap => JHashMap}
 
 import io.netty.buffer.{ByteBufAllocator, CompositeByteBuf, 
UnpooledByteBufAllocator}
 import org.apache.commons.io.IOUtils
+import org.apache.hadoop.fs.{FileSystem, FSDataOutputStream, Path}
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchersSugar.eqTo
+import org.mockito.ArgumentMatchersSugar.{any, eqTo}
 import org.mockito.MockitoSugar.{verify, _}
 import org.scalatest.prop.TableDrivenPropertyChecks.forAll
 import org.scalatest.prop.Tables.Table
 
 import org.apache.celeborn.CelebornFunSuite
 import org.apache.celeborn.common.metrics.source.AbstractSource
+import org.apache.celeborn.common.protocol.StorageInfo.Type
 import org.apache.celeborn.server.common.service.mpu.MultipartUploadHandler
 import org.apache.celeborn.service.deploy.worker.WorkerSource
 
@@ -84,6 +87,70 @@ class FlushTaskSuite extends CelebornFunSuite {
       })
   }
 
+  test("HdfsFlushTask flush should work with buffers of various sizes") {
+    val bytes = "another test data".getBytes("UTF-8")
+    val len = bytes.length
+
+    val scenarios = Table(
+      ("description", "allocatedSize"),
+      ("provider buffer is the same size as the buffer", len),
+      ("provider buffer is bigger", len + 10),
+      ("provider buffer smaller", len - 5))
+
+    forAll(scenarios) { (description, bufferSize) =>
+      val mockBuffer = spy(ALLOCATOR.compositeBuffer())
+      mockBuffer.writeBytes(bytes)
+      val mockNotifier = mock[FlushNotifier]
+      val mockSource = mock[AbstractSource]
+      val mockHdfsStream = mock[FSDataOutputStream]
+      val mockFs = mock[FileSystem]
+      val mockPath = mock[Path]
+
+      val fsMap = new JHashMap[Type, FileSystem]()
+      fsMap.put(Type.HDFS, mockFs)
+      when(mockFs.append(any[Path], any[Int])).thenReturn(mockHdfsStream)
+
+      val origMap = StorageManager.hadoopFs
+      try {
+        StorageManager.hadoopFs = fsMap
+
+        val flushTask = new HdfsFlushTask(
+          mockBuffer,
+          mockPath,
+          mockNotifier,
+          false,
+          mockSource)
+
+        val copyBytesArray = Array.fill[Byte](bufferSize)(0xFF.toByte)
+        flushTask.flush(copyBytesArray)
+
+        assert(mockBuffer.readableBytes() == bytes.length)
+
+        val bytesCaptor = ArgumentCaptor.forClass(classOf[Array[Byte]])
+        val offsetCaptor = ArgumentCaptor.forClass(classOf[Integer])
+        val lengthCaptor = ArgumentCaptor.forClass(classOf[Integer])
+        verify(mockHdfsStream).write(
+          bytesCaptor.capture(),
+          offsetCaptor.capture(),
+          lengthCaptor.capture())
+        verify(mockSource).incCounter(WorkerSource.HDFS_FLUSH_COUNT)
+        verify(mockSource).incCounter(WorkerSource.HDFS_FLUSH_SIZE, 
bytes.length)
+
+        assert(offsetCaptor.getValue == 0, s"Offset mismatch on: $description")
+        assert(
+          lengthCaptor.getValue == bytes.length,
+          s"Length mismatch on: $description")
+        val capturedBytes = bytesCaptor.getValue
+          .slice(offsetCaptor.getValue, offsetCaptor.getValue + 
lengthCaptor.getValue)
+        assert(capturedBytes sameElements bytes, s"Content mismatch on: 
$description")
+
+        mockBuffer.release()
+      } finally {
+        StorageManager.hadoopFs = origMap
+      }
+    }
+  }
+
   def runTest(
       builder: (
           CompositeByteBuf,

Reply via email to