This is an automated email from the ASF dual-hosted git repository.
SteNicholas 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 e5099f90d [CELEBORN-2313] Extend E2E checked zone to batch assembly
point
e5099f90d is described below
commit e5099f90d55e7b70292b0677c3a889f72c9b7efb
Author: James Xu <[email protected]>
AuthorDate: Tue Jun 9 03:18:57 2026 +0800
[CELEBORN-2313] Extend E2E checked zone to batch assembly point
### What changes were proposed in this pull request?
Celeborn's E2E integrity check computes CRC_M inside
`ShuffleClientImpl.pushOrMergeData()`, which runs in the async `DataPusher`
thread. This leaves the segment from batch assembly in the writer thread
through the `DataPusher` queue entirely outside the checked zone — meaning any
corruption that occurs in that window is invisible to the integrity check and
reaches reducers silently.
This change closes that gap and enables detection of a class of correctness
bugs where data corruption occurs between batch assembly and async push
dispatch, including bugs involving shared buffer pool references.
Introduce `ShuffleClient.computeBatchCRC()` and consolidate its invocation
into two choke points:
- `DataPusher.addTask()`: covers all async push paths. The CRC is recorded
on the writer thread immediately before the buffer is enqueued, so
`DataPusher.pushData()` intentionally uses the bare `client.pushData()` to
avoid double-counting the same batch into CommitMetadata.
- `ShuffleClient.pushDataWithCRC()` and `ShuffleClient.mergeDataWithCRC()`:
new concrete convenience methods that call `computeBatchCRC()` then delegate to
the abstract `pushData()`/`mergeData()`. These cover all synchronous push paths
(`pushGiantRecord`, `close()` flush). The abstract `pushData()`/`mergeData()`
are now documented as internal-use-only; all writer call sites across
`HashBasedShuffleWriter` (spark-2/3), `SortBasedShuffleWriter` (spark-2/3), and
`SortBasedPusher` use the [...]
The now-redundant CRC computation inside `pushOrMergeData()` is removed.
This consolidation eliminates 7 scattered `computeBatchCRC` call sites that
previously had to be manually paired with each push/merge call, reducing the
risk of a future call site omitting the CRC step.
### Why are the changes needed?
Enhance E2E Integrity Check, so it can cover more code path.
### Does this PR resolve a correctness bug?
- [ ] Yes
### Does this PR introduce _any_ user-facing change?
- [ ] Yes
### How was this patch tested?
Unit Test.
Closes #3716 from xumingming/extend-e2e-checked-zone-v2.
Authored-by: James Xu <[email protected]>
Signed-off-by: Nicholas Jiang <[email protected]>
---
.../spark/shuffle/celeborn/SortBasedPusher.java | 2 +-
.../shuffle/celeborn/HashBasedShuffleWriter.java | 4 +-
.../shuffle/celeborn/SortBasedShuffleWriter.java | 2 +-
.../shuffle/celeborn/HashBasedShuffleWriter.java | 4 +-
.../shuffle/celeborn/SortBasedShuffleWriter.java | 2 +-
.../celeborn/CelebornShuffleWriterSuiteBase.java | 93 ++++++++++++++++++
.../apache/celeborn/client/DummyShuffleClient.java | 59 +++++++++++-
.../org/apache/celeborn/client/ShuffleClient.java | 106 ++++++++++++++++++++-
.../apache/celeborn/client/ShuffleClientImpl.java | 26 +++--
.../apache/celeborn/client/write/DataPusher.java | 3 +
.../celeborn/client/ShuffleClientSuiteJ.java | 97 +++++++++++++++++++
11 files changed, 383 insertions(+), 15 deletions(-)
diff --git
a/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedPusher.java
b/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedPusher.java
index 013785ecc..8297aefb6 100644
---
a/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedPusher.java
+++
b/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedPusher.java
@@ -222,7 +222,7 @@ public class SortBasedPusher extends MemoryConsumer {
currentPartition = partition;
} else {
int bytesWritten =
- shuffleClient.mergeData(
+ shuffleClient.mergeDataWithCRC(
shuffleId,
mapId,
attemptNumber,
diff --git
a/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
b/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
index de7d00b4e..4d55c14be 100644
---
a/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
+++
b/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
@@ -278,7 +278,7 @@ public class HashBasedShuffleWriter<K, V, C> extends
ShuffleWriter<K, V> {
private void pushGiantRecord(int partitionId, byte[] buffer, int numBytes)
throws IOException {
logger.debug("Push giant record for partition {}, size {}.", partitionId,
numBytes);
int bytesWritten =
- shuffleClient.pushData(
+ shuffleClient.pushDataWithCRC(
shuffleId,
mapId,
encodedAttemptId,
@@ -339,7 +339,7 @@ public class HashBasedShuffleWriter<K, V, C> extends
ShuffleWriter<K, V> {
final int size = sendOffsets[i];
if (size > 0) {
int bytesWritten =
- shuffleClient.mergeData(
+ shuffleClient.mergeDataWithCRC(
shuffleId,
mapId,
encodedAttemptId,
diff --git
a/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
b/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
index 9ba908ade..2ab3139b5 100644
---
a/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
+++
b/client-spark/spark-2/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
@@ -284,7 +284,7 @@ public class SortBasedShuffleWriter<K, V, C> extends
ShuffleWriter<K, V> {
private void pushGiantRecord(int partitionId, byte[] buffer, int numBytes)
throws IOException {
logger.debug("Push giant record, size {}.", Utils.bytesToString(numBytes));
int bytesWritten =
- shuffleClient.pushData(
+ shuffleClient.pushDataWithCRC(
shuffleId,
mapId,
encodedAttemptId,
diff --git
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
index 49c6d6954..8b454ebd5 100644
---
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
+++
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/HashBasedShuffleWriter.java
@@ -279,7 +279,7 @@ public class HashBasedShuffleWriter<K, V, C> extends
ShuffleWriter<K, V> {
logger.debug("Push giant record, size {}.", numBytes);
long start = System.nanoTime();
int bytesWritten =
- shuffleClient.pushData(
+ shuffleClient.pushDataWithCRC(
shuffleId,
mapId,
encodedAttemptId,
@@ -343,7 +343,7 @@ public class HashBasedShuffleWriter<K, V, C> extends
ShuffleWriter<K, V> {
protected void mergeData(int partitionId, byte[] buffer, int offset, int
length)
throws IOException {
int bytesWritten =
- shuffleClient.mergeData(
+ shuffleClient.mergeDataWithCRC(
shuffleId,
mapId,
encodedAttemptId,
diff --git
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
index e413ce42f..ebc1d44ea 100644
---
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
+++
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SortBasedShuffleWriter.java
@@ -349,7 +349,7 @@ public class SortBasedShuffleWriter<K, V, C> extends
ShuffleWriter<K, V> {
logger.debug("Push giant record, size {}.",
Utils.bytesToString(numBytes));
long start = System.nanoTime();
int bytesWritten =
- shuffleClient.pushData(
+ shuffleClient.pushDataWithCRC(
shuffleId,
mapId,
encodedAttemptId,
diff --git
a/client-spark/spark-3/src/test/java/org/apache/spark/shuffle/celeborn/CelebornShuffleWriterSuiteBase.java
b/client-spark/spark-3/src/test/java/org/apache/spark/shuffle/celeborn/CelebornShuffleWriterSuiteBase.java
index 62d31ee6d..8c74e0946 100644
---
a/client-spark/spark-3/src/test/java/org/apache/spark/shuffle/celeborn/CelebornShuffleWriterSuiteBase.java
+++
b/client-spark/spark-3/src/test/java/org/apache/spark/shuffle/celeborn/CelebornShuffleWriterSuiteBase.java
@@ -26,6 +26,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
@@ -78,9 +80,11 @@ import org.slf4j.LoggerFactory;
import org.apache.celeborn.client.DummyShuffleClient;
import org.apache.celeborn.client.ShuffleClient;
import org.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.CommitMetadata;
import org.apache.celeborn.common.identity.UserIdentifier;
import org.apache.celeborn.common.util.JavaUtils;
import org.apache.celeborn.common.util.Utils;
+import org.apache.celeborn.common.write.PushState;
import org.apache.celeborn.reflect.DynConstructors;
public abstract class CelebornShuffleWriterSuiteBase {
@@ -213,6 +217,95 @@ public abstract class CelebornShuffleWriterSuiteBase {
check(2 << 30, conf, serializer);
}
+ @Test
+ public void testIntegrityCheckAccumulation() throws Exception {
+ final KryoSerializer serializer = new KryoSerializer(sparkConf);
+ final CelebornConf conf =
+ new CelebornConf()
+ .set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE().key(), "128")
+ .set("celeborn.client.shuffle.integrityCheck.enabled", "true");
+ checkWithIntegrity(10000, conf, serializer);
+ }
+
+ @Test
+ public void testIntegrityCheckAccumulationWithFastWrite() throws Exception {
+ final UnsafeRowSerializer serializer = new UnsafeRowSerializer(2, null);
+ final CelebornConf conf =
+ new CelebornConf()
+ .set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE().key(), "128")
+ .set("celeborn.client.shuffle.integrityCheck.enabled", "true");
+ checkWithIntegrity(10000, conf, serializer);
+ }
+
+ private void checkWithIntegrity(
+ final int approximateSize, final CelebornConf conf, final Serializer
serializer)
+ throws Exception {
+ final boolean useUnsafe = serializer instanceof UnsafeRowSerializer;
+
+ String partitionIdPassthroughClazz;
+ if (SparkVersionUtil.isGreaterThan(3, 3)) {
+ partitionIdPassthroughClazz = "org.apache.spark.PartitionIdPassthrough";
+ } else {
+ partitionIdPassthroughClazz =
"org.apache.spark.sql.execution.PartitionIdPassthrough";
+ }
+ DynConstructors.Ctor<Partitioner> partitionIdPassthroughCtor =
+ DynConstructors.builder().impl(partitionIdPassthroughClazz,
int.class).build();
+ final Partitioner partitioner =
+ useUnsafe
+ ? partitionIdPassthroughCtor.newInstance(numPartitions)
+ : new HashPartitioner(numPartitions);
+ Mockito.doReturn(partitioner).when(dependency).partitioner();
+ Mockito.doReturn(serializer).when(dependency).serializer();
+
+ final File tempFile = new File(tempDir, UUID.randomUUID().toString());
+ final DummyShuffleClient client = new DummyShuffleClient(conf, tempFile);
+ client.initReducePartitionMap(shuffleId, numPartitions, 1);
+
+ final CelebornShuffleHandle<Integer, String, String> handle =
+ new CelebornShuffleHandle<>(
+ appId, host, port, userIdentifier, shuffleId, false, numMaps,
dependency);
+ final ShuffleWriter<Integer, String> writer =
+ createShuffleWriter(handle, taskContext, conf, client,
metrics.shuffleWriteMetrics());
+
+ AtomicInteger total = new AtomicInteger(0);
+ // Use mix=true to exercise both giant and normal record paths.
+ Iterator iterator = getIterator(approximateSize, total, useUnsafe, true);
+
+ writer.write(iterator);
+ Option<MapStatus> status = writer.stop(true);
+
+ assertNotNull(status);
+ assertTrue(status.isDefined());
+
+ // mapId=0 and attemptId=0 from the mock TaskContext.
+ String mapKey = Utils.makeMapKey(shuffleId, 0, 0);
+ PushState pushState = client.getPushState(mapKey);
+
+ int[] crcPerPartition = pushState.getCRC32PerPartition(true,
numPartitions);
+ long[] bytesPerPartition = pushState.getBytesWrittenPerPartition(true,
numPartitions);
+
+ Map<Integer, List<byte[]>> crcData = client.getCrcDataByPartition(mapKey);
+ Map<Integer, List<byte[]>> pushData =
client.getPushDataByPartition(mapKey);
+
+ for (int i = 0; i < numPartitions; i++) {
+ List<byte[]> crcBatches = crcData.getOrDefault(i,
Collections.emptyList());
+ long expectedBytes = 0;
+ CommitMetadata expected = new CommitMetadata();
+ for (byte[] batch : crcBatches) {
+ expected.addDataWithOffsetAndLength(batch, 0, batch.length);
+ expectedBytes += batch.length;
+ }
+ assertEquals("Partition " + i + " bytes mismatch", expectedBytes,
bytesPerPartition[i]);
+ assertEquals("Partition " + i + " CRC mismatch", expected.getChecksum(),
crcPerPartition[i]);
+
+ int pushCalls = pushData.getOrDefault(i, Collections.emptyList()).size();
+ assertEquals(
+ "Partition " + i + " CRC/push call count mismatch", pushCalls,
crcBatches.size());
+ }
+
+ client.shutdown();
+ }
+
private void check(
final int approximateSize, final CelebornConf conf, final Serializer
serializer)
throws Exception {
diff --git
a/client/src/main/java/org/apache/celeborn/client/DummyShuffleClient.java
b/client/src/main/java/org/apache/celeborn/client/DummyShuffleClient.java
index 6ca3406be..8390a9c1b 100644
--- a/client/src/main/java/org/apache/celeborn/client/DummyShuffleClient.java
+++ b/client/src/main/java/org/apache/celeborn/client/DummyShuffleClient.java
@@ -25,6 +25,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -47,6 +49,7 @@ import org.apache.celeborn.common.protocol.PbStreamHandler;
import org.apache.celeborn.common.rpc.RpcEndpointRef;
import org.apache.celeborn.common.util.ExceptionMaker;
import org.apache.celeborn.common.util.JavaUtils;
+import org.apache.celeborn.common.util.Utils;
import org.apache.celeborn.common.write.LocationPushFailedBatches;
import org.apache.celeborn.common.write.PushState;
@@ -56,15 +59,24 @@ public class DummyShuffleClient extends ShuffleClient {
private final OutputStream os;
private final CelebornConf conf;
+ private final boolean shuffleIntegrityCheckEnabled;
private final Map<Integer, ConcurrentHashMap<Integer, PartitionLocation>>
reducePartitionMap =
new HashMap<>();
+ // Tracking for CRC verification in tests
+ private final Map<String, PushState> pushStateMap =
JavaUtils.newConcurrentHashMap();
+ private final Map<String, Map<Integer, List<byte[]>>> crcDataByMapKey =
+ JavaUtils.newConcurrentHashMap();
+ private final Map<String, Map<Integer, List<byte[]>>> pushDataByMapKey =
+ JavaUtils.newConcurrentHashMap();
+
public AtomicInteger fetchFailureCount = new AtomicInteger();
public DummyShuffleClient(CelebornConf conf, File file) throws Exception {
this.os = new BufferedOutputStream(new FileOutputStream(file));
this.conf = conf;
+ this.shuffleIntegrityCheckEnabled =
conf.clientShuffleIntegrityCheckEnabled();
}
@Override
@@ -88,10 +100,40 @@ public class DummyShuffleClient extends ShuffleClient {
int numMappers,
int numPartitions)
throws IOException {
+ String mapKey = Utils.makeMapKey(shuffleId, mapId, attemptId);
+ Map<Integer, List<byte[]>> partitionData =
+ pushDataByMapKey.computeIfAbsent(mapKey, k ->
JavaUtils.newConcurrentHashMap());
+ partitionData
+ .computeIfAbsent(partitionId, k -> Collections.synchronizedList(new
ArrayList<>()))
+ .add(Arrays.copyOfRange(data, offset, offset + length));
+
os.write(data, offset, length);
return length;
}
+ @Override
+ public void computeBatchCRC(
+ int shuffleId,
+ int mapId,
+ int attemptId,
+ int partitionId,
+ byte[] data,
+ int offset,
+ int length) {
+ if (!shuffleIntegrityCheckEnabled) {
+ return;
+ }
+ String mapKey = Utils.makeMapKey(shuffleId, mapId, attemptId);
+ PushState pushState = pushStateMap.computeIfAbsent(mapKey, k -> new
PushState(conf));
+ pushState.addDataWithOffsetAndLength(partitionId, data, offset, length);
+
+ Map<Integer, List<byte[]>> partitionData =
+ crcDataByMapKey.computeIfAbsent(mapKey, k ->
JavaUtils.newConcurrentHashMap());
+ partitionData
+ .computeIfAbsent(partitionId, k -> Collections.synchronizedList(new
ArrayList<>()))
+ .add(Arrays.copyOfRange(data, offset, offset + length));
+ }
+
@Override
public int mergeData(
int shuffleId,
@@ -104,6 +146,13 @@ public class DummyShuffleClient extends ShuffleClient {
int numMappers,
int numPartitions)
throws IOException {
+ String mapKey = Utils.makeMapKey(shuffleId, mapId, attemptId);
+ Map<Integer, List<byte[]>> partitionData =
+ pushDataByMapKey.computeIfAbsent(mapKey, k ->
JavaUtils.newConcurrentHashMap());
+ partitionData
+ .computeIfAbsent(partitionId, k -> Collections.synchronizedList(new
ArrayList<>()))
+ .add(Arrays.copyOfRange(data, offset, offset + length));
+
os.write(data, offset, length);
return length;
}
@@ -188,7 +237,15 @@ public class DummyShuffleClient extends ShuffleClient {
@Override
public PushState getPushState(String mapKey) {
- return new PushState(conf);
+ return pushStateMap.computeIfAbsent(mapKey, k -> new PushState(conf));
+ }
+
+ public Map<Integer, List<byte[]>> getCrcDataByPartition(String mapKey) {
+ return crcDataByMapKey.getOrDefault(mapKey, Collections.emptyMap());
+ }
+
+ public Map<Integer, List<byte[]>> getPushDataByPartition(String mapKey) {
+ return pushDataByMapKey.getOrDefault(mapKey, Collections.emptyMap());
}
@Override
diff --git a/client/src/main/java/org/apache/celeborn/client/ShuffleClient.java
b/client/src/main/java/org/apache/celeborn/client/ShuffleClient.java
index 7a89b051d..7035478eb 100644
--- a/client/src/main/java/org/apache/celeborn/client/ShuffleClient.java
+++ b/client/src/main/java/org/apache/celeborn/client/ShuffleClient.java
@@ -161,7 +161,44 @@ public abstract class ShuffleClient {
public abstract void setExtension(byte[] extension);
/**
- * Write data to a specific reduce partition
+ * Write data to a specific reduce partition, computing and recording a CRC
over the batch before
+ * pushing. Prefer this over {@link #pushData} at all writer call sites.
+ *
+ * @param shuffleId the unique shuffle id of the application
+ * @param mapId the map id of the shuffle
+ * @param attemptId the attempt id of the map task, i.e. speculative task or
task rerun for Apache
+ * Spark
+ * @param partitionId the partition id the data belongs to
+ * @param data byte array containing data to be pushed
+ * @param offset start position of data to be pushed
+ * @param length length of data to be pushed
+ * @param numMappers the number map tasks in the shuffle
+ * @param numPartitions the number of partitions in the shuffle
+ * @return bytes pushed
+ * @throws IOException
+ */
+ public int pushDataWithCRC(
+ int shuffleId,
+ int mapId,
+ int attemptId,
+ int partitionId,
+ byte[] data,
+ int offset,
+ int length,
+ int numMappers,
+ int numPartitions)
+ throws IOException {
+ computeBatchCRC(shuffleId, mapId, attemptId, partitionId, data, offset,
length);
+ return pushData(
+ shuffleId, mapId, attemptId, partitionId, data, offset, length,
numMappers, numPartitions);
+ }
+
+ /**
+ * Write data to a specific reduce partition.
+ *
+ * <p><b>Internal use only.</b> Callers outside the async push pipeline
(i.e. {@link
+ * org.apache.celeborn.client.write.DataPusher}) should use {@link
#pushDataWithCRC} instead,
+ * which additionally records a CRC over the batch for end-to-end integrity
checking.
*
* @param shuffleId the unique shuffle id of the application
* @param mapId the map id of the shuffle
@@ -188,6 +225,73 @@ public abstract class ShuffleClient {
int numPartitions)
throws IOException;
+ /**
+ * Pre-compute CRC for a batch immediately after assembly in the writer,
before the data enters
+ * the async push pipeline. This is the sole CRC accumulation path when
shuffle integrity check is
+ * enabled.
+ */
+ public abstract void computeBatchCRC(
+ int shuffleId,
+ int mapId,
+ int attemptId,
+ int partitionId,
+ byte[] data,
+ int offset,
+ int length);
+
+ /**
+ * Merge data into a specific reduce partition, computing and recording a
CRC over the batch
+ * before merging. Prefer this over {@link #mergeData} at all writer call
sites.
+ *
+ * @param shuffleId the unique shuffle id of the application
+ * @param mapId the map id of the shuffle
+ * @param attemptId the attempt id of the map task, i.e. speculative task or
task rerun for Apache
+ * Spark
+ * @param partitionId the partition id the data belongs to
+ * @param data byte array containing data to be merged
+ * @param offset start position of data to be merged
+ * @param length length of data to be merged
+ * @param numMappers the number map tasks in the shuffle
+ * @param numPartitions the number of partitions in the shuffle
+ * @return bytes merged
+ * @throws IOException
+ */
+ public int mergeDataWithCRC(
+ int shuffleId,
+ int mapId,
+ int attemptId,
+ int partitionId,
+ byte[] data,
+ int offset,
+ int length,
+ int numMappers,
+ int numPartitions)
+ throws IOException {
+ computeBatchCRC(shuffleId, mapId, attemptId, partitionId, data, offset,
length);
+ return mergeData(
+ shuffleId, mapId, attemptId, partitionId, data, offset, length,
numMappers, numPartitions);
+ }
+
+ /**
+ * Merge data into a specific reduce partition.
+ *
+ * <p><b>Internal use only.</b> Callers outside the async push pipeline
should use {@link
+ * #mergeDataWithCRC} instead, which additionally records a CRC over the
batch for end-to-end
+ * integrity checking.
+ *
+ * @param shuffleId the unique shuffle id of the application
+ * @param mapId the map id of the shuffle
+ * @param attemptId the attempt id of the map task, i.e. speculative task or
task rerun for Apache
+ * Spark
+ * @param partitionId the partition id the data belongs to
+ * @param data byte array containing data to be merged
+ * @param offset start position of data to be merged
+ * @param length length of data to be merged
+ * @param numMappers the number map tasks in the shuffle
+ * @param numPartitions the number of partitions in the shuffle
+ * @return bytes merged
+ * @throws IOException
+ */
public abstract int mergeData(
int shuffleId,
int mapId,
diff --git
a/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
b/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
index 225a9cb99..358bc227a 100644
--- a/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
+++ b/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
@@ -1044,12 +1044,6 @@ public class ShuffleClientImpl extends ShuffleClient {
// increment batchId
final int nextBatchId = pushState.nextBatchId();
- // Track commit metadata if shuffle compression and integrity check are
enabled and this request
- // is not for pushing metadata itself.
- if (shuffleIntegrityCheckEnabled) {
- pushState.addDataWithOffsetAndLength(partitionId, data, offset, length);
- }
-
if (shuffleCompressionEnabled && !skipCompress) {
// compress data
final Compressor compressor = compressorThreadLocal.get();
@@ -1404,6 +1398,26 @@ public class ShuffleClientImpl extends ShuffleClient {
false);
}
+ @Override
+ public void computeBatchCRC(
+ int shuffleId,
+ int mapId,
+ int attemptId,
+ int partitionId,
+ byte[] data,
+ int offset,
+ int length) {
+ if (!shuffleIntegrityCheckEnabled) {
+ return;
+ }
+ final String mapKey = Utils.makeMapKey(shuffleId, mapId, attemptId);
+ if (mapperEnded(shuffleId, mapId)) {
+ return;
+ }
+ PushState pushState = getPushState(mapKey);
+ pushState.addDataWithOffsetAndLength(partitionId, data, offset, length);
+ }
+
@Override
public void pushMergedData(int shuffleId, int mapId, int attemptId) throws
IOException {
final String mapKey = Utils.makeMapKey(shuffleId, mapId, attemptId);
diff --git
a/client/src/main/java/org/apache/celeborn/client/write/DataPusher.java
b/client/src/main/java/org/apache/celeborn/client/write/DataPusher.java
index 73af67b07..0677502ab 100644
--- a/client/src/main/java/org/apache/celeborn/client/write/DataPusher.java
+++ b/client/src/main/java/org/apache/celeborn/client/write/DataPusher.java
@@ -153,6 +153,7 @@ public class DataPusher {
public void addTask(int partitionId, byte[] buffer, int size)
throws IOException, InterruptedException {
+ client.computeBatchCRC(shuffleId, mapId, attemptId, partitionId, buffer,
0, size);
try {
PushTask task = null;
while (task == null) {
@@ -208,6 +209,8 @@ public class DataPusher {
}
protected void pushData(PushTask task) throws IOException {
+ // CRC was already recorded in addTask() on the writer thread before the
buffer was enqueued.
+ // Use the bare pushData here to avoid double-counting the same batch into
CommitMetadata.
int bytesWritten =
client.pushData(
shuffleId,
diff --git
a/client/src/test/java/org/apache/celeborn/client/ShuffleClientSuiteJ.java
b/client/src/test/java/org/apache/celeborn/client/ShuffleClientSuiteJ.java
index e6d450d87..74fe6379c 100644
--- a/client/src/test/java/org/apache/celeborn/client/ShuffleClientSuiteJ.java
+++ b/client/src/test/java/org/apache/celeborn/client/ShuffleClientSuiteJ.java
@@ -47,6 +47,7 @@ import org.mockito.ArgumentCaptor;
import org.apache.celeborn.client.compress.Compressor;
import org.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.CommitMetadata;
import org.apache.celeborn.common.exception.CelebornIOException;
import org.apache.celeborn.common.identity.UserIdentifier;
import org.apache.celeborn.common.network.client.TransportClient;
@@ -61,6 +62,8 @@ import
org.apache.celeborn.common.protocol.message.ControlMessages.RegisterShuff
import org.apache.celeborn.common.protocol.message.StatusCode;
import org.apache.celeborn.common.rpc.RpcEndpointRef;
import org.apache.celeborn.common.rpc.RpcTimeoutException;
+import org.apache.celeborn.common.util.Utils;
+import org.apache.celeborn.common.write.PushState;
public class ShuffleClientSuiteJ {
@@ -709,4 +712,98 @@ public class ShuffleClientSuiteJ {
assertEquals(crc32, capturedRequest.getCrc32());
assertEquals(bytesWritten, capturedRequest.getBytesWritten());
}
+
+ @Test
+ public void testComputeBatchCRCAccumulatesCorrectly() {
+ CelebornConf conf = new CelebornConf();
+ conf.set("celeborn.client.shuffle.integrityCheck.enabled", "true");
+ shuffleClient =
+ new ShuffleClientImpl(TEST_APPLICATION_ID, conf, new
UserIdentifier("mock", "mock"));
+ shuffleClient.setupLifecycleManagerRef(endpointRef);
+
+ byte[] batch0 = "hello world".getBytes(StandardCharsets.UTF_8);
+ byte[] batch1a = "foo".getBytes(StandardCharsets.UTF_8);
+ byte[] batch1b = "bar".getBytes(StandardCharsets.UTF_8);
+
+ shuffleClient.computeBatchCRC(
+ TEST_SHUFFLE_ID, TEST_MAP_ID, TEST_ATTEMPT_ID, 0, batch0, 0,
batch0.length);
+ shuffleClient.computeBatchCRC(
+ TEST_SHUFFLE_ID, TEST_MAP_ID, TEST_ATTEMPT_ID, 1, batch1a, 0,
batch1a.length);
+ shuffleClient.computeBatchCRC(
+ TEST_SHUFFLE_ID, TEST_MAP_ID, TEST_ATTEMPT_ID, 1, batch1b, 0,
batch1b.length);
+
+ PushState pushState =
+ shuffleClient.getPushState(Utils.makeMapKey(TEST_SHUFFLE_ID,
TEST_MAP_ID, TEST_ATTEMPT_ID));
+
+ int numPartitions = 2;
+ int[] crcPerPartition = pushState.getCRC32PerPartition(true,
numPartitions);
+ long[] bytesPerPartition = pushState.getBytesWrittenPerPartition(true,
numPartitions);
+
+ // compute expected values via CommitMetadata — same code path as
production
+ CommitMetadata expected0 = new CommitMetadata();
+ expected0.addDataWithOffsetAndLength(batch0, 0, batch0.length);
+ assertEquals(expected0.getChecksum(), crcPerPartition[0]);
+ assertEquals(expected0.getBytes(), bytesPerPartition[0]);
+
+ CommitMetadata expected1 = new CommitMetadata();
+ expected1.addDataWithOffsetAndLength(batch1a, 0, batch1a.length);
+ expected1.addDataWithOffsetAndLength(batch1b, 0, batch1b.length);
+ assertEquals(expected1.getChecksum(), crcPerPartition[1]);
+ assertEquals(expected1.getBytes(), bytesPerPartition[1]);
+ }
+
+ @Test
+ public void testComputeBatchCRCDisabled() {
+ CelebornConf conf = new CelebornConf();
+ conf.set("celeborn.client.shuffle.integrityCheck.enabled", "false");
+ shuffleClient =
+ new ShuffleClientImpl(TEST_APPLICATION_ID, conf, new
UserIdentifier("mock", "mock"));
+ shuffleClient.setupLifecycleManagerRef(endpointRef);
+
+ byte[] data = "hello world".getBytes(StandardCharsets.UTF_8);
+ shuffleClient.computeBatchCRC(
+ TEST_SHUFFLE_ID, TEST_MAP_ID, TEST_ATTEMPT_ID, 0, data, 0,
data.length);
+
+ PushState pushState =
+ shuffleClient.getPushState(Utils.makeMapKey(TEST_SHUFFLE_ID,
TEST_MAP_ID, TEST_ATTEMPT_ID));
+
+ int numPartitions = 2;
+ // When integrity check is disabled at the client level, computeBatchCRC
is a no-op,
+ // so the PushState should remain empty even though we pass true to the
getter.
+ int[] crcPerPartition = pushState.getCRC32PerPartition(true,
numPartitions);
+ long[] bytesPerPartition = pushState.getBytesWrittenPerPartition(true,
numPartitions);
+
+ for (int i = 0; i < numPartitions; i++) {
+ assertEquals("Partition " + i + " CRC should be zero", 0,
crcPerPartition[i]);
+ assertEquals("Partition " + i + " bytes should be zero", 0,
bytesPerPartition[i]);
+ }
+ }
+
+ @Test
+ public void testComputeBatchCRCAttemptIdConsistency() {
+ CelebornConf conf = new CelebornConf();
+ conf.set("celeborn.client.shuffle.integrityCheck.enabled", "true");
+ shuffleClient =
+ new ShuffleClientImpl(TEST_APPLICATION_ID, conf, new
UserIdentifier("mock", "mock"));
+ shuffleClient.setupLifecycleManagerRef(endpointRef);
+
+ byte[] data = "test".getBytes(StandardCharsets.UTF_8);
+
+ // Call with attemptId=0
+ shuffleClient.computeBatchCRC(
+ TEST_SHUFFLE_ID, TEST_MAP_ID, TEST_ATTEMPT_ID, 0, data, 0,
data.length);
+
+ // PushState for attemptId=0 should have the data
+ PushState pushState0 =
+ shuffleClient.getPushState(Utils.makeMapKey(TEST_SHUFFLE_ID,
TEST_MAP_ID, TEST_ATTEMPT_ID));
+ int[] crc0 = pushState0.getCRC32PerPartition(true, 2);
+ assertTrue(crc0[0] != 0);
+
+ // PushState for attemptId=1 should be empty
+ PushState pushState1 =
+ shuffleClient.getPushState(
+ Utils.makeMapKey(TEST_SHUFFLE_ID, TEST_MAP_ID, TEST_ATTEMPT_ID +
1));
+ int[] crc1 = pushState1.getCRC32PerPartition(true, 2);
+ assertEquals(0, crc1[0]);
+ }
}