This is an automated email from the ASF dual-hosted git repository.
zaynt4606 pushed a commit to branch branch-0.7
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/branch-0.7 by this push:
new 6b0c0b0214 [CELEBORN-2481] Fix NPE when persisting MapFileMeta without
segment granularity
6b0c0b0214 is described below
commit 6b0c0b0214751a06dc2340736bd6f7ac6242f33e
Author: Rudy Attias <[email protected]>
AuthorDate: Mon Oct 19 12:05:21 2026 +0800
[CELEBORN-2481] Fix NPE when persisting MapFileMeta without segment
granularity
### What changes were proposed in this pull request?
`PbSerDeUtils.toPbFileInfo` now adds the MAP partition's writing-segment
map and segment indexes only when they are non-null.
`MapFileMeta` creates both lazily, on the first `addPartitionSegmentId` /
`addSegmentIdAndFirstBufferIndex` call. The worker builds MAP file metas with
`new MapFileMeta()`, so for a MAP partition written without segment granularity
both fields stay `null`, and `putAllPartitionWritingSegment(null)` threw:
```
java.lang.NullPointerException: Cannot invoke "java.util.Map.keySet()"
because "m" is null
at
com.google.protobuf.MapField$MutabilityAwareMap.putAll(MapField.java:312)
at
org.apache.celeborn.common.protocol.PbFileInfo$Builder.putAllPartitionWritingSegment(PbFileInfo.java:1674)
at
org.apache.celeborn.common.util.PbSerDeUtils$.toPbFileInfo(PbSerDeUtils.scala:164)
```
On restore, `fromPbFileInfo` restores both fields as `null` when they are
empty, matching a live meta that never recorded a segment, so
`MapFileMeta#hasPartitionSegmentIds()` gives the same answer before and after a
restart. Restored metas are already marked writer-closed, so nothing writes to
them. Partitions with segment granularity serialize and restore exactly as
before.
### Why are the changes needed?
When graceful shutdown is enabled, the worker persists committed file infos
through `toPbFileInfoMap` → `toPbFileInfo` in two places, and the NPE breaks
both:
- **The periodic saver**
(`worker-storage-manager-committed-fileinfo-saver`). It runs with
`scheduleWithFixedDelay` and does not catch the NPE, so the first failure
cancels it and nothing is saved afterwards.
- **`saveAllCommittedFileInfosToDB` at shutdown.** It logs the error and
skips any shuffle that contains such a MAP partition.
As a result, after a graceful restart the worker cannot recover MAP
partitions written without segment granularity, such as Flink shuffle
partitions. Readers fail to find them, and the producing tasks have to be
re-run.
### Does this PR resolve a correctness bug?
- [ ] Yes
Shuffle data is never incorrect. Partitions are lost on graceful restart
instead of being recovered.
### Does this PR introduce _any_ user-facing change?
- [x] Yes
With graceful shutdown enabled, a worker restart now recovers committed MAP
partitions without segment granularity. No configuration or protocol change.
### How was this patch tested?
Added two tests to `PbSerDeUtilsTest`:
- `fromAndToPbFileInfo with MapFileMeta without segment granularity` builds
the meta the way the worker does (no-arg constructor plus buffer size and
subpartition count). It round-trips it through `toPbFileInfo`/`fromPbFileInfo`
and through `toPbFileInfoMap`/`fromPbFileInfoMap`, checking the buffer size,
subpartition count, segment-granularity flag, writer-closed flag, that
`hasPartitionSegmentIds()` is false and the segment indexes are null, a segment
lookup, path and length. On `mai [...]
- `fromAndToPbFileInfo with MapFileMeta with segment granularity` is a
regression check. It confirms that `hasPartitionSegmentIds()` stays true and
that recorded segment IDs and segment indexes still round-trip.
```
build/mvn -pl common -am -Dtest=none
-DwildcardSuites=org.apache.celeborn.common.util.PbSerDeUtilsTest test
Tests: succeeded 36, failed 0
```
Closes #3874 from attiasr/fix-map-fileinfo-serde.
Authored-by: Rudy Attias <[email protected]>
Signed-off-by: zhengtao <[email protected]>
AI-Contributed/Feature: 0/19
AI-Contributed/UT: 0/60
(cherry picked from commit bc06a1830148672ef50edb3c946d9a9b349ef962)
Signed-off-by: zhengtao <[email protected]>
---
.../apache/celeborn/common/util/PbSerDeUtils.scala | 19 +++++--
.../celeborn/common/util/PbSerDeUtilsTest.scala | 60 ++++++++++++++++++++++
2 files changed, 74 insertions(+), 5 deletions(-)
diff --git
a/common/src/main/scala/org/apache/celeborn/common/util/PbSerDeUtils.scala
b/common/src/main/scala/org/apache/celeborn/common/util/PbSerDeUtils.scala
index e9c407ce80..cac7550871 100644
--- a/common/src/main/scala/org/apache/celeborn/common/util/PbSerDeUtils.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/util/PbSerDeUtils.scala
@@ -104,12 +104,16 @@ object PbSerDeUtils {
case PartitionType.REDUCE =>
new ReduceFileMeta(pbFileInfo.getChunkOffsetsList)
case PartitionType.MAP =>
+ // Protobuf cannot tell an empty field from an unset one; restore both
as null, which
+ // is what MapFileMeta#hasPartitionSegmentIds reads as "no segments
recorded".
val fileMeta = new MapFileMeta(
pbFileInfo.getBufferSize,
pbFileInfo.getNumSubpartitions,
pbFileInfo.getIsSegmentGranularityVisible,
- pbFileInfo.getPartitionWritingSegmentMap,
- fromPbSegmentIndexList(pbFileInfo.getSegmentIndexList))
+ if (pbFileInfo.getPartitionWritingSegmentCount == 0) null
+ else pbFileInfo.getPartitionWritingSegmentMap,
+ if (pbFileInfo.getSegmentIndexCount == 0) null
+ else fromPbSegmentIndexList(pbFileInfo.getSegmentIndexList))
// writer always closed as this is committed file info.
fileMeta.setIsWriterClosed(true)
fileMeta
@@ -161,9 +165,14 @@ object PbSerDeUtils {
builder.setBufferSize(mapFileMeta.getBufferSize)
builder.setNumSubpartitions(mapFileMeta.getNumSubpartitions)
builder.setIsSegmentGranularityVisible(mapFileMeta.isSegmentGranularityVisible)
-
builder.putAllPartitionWritingSegment(mapFileMeta.getSubPartitionWritingSegmentId)
- builder.addAllSegmentIndex(
-
mapFileMeta.getSubPartitionSegmentIndexes.asScala.map(toPbSegmentIndex).asJava)
+ // Both are created lazily on the first segment: null without segment
granularity.
+ if (mapFileMeta.getSubPartitionWritingSegmentId != null) {
+
builder.putAllPartitionWritingSegment(mapFileMeta.getSubPartitionWritingSegmentId)
+ }
+ if (mapFileMeta.getSubPartitionSegmentIndexes != null) {
+ builder.addAllSegmentIndex(
+
mapFileMeta.getSubPartitionSegmentIndexes.asScala.map(toPbSegmentIndex).asJava)
+ }
} else {
val reduceFileMeta = fileInfo.getFileMeta.asInstanceOf[ReduceFileMeta]
builder.setPartitionType(PartitionType.REDUCE.getValue)
diff --git
a/common/src/test/scala/org/apache/celeborn/common/util/PbSerDeUtilsTest.scala
b/common/src/test/scala/org/apache/celeborn/common/util/PbSerDeUtilsTest.scala
index 15c4df0fc7..3010f7e049 100644
---
a/common/src/test/scala/org/apache/celeborn/common/util/PbSerDeUtilsTest.scala
+++
b/common/src/test/scala/org/apache/celeborn/common/util/PbSerDeUtilsTest.scala
@@ -410,6 +410,66 @@ class PbSerDeUtilsTest extends CelebornFunSuite {
assert(restoredFileInfo6.isDFS && restoredFileInfo6.isS3)
}
+ test("fromAndToPbFileInfo with MapFileMeta without segment granularity") {
+ // A worker creates MAP file metas with the no-arg constructor and only
sets the
+ // buffer size and subpartition count on handshake.
+ val mapFileMeta = new MapFileMeta()
+ mapFileMeta.setBufferSize(32 * 1024)
+ mapFileMeta.setNumSubPartitions(8)
+ val mapFileInfo = new DiskFileInfo(
+ userIdentifier1,
+ true,
+ mapFileMeta,
+ file1.getAbsolutePath,
+ StorageInfo.Type.HDD,
+ 6000L)
+
+ val restoredMeta =
PbSerDeUtils.fromPbFileInfo(PbSerDeUtils.toPbFileInfo(mapFileInfo))
+ .getFileMeta.asInstanceOf[MapFileMeta]
+ assert(restoredMeta.getBufferSize == 32 * 1024)
+ assert(restoredMeta.getNumSubpartitions == 8)
+ assert(!restoredMeta.isSegmentGranularityVisible)
+ assert(restoredMeta.isWriterClosed)
+ assert(!restoredMeta.hasPartitionSegmentIds)
+ assert(restoredMeta.getSubPartitionSegmentIndexes == null)
+ assert(restoredMeta.getSegmentIdByFirstBufferIndex(0, 0) == null)
+
+ val files = JavaUtils.newConcurrentHashMap[String, DiskFileInfo]()
+ files.put("mapFile", mapFileInfo)
+ val restoredFiles = PbSerDeUtils.fromPbFileInfoMap(
+ PbSerDeUtils.toPbFileInfoMap(files),
+ JavaUtils.newConcurrentHashMap[String, UserIdentifier](),
+ mountPoints)
+ val restoredFile = restoredFiles.get("mapFile")
+ assert(restoredFile.getFilePath == mapFileInfo.getFilePath)
+ assert(restoredFile.getFileLength == mapFileInfo.getFileLength)
+
assert(restoredFile.getFileMeta.asInstanceOf[MapFileMeta].getNumSubpartitions
== 8)
+ }
+
+ test("fromAndToPbFileInfo with MapFileMeta with segment granularity") {
+ val mapFileMeta = new MapFileMeta()
+ mapFileMeta.setBufferSize(32 * 1024)
+ mapFileMeta.setNumSubPartitions(2)
+ mapFileMeta.setSegmentGranularityVisible(true)
+ mapFileMeta.addPartitionSegmentId(1, 3)
+ mapFileMeta.addSegmentIdAndFirstBufferIndex(1, 5, 3)
+ val mapFileInfo = new DiskFileInfo(
+ userIdentifier1,
+ true,
+ mapFileMeta,
+ file1.getAbsolutePath,
+ StorageInfo.Type.HDD,
+ 6000L)
+
+ val restoredMeta =
PbSerDeUtils.fromPbFileInfo(PbSerDeUtils.toPbFileInfo(mapFileInfo))
+ .getFileMeta.asInstanceOf[MapFileMeta]
+ assert(restoredMeta.isSegmentGranularityVisible)
+ assert(restoredMeta.hasPartitionSegmentIds)
+ assert(restoredMeta.getPartitionWritingSegmentId(1) == 3)
+ assert(restoredMeta.getSegmentIdByFirstBufferIndex(1, 5) == 3)
+ assert(restoredMeta.getSegmentIdByFirstBufferIndex(0, 5) == null)
+ }
+
test("fromAndToPbUserIdentifier") {
val pbUserIdentifier = PbSerDeUtils.toPbUserIdentifier(userIdentifier1)
val restoredUserIdentifier =
PbSerDeUtils.fromPbUserIdentifier(pbUserIdentifier)