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

zaynt4606 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/main by this push:
     new 71724db239 [CELEBORN-2361] Unblock stage-end when CommitFilesResponse 
carries an empty map-id bitmap
71724db239 is described below

commit 71724db23980cc80cb920fee2a057b96f26893f7
Author: strelok89 <[email protected]>
AuthorDate: Mon Oct 12 17:47:47 2026 +0800

    [CELEBORN-2361] Unblock stage-end when CommitFilesResponse carries an empty 
map-id bitmap
    
    This revives #3736 by afterincomparableyum, which was closed by the stale 
bot rather than
    rejected. Rebased onto current `main`, credit preserved via 
`Co-Authored-By`.
    
    A worker serializes an empty `RoaringBitmap` as an empty protobuf 
bytestring, and
    `Utils.byteStringToRoaringBitmap` deserializes empty bytes back to `null`. 
That `null` was
    stored in `CommitFilesResponse.committedMapIdBitMap` (a `HashMap`, which 
allows null values)
    and then merged into the `ConcurrentHashMap` 
`ShuffleCommittedInfo.committedMapIdBitmap` via
    `putAll`, which rejects null values and throws NPE in 
`CommitHandler.processResponse`.
    
    Since `ReducePartitionCommitHandler.tryFinalCommit` had no `try/finally`, 
the shuffle was
    left in `inProcessStageEndShuffleSet` and `setStageEnd` was never called, 
so every
    `GetReducerFileGroup` request parked until the executor RPC timeout, 
hanging the Spark
    client process.
    
    Fixes:
    
    - `ControlMessages`: skip null bitmaps when deserializing 
`CommitFilesResponse` so `null`
      never reaches `ConcurrentHashMap.putAll`.
    - `ReducePartitionCommitHandler`: wrap the commit in `try/finally` so the 
in-process
      stage-end marker is always cleared, allowing a retry.
    
    The original revision of this PR also made 
`CelebornInputStream.skipLocation` treat a null
    mapId bitmap as "do not skip". That has since landed on `main` via 
CELEBORN-2442
    (`adadbd924`), which moved the logic into 
`CelebornInputStream.shouldSkipLocation` with the
    same null-safe behavior, so it is dropped here.
    
    The null-means-unknown semantics are kept deliberately. The wire format 
collapses null and
    empty to `ByteString.EMPTY`, so making `byteStringToRoaringBitmap` return 
an empty bitmap
    would turn "unknown" into "no map ids here" at the `PbSerDeUtils` call 
sites and make
    `shouldSkipLocation` skip locations that must be read.
    
    Without this, Spark clients can hang for a long time and emit repeated NPEs.
    
    - [x] Yes
    
    - [ ] Yes
    
    Added a `UtilsSuite` regression test asserting a `CommitFilesResponse` with 
an empty bitmap
    survives the transport round-trip without null map values. Verified it 
fails without the
    `ControlMessages` fix and passes with it. `celeborn-client/Test/compile` and
    `spotless:check -pl common,client` pass locally; the rest is left to CI.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Closes #3833 from strelok89/CELEBORN-2361.
    
    Authored-by: strelok89 <[email protected]>
    Signed-off-by: zhengtao <[email protected]>
    
    Co-Authored-By: afterincomparableyum 
<[email protected]>
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Co-Authored-By: Codex <[email protected]>
    AI-Model: gpt-6
    AI-Contributed/Feature: 0/44
    AI-Contributed/UT: 0/30
---
 .../commit/ReducePartitionCommitHandler.scala      | 37 ++++++++++++----------
 .../common/protocol/message/ControlMessages.scala  |  7 +++-
 .../apache/celeborn/common/util/UtilsSuite.scala   | 30 +++++++++++++++++-
 3 files changed, 56 insertions(+), 18 deletions(-)

diff --git 
a/client/src/main/scala/org/apache/celeborn/client/commit/ReducePartitionCommitHandler.scala
 
b/client/src/main/scala/org/apache/celeborn/client/commit/ReducePartitionCommitHandler.scala
index b3e7aa90ab..febee90f8d 100644
--- 
a/client/src/main/scala/org/apache/celeborn/client/commit/ReducePartitionCommitHandler.scala
+++ 
b/client/src/main/scala/org/apache/celeborn/client/commit/ReducePartitionCommitHandler.scala
@@ -210,23 +210,28 @@ class ReducePartitionCommitHandler(
       }
     }
 
-    // ask allLocations workers holding partitions to commit files
-    val allocatedWorkers = shuffleAllocatedWorkers.get(shuffleId)
-    val (dataLost, commitFailedWorkers) = handleFinalCommitFiles(shuffleId, 
allocatedWorkers)
-    recordWorkerFailure(commitFailedWorkers)
-    // reply
-    if (!dataLost) {
-      logInfo(s"Succeed to handle stageEnd for $shuffleId.")
-      // record in stageEndShuffleSet
-      setStageEnd(shuffleId)
-    } else {
-      logError(s"Failed to handle stageEnd for $shuffleId, lost file!")
-      dataLostShuffleSet.add(shuffleId)
-      // record in stageEndShuffleSet
-      setStageEnd(shuffleId)
+    try {
+      // ask allLocations workers holding partitions to commit files
+      val allocatedWorkers = shuffleAllocatedWorkers.get(shuffleId)
+      val (dataLost, commitFailedWorkers) = handleFinalCommitFiles(shuffleId, 
allocatedWorkers)
+      recordWorkerFailure(commitFailedWorkers)
+      // reply
+      if (!dataLost) {
+        logInfo(s"Succeed to handle stageEnd for $shuffleId.")
+        // record in stageEndShuffleSet
+        setStageEnd(shuffleId)
+      } else {
+        logError(s"Failed to handle stageEnd for $shuffleId, lost file!")
+        dataLostShuffleSet.add(shuffleId)
+        // record in stageEndShuffleSet
+        setStageEnd(shuffleId)
+      }
+      true
+    } finally {
+      // Clear the in-process marker even if the commit threw, so stage-end 
can be retried
+      // instead of parking every GetReducerFileGroup until the RPC timeout.
+      inProcessStageEndShuffleSet.remove(shuffleId)
     }
-    inProcessStageEndShuffleSet.remove(shuffleId)
-    true
   }
 
   private def handleFinalCommitFiles(
diff --git 
a/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
 
b/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
index b305539b65..580c986ae6 100644
--- 
a/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
+++ 
b/common/src/main/scala/org/apache/celeborn/common/protocol/message/ControlMessages.scala
@@ -1492,7 +1492,12 @@ object ControlMessages extends Logging {
         
pbCommitFilesResponse.getCommittedReplicaStorageInfosMap.asScala.foreach(entry 
=>
           committedReplicaStorageInfos.put(entry._1, 
StorageInfo.fromPb(entry._2)))
         pbCommitFilesResponse.getMapIdBitmapMap.asScala.foreach { entry =>
-          committedBitMap.put(entry._1, 
Utils.byteStringToRoaringBitmap(entry._2))
+          // An empty bitmap is serialized as ByteString.EMPTY and 
deserializes back to null.
+          // Skip it: null values break the ConcurrentHashMap.putAll in 
processResponse.
+          val bitmap = Utils.byteStringToRoaringBitmap(entry._2)
+          if (bitmap != null) {
+            committedBitMap.put(entry._1, bitmap)
+          }
         }
         CommitFilesResponse(
           StatusCode.fromValue(pbCommitFilesResponse.getStatus),
diff --git 
a/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala 
b/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
index a3e03f7c9b..7b115f044d 100644
--- a/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
+++ b/common/src/test/scala/org/apache/celeborn/common/util/UtilsSuite.scala
@@ -22,6 +22,7 @@ import java.util
 import java.util.Collections
 import java.util.concurrent.atomic.AtomicInteger
 
+import org.roaringbitmap.RoaringBitmap
 import org.scalatest.matchers.must.Matchers.contain
 import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper
 
@@ -33,7 +34,7 @@ import 
org.apache.celeborn.common.identity.{DefaultIdentityProvider, UserIdentif
 import org.apache.celeborn.common.meta.WorkerInfo
 import org.apache.celeborn.common.network.protocol.SerdeVersion
 import org.apache.celeborn.common.protocol.{PartitionLocation, 
PbRequestWorkers, PbRequestWorkersResponse, PbReviseLostShuffles, 
PbReviseLostShufflesResponse, StorageInfo, TransportModuleConstants}
-import 
org.apache.celeborn.common.protocol.message.ControlMessages.{GetReducerFileGroupResponse,
 MapperEnd, ReviseLostShuffles, ReviseLostShufflesResponse}
+import 
org.apache.celeborn.common.protocol.message.ControlMessages.{CommitFilesResponse,
 GetReducerFileGroupResponse, MapperEnd, ReviseLostShuffles, 
ReviseLostShufflesResponse}
 import org.apache.celeborn.common.protocol.message.StatusCode
 
 class UtilsSuite extends CelebornFunSuite {
@@ -311,6 +312,33 @@ class UtilsSuite extends CelebornFunSuite {
     assert(set.size == 0)
   }
 
+  test("CommitFilesResponse with empty map-id bitmap survives transport 
round-trip") {
+    // An empty RoaringBitmap serializes to ByteString.EMPTY and deserializes 
back to null,
+    // which used to NPE when merged into the ConcurrentHashMap in 
CommitHandler.processResponse.
+    val committedMapIdBitMap = new util.HashMap[String, RoaringBitmap]()
+    committedMapIdBitMap.put("0-0", new RoaringBitmap())
+    val nonEmpty = RoaringBitmap.bitmapOf(1, 2, 3)
+    committedMapIdBitMap.put("0-1", nonEmpty)
+
+    val response = CommitFilesResponse(
+      StatusCode.SUCCESS,
+      Collections.emptyList[String](),
+      Collections.emptyList[String](),
+      Collections.emptyList[String](),
+      Collections.emptyList[String](),
+      committedMapIdBitMap = committedMapIdBitMap)
+    val responseTrans = 
Utils.fromTransportMessage(Utils.toTransportMessage(response))
+      .asInstanceOf[CommitFilesResponse]
+
+    assert(!responseTrans.committedMapIdBitMap.containsValue(null))
+    assert(!responseTrans.committedMapIdBitMap.containsKey("0-0"))
+    assert(responseTrans.committedMapIdBitMap.get("0-1") == nonEmpty)
+
+    val concurrent = JavaUtils.newConcurrentHashMap[String, RoaringBitmap]()
+    concurrent.putAll(responseTrans.committedMapIdBitMap)
+    assert(concurrent.size() == 1)
+  }
+
   test("validate number of client/server netty threads") {
     val celebornConf = new CelebornConf()
     celebornConf.set("celeborn.io.maxDefaultNettyThreads", "100")

Reply via email to