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 1e715b2cf2 [CELEBORN-2329][CIP22] Encryption at Rest Spark Impl
1e715b2cf2 is described below
commit 1e715b2cf2936a3704c6a44da463628f7e737f35
Author: Aravind Patnam <[email protected]>
AuthorDate: Wed Jul 8 13:13:12 2026 +0800
[CELEBORN-2329][CIP22] Encryption at Rest Spark Impl
### What changes were proposed in this pull request?
Adds EAR support for Spark side.
See more details in
[doc](https://docs.google.com/document/d/1xBrLtpb8bk8CdJENiM3aLJCbFThRKLor8uAjSsfXoxE/edit?usp=sharing).
### Why are the changes needed?
See more details in
[doc](https://docs.google.com/document/d/1xBrLtpb8bk8CdJENiM3aLJCbFThRKLor8uAjSsfXoxE/edit?usp=sharing).
### Does this PR resolve a correctness bug?
- [ ] Yes.
### Does this PR introduce _any_ user-facing change?
- [ ] Yes.
### How was this patch tested?
Unit tests and tested in production internally.
Closes #3689 from akpatnam25/ear-spark-impl.
Lead-authored-by: Aravind Patnam <[email protected]>
Co-authored-by: Aravind Patnam <[email protected]>
Signed-off-by: Nicholas Jiang <[email protected]>
---
.../spark/shuffle/celeborn/SparkCommonUtils.java | 29 ++
.../spark/shuffle/celeborn/SparkCryptoHandler.java | 91 ++++++
.../shuffle/celeborn/SparkCryptoHandlerSuiteJ.java | 165 ++++++++++
.../celeborn/CelebornColumnarShuffleReader.scala | 9 +-
.../CelebornColumnarShuffleReaderSuite.scala | 8 +-
.../celeborn/CelebornColumnarShuffleReader.scala | 9 +-
.../CelebornColumnarShuffleReaderSuite.scala | 8 +-
.../shuffle/celeborn/SparkShuffleManager.java | 28 +-
.../apache/spark/shuffle/celeborn/SparkUtils.java | 11 +-
.../shuffle/celeborn/CelebornShuffleReader.scala | 35 ++-
.../celeborn/CelebornShuffleReaderSuite.scala | 2 +-
.../celeborn/CelebornColumnarShuffleReader.scala | 9 +-
.../CelebornColumnarShuffleReaderSuite.scala | 8 +-
.../apache/celeborn/client/DummyShuffleClient.java | 5 +
.../org/apache/celeborn/client/ShuffleClient.java | 24 ++
.../apache/celeborn/client/ShuffleClientImpl.java | 32 +-
.../celeborn/client/read/CelebornInputStream.java | 162 ++++++----
.../celeborn/client/security/CryptoHandler.java | 26 ++
.../CelebornInputStreamCryptoRoundTripSuiteJ.java | 334 +++++++++++++++++++++
.../read/CelebornInputStreamPeerFailoverTest.java | 8 +-
20 files changed, 924 insertions(+), 79 deletions(-)
diff --git
a/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCommonUtils.java
b/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCommonUtils.java
index 84d74f8c14..08e217390c 100644
---
a/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCommonUtils.java
+++
b/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCommonUtils.java
@@ -19,15 +19,25 @@ package org.apache.spark.shuffle.celeborn;
import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
+
+import scala.Option;
import org.apache.spark.SparkConf;
+import org.apache.spark.SparkEnv;
import org.apache.spark.TaskContext;
+import org.apache.spark.internal.config.package$;
import org.apache.spark.memory.SparkOutOfMemoryError;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.reflect.DynConstructors;
import org.apache.celeborn.reflect.DynMethods;
public class SparkCommonUtils {
+ private static final Logger logger =
LoggerFactory.getLogger(SparkCommonUtils.class);
+
public static void validateAttemptConfig(SparkConf conf) throws
IllegalArgumentException {
int DEFAULT_MAX_CONSECUTIVE_STAGE_ATTEMPTS = 4;
int maxStageAttempts =
@@ -96,4 +106,23 @@ public class SparkCommonUtils {
}
}
}
+
+ public static Optional<CryptoHandler> getCryptoHandler(SparkConf conf) {
+ if (!(Boolean) conf.get(package$.MODULE$.IO_ENCRYPTION_ENABLED())) {
+ return Optional.empty();
+ }
+ SparkEnv env = SparkEnv.get();
+ if (env == null) {
+ return Optional.empty();
+ }
+ Option<byte[]> key = env.securityManager().getIOEncryptionKey();
+ if (!key.isDefined()) {
+ logger.warn(
+ "IO encryption is enabled (spark.io.encryption.enabled=true) but the
IO encryption key "
+ + "is not available from the SecurityManager. Shuffle data will
be written as "
+ + "plaintext. Ensure the SecurityManager provides an IO
encryption key.");
+ return Optional.empty();
+ }
+ return Optional.of(new SparkCryptoHandler(conf, key.get()));
+ }
}
diff --git
a/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCryptoHandler.java
b/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCryptoHandler.java
new file mode 100644
index 0000000000..bf3922d484
--- /dev/null
+++
b/client-spark/common/src/main/java/org/apache/spark/shuffle/celeborn/SparkCryptoHandler.java
@@ -0,0 +1,91 @@
+/*
+ * 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.spark.shuffle.celeborn;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Properties;
+
+import org.apache.spark.SparkConf;
+import org.apache.spark.security.CryptoStreamUtils;
+
+import org.apache.celeborn.client.security.CryptoHandler;
+
+public class SparkCryptoHandler implements CryptoHandler {
+ // On-wire format: [4-byte plaintext length][16-byte IV][ciphertext].
+ // The minimum overhead (length prefix + IV) added by the crypto stream.
+ private static final int CRYPTO_OVERHEAD_BYTES =
+ Integer.BYTES + CryptoStreamUtils.IV_LENGTH_IN_BYTES();
+
+ private final SparkConf sparkConf;
+ private final byte[] key;
+ // Each push thread reuses its own ByteArrayOutputStream to avoid per-batch
allocation.
+ // The internal buffer grows to the high-water-mark once per thread and is
reset() each call.
+ private final ThreadLocal<ByteArrayOutputStream> encryptBaos =
+ ThreadLocal.withInitial(ByteArrayOutputStream::new);
+
+ public SparkCryptoHandler(SparkConf sparkConf, byte[] key) {
+ // Pre-filter sparkConf to only crypto-relevant keys so that
+ // CryptoStreamUtils.toCryptoConf() does not scan the full SparkConf on
every batch.
+ Properties cryptoProps = CryptoStreamUtils.toCryptoConf(sparkConf);
+ SparkConf minimalConf = new SparkConf(false);
+ String prefix =
CryptoStreamUtils.SPARK_IO_ENCRYPTION_COMMONS_CONFIG_PREFIX();
+ for (String propKey : cryptoProps.stringPropertyNames()) {
+ minimalConf.set(prefix + propKey, cryptoProps.getProperty(propKey));
+ }
+ this.sparkConf = minimalConf;
+ this.key = key;
+ }
+
+ @Override
+ public byte[] encrypt(byte[] input, int offset, int length) throws
IOException {
+ ByteArrayOutputStream baos = encryptBaos.get();
+ baos.reset();
+ DataOutputStream dos = new DataOutputStream(baos);
+ dos.writeInt(length);
+ try (OutputStream cos = CryptoStreamUtils.createCryptoOutputStream(dos,
sparkConf, key)) {
+ cos.write(input, offset, length);
+ }
+ return baos.toByteArray();
+ }
+
+ @Override
+ public byte[] decrypt(byte[] input, int offset, int length) throws
IOException {
+ ByteArrayInputStream bais = new ByteArrayInputStream(input, offset,
length);
+ DataInputStream dis = new DataInputStream(bais);
+ int decryptedLength = dis.readInt();
+ // The encrypted payload format is: [4-byte plaintext length][16-byte
IV][ciphertext].
+ // The minimum on-wire overhead is CRYPTO_OVERHEAD_BYTES (4 + 16 = 20), so
the maximum
+ // valid plaintext length is length - 20. A value outside this range
indicates corruption
+ // or a wrong key.
+ if (decryptedLength < 0 || decryptedLength > length -
CRYPTO_OVERHEAD_BYTES) {
+ throw new IOException(
+ "Invalid decrypted length: " + decryptedLength + ", encrypted
length: " + length);
+ }
+ try (DataInputStream cis =
+ new DataInputStream(CryptoStreamUtils.createCryptoInputStream(dis,
sparkConf, key))) {
+ byte[] decrypted = new byte[decryptedLength];
+ cis.readFully(decrypted);
+ return decrypted;
+ }
+ }
+}
diff --git
a/client-spark/common/src/test/java/org/apache/spark/shuffle/celeborn/SparkCryptoHandlerSuiteJ.java
b/client-spark/common/src/test/java/org/apache/spark/shuffle/celeborn/SparkCryptoHandlerSuiteJ.java
new file mode 100644
index 0000000000..6baa62626f
--- /dev/null
+++
b/client-spark/common/src/test/java/org/apache/spark/shuffle/celeborn/SparkCryptoHandlerSuiteJ.java
@@ -0,0 +1,165 @@
+/*
+ * 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.spark.shuffle.celeborn;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import org.apache.spark.SparkConf;
+import org.apache.spark.internal.config.package$;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.celeborn.client.security.CryptoHandler;
+
+public class SparkCryptoHandlerSuiteJ {
+
+ private byte[] key;
+ private CryptoHandler handler;
+
+ @Before
+ public void setUp() {
+ key = new byte[16];
+ new SecureRandom().nextBytes(key);
+ SparkConf sparkConf = new SparkConf(false);
+ sparkConf.set(package$.MODULE$.IO_ENCRYPTION_ENABLED(), true);
+ handler = new SparkCryptoHandler(sparkConf, key);
+ }
+
+ @Test
+ public void testRoundTrip() throws IOException {
+ byte[] plaintext = "hello world, this is a test of encryption".getBytes();
+
+ byte[] encrypted = handler.encrypt(plaintext, 0, plaintext.length);
+ assertFalse(
+ "Encrypted output should differ from plaintext",
Arrays.equals(plaintext, encrypted));
+
+ byte[] decrypted = handler.decrypt(encrypted, 0, encrypted.length);
+ assertArrayEquals(plaintext, decrypted);
+ }
+
+ @Test
+ public void testEncryptedDiffersFromPlaintext() throws IOException {
+ byte[] plaintext = "deterministic test data for comparison".getBytes();
+
+ byte[] encrypted = handler.encrypt(plaintext, 0, plaintext.length);
+ assertFalse(
+ "Encrypted output should differ from plaintext",
Arrays.equals(plaintext, encrypted));
+ }
+
+ @Test
+ public void testSameDataEncryptsThenDecrypts() throws IOException {
+ byte[] plaintext = "same data encrypted twice".getBytes();
+
+ byte[] encrypted1 = handler.encrypt(plaintext, 0, plaintext.length);
+ byte[] encrypted2 = handler.encrypt(plaintext, 0, plaintext.length);
+
+ // Both should decrypt to the same plaintext
+ byte[] decrypted1 = handler.decrypt(encrypted1, 0, encrypted1.length);
+ byte[] decrypted2 = handler.decrypt(encrypted2, 0, encrypted2.length);
+
+ assertArrayEquals(plaintext, decrypted1);
+ assertArrayEquals(plaintext, decrypted2);
+ }
+
+ @Test
+ public void testEncryptWithOffset() throws IOException {
+ byte[] actual = "offset test data".getBytes();
+ int offset = 10;
+ byte[] padded = new byte[offset + actual.length + 20];
+ System.arraycopy(actual, 0, padded, offset, actual.length);
+
+ byte[] encrypted = handler.encrypt(padded, offset, actual.length);
+ byte[] decrypted = handler.decrypt(encrypted, 0, encrypted.length);
+
+ assertArrayEquals(actual, decrypted);
+ }
+
+ @Test
+ public void testDecryptWithWrongKeyFails() throws IOException {
+ byte[] plaintext = "secret data".getBytes();
+ byte[] encrypted = handler.encrypt(plaintext, 0, plaintext.length);
+
+ byte[] wrongKey = new byte[16];
+ new SecureRandom().nextBytes(wrongKey);
+ SparkConf sparkConf = new SparkConf(false);
+ sparkConf.set(package$.MODULE$.IO_ENCRYPTION_ENABLED(), true);
+ CryptoHandler wrongHandler = new SparkCryptoHandler(sparkConf, wrongKey);
+
+ byte[] decrypted = null;
+ try {
+ decrypted = wrongHandler.decrypt(encrypted, 0, encrypted.length);
+ } catch (IOException e) {
+ // acceptable — some implementations throw on wrong key
+ return;
+ }
+ // CryptoStreamUtils may return garbage instead of throwing
+ assertFalse(
+ "Decryption with wrong key should not produce original plaintext",
+ Arrays.equals(plaintext, decrypted));
+ }
+
+ @Test
+ public void testLargeData() throws IOException {
+ byte[] plaintext = new byte[64 * 1024]; // 64KB
+ new SecureRandom().nextBytes(plaintext);
+
+ byte[] encrypted = handler.encrypt(plaintext, 0, plaintext.length);
+ byte[] decrypted = handler.decrypt(encrypted, 0, encrypted.length);
+
+ assertArrayEquals(plaintext, decrypted);
+ }
+
+ @Test
+ public void testEmptyData() throws IOException {
+ byte[] encrypted = handler.encrypt(new byte[0], 0, 0);
+
+ byte[] decrypted = handler.decrypt(encrypted, 0, encrypted.length);
+ assertEquals(0, decrypted.length);
+ }
+
+ /**
+ * Verifies that the decrypt bounds check uses {@code length - 20} (4-byte
length prefix + 16-byte
+ * IV), not the previous {@code length - 4}. A crafted payload whose
embedded length value is
+ * between {@code length - 19} and {@code length - 5} (inclusive) must be
rejected.
+ */
+ @Test
+ public void
testDecryptRejectsCraftedLengthBetweenLengthMinus4AndLengthMinus20()
+ throws IOException {
+ // Construct a minimal on-wire buffer: [4-byte length][16-byte IV][0-byte
ciphertext].
+ // Total = 20 bytes. Embed a plaintext length of 1 — valid under the old
(length-4)
+ // guard (1 <= 20-4=16) but invalid under the corrected (length-20) guard
(1 > 20-20=0).
+ int totalLen = 20; // 4 (length prefix) + 16 (IV) + 0 (ciphertext)
+ byte[] crafted = new byte[totalLen];
+ ByteBuffer.wrap(crafted).order(ByteOrder.BIG_ENDIAN).putInt(1); // claim 1
byte of plaintext
+
+ try {
+ handler.decrypt(crafted, 0, crafted.length);
+ fail("Expected IOException for crafted length > length - 20");
+ } catch (IOException e) {
+ assertTrue(
+ "Exception message should mention decrypted length",
+ e.getMessage().contains("decrypted length") ||
e.getMessage().contains("Invalid"));
+ }
+ }
+}
diff --git
a/client-spark/spark-3-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
b/client-spark/spark-3-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
index fd888fb9dc..c32dfaf2eb 100644
---
a/client-spark/spark-3-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
+++
b/client-spark/spark-3-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
@@ -17,12 +17,15 @@
package org.apache.spark.shuffle.celeborn
+import java.util.Optional
+
import org.apache.spark.{ShuffleDependency, TaskContext}
import org.apache.spark.serializer.SerializerInstance
import org.apache.spark.shuffle.ShuffleReadMetricsReporter
import org.apache.spark.sql.execution.UnsafeRowSerializer
import org.apache.spark.sql.execution.columnar.{CelebornBatchBuilder,
CelebornColumnarBatchSerializer}
+import org.apache.celeborn.client.security.CryptoHandler
import org.apache.celeborn.common.CelebornConf
class CelebornColumnarShuffleReader[K, C](
@@ -34,7 +37,8 @@ class CelebornColumnarShuffleReader[K, C](
context: TaskContext,
conf: CelebornConf,
metrics: ShuffleReadMetricsReporter,
- shuffleIdTracker: ExecutorShuffleIdTracker)
+ shuffleIdTracker: ExecutorShuffleIdTracker,
+ cryptoHandler: Optional[CryptoHandler] = Optional.empty())
extends CelebornShuffleReader[K, C](
handle,
startPartition,
@@ -44,7 +48,8 @@ class CelebornColumnarShuffleReader[K, C](
context,
conf,
metrics,
- shuffleIdTracker) {
+ shuffleIdTracker,
+ cryptoHandler) {
override def newSerializerInstance(dep: ShuffleDependency[K, _, C]):
SerializerInstance = {
val schema = CustomShuffleDependencyUtils.getSchema(dep)
diff --git
a/client-spark/spark-3-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
b/client-spark/spark-3-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
index d0f4462be3..edc67d9833 100644
---
a/client-spark/spark-3-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
+++
b/client-spark/spark-3-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
@@ -17,6 +17,8 @@
package org.apache.spark.shuffle.celeborn
+import java.util.Optional
+
import org.apache.spark.{ShuffleDependency, SparkConf, TaskContext}
import org.apache.spark.serializer.{KryoSerializer, KryoSerializerInstance}
import org.apache.spark.sql.execution.UnsafeRowSerializer
@@ -58,7 +60,8 @@ class CelebornColumnarShuffleReaderSuite {
taskContext,
new CelebornConf(),
null,
- new ExecutorShuffleIdTracker())
+ new ExecutorShuffleIdTracker(),
+ Optional.empty())
assert(shuffleReader.getClass ==
classOf[CelebornColumnarShuffleReader[Int, String]])
} finally {
if (shuffleClient != null) {
@@ -92,7 +95,8 @@ class CelebornColumnarShuffleReaderSuite {
taskContext,
new CelebornConf(),
null,
- new ExecutorShuffleIdTracker())
+ new ExecutorShuffleIdTracker(),
+ Optional.empty())
val shuffleDependency = Mockito.mock(classOf[ShuffleDependency[Int,
String, String]])
Mockito.when(shuffleDependency.shuffleId).thenReturn(0)
Mockito.when(shuffleDependency.serializer).thenReturn(new KryoSerializer(
diff --git
a/client-spark/spark-3.5-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
b/client-spark/spark-3.5-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
index fd888fb9dc..c32dfaf2eb 100644
---
a/client-spark/spark-3.5-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
+++
b/client-spark/spark-3.5-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
@@ -17,12 +17,15 @@
package org.apache.spark.shuffle.celeborn
+import java.util.Optional
+
import org.apache.spark.{ShuffleDependency, TaskContext}
import org.apache.spark.serializer.SerializerInstance
import org.apache.spark.shuffle.ShuffleReadMetricsReporter
import org.apache.spark.sql.execution.UnsafeRowSerializer
import org.apache.spark.sql.execution.columnar.{CelebornBatchBuilder,
CelebornColumnarBatchSerializer}
+import org.apache.celeborn.client.security.CryptoHandler
import org.apache.celeborn.common.CelebornConf
class CelebornColumnarShuffleReader[K, C](
@@ -34,7 +37,8 @@ class CelebornColumnarShuffleReader[K, C](
context: TaskContext,
conf: CelebornConf,
metrics: ShuffleReadMetricsReporter,
- shuffleIdTracker: ExecutorShuffleIdTracker)
+ shuffleIdTracker: ExecutorShuffleIdTracker,
+ cryptoHandler: Optional[CryptoHandler] = Optional.empty())
extends CelebornShuffleReader[K, C](
handle,
startPartition,
@@ -44,7 +48,8 @@ class CelebornColumnarShuffleReader[K, C](
context,
conf,
metrics,
- shuffleIdTracker) {
+ shuffleIdTracker,
+ cryptoHandler) {
override def newSerializerInstance(dep: ShuffleDependency[K, _, C]):
SerializerInstance = {
val schema = CustomShuffleDependencyUtils.getSchema(dep)
diff --git
a/client-spark/spark-3.5-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
b/client-spark/spark-3.5-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
index d0f4462be3..edc67d9833 100644
---
a/client-spark/spark-3.5-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
+++
b/client-spark/spark-3.5-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
@@ -17,6 +17,8 @@
package org.apache.spark.shuffle.celeborn
+import java.util.Optional
+
import org.apache.spark.{ShuffleDependency, SparkConf, TaskContext}
import org.apache.spark.serializer.{KryoSerializer, KryoSerializerInstance}
import org.apache.spark.sql.execution.UnsafeRowSerializer
@@ -58,7 +60,8 @@ class CelebornColumnarShuffleReaderSuite {
taskContext,
new CelebornConf(),
null,
- new ExecutorShuffleIdTracker())
+ new ExecutorShuffleIdTracker(),
+ Optional.empty())
assert(shuffleReader.getClass ==
classOf[CelebornColumnarShuffleReader[Int, String]])
} finally {
if (shuffleClient != null) {
@@ -92,7 +95,8 @@ class CelebornColumnarShuffleReaderSuite {
taskContext,
new CelebornConf(),
null,
- new ExecutorShuffleIdTracker())
+ new ExecutorShuffleIdTracker(),
+ Optional.empty())
val shuffleDependency = Mockito.mock(classOf[ShuffleDependency[Int,
String, String]])
Mockito.when(shuffleDependency.shuffleId).thenReturn(0)
Mockito.when(shuffleDependency.serializer).thenReturn(new KryoSerializer(
diff --git
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkShuffleManager.java
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkShuffleManager.java
index ed7865e19f..f528934b39 100644
---
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkShuffleManager.java
+++
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkShuffleManager.java
@@ -19,6 +19,7 @@ package org.apache.spark.shuffle.celeborn;
import java.io.IOException;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.spark.*;
@@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory;
import org.apache.celeborn.client.LifecycleManager;
import org.apache.celeborn.client.ShuffleClient;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.protocol.ShuffleMode;
import org.apache.celeborn.reflect.DynMethods;
@@ -91,6 +93,23 @@ public class SparkShuffleManager implements ShuffleManager {
private ExecutorShuffleIdTracker shuffleIdTracker = new
ExecutorShuffleIdTracker();
+ // The IO encryption key is fixed for the app lifetime. Lazily initialized
on first
+ // writer/reader call (not in the constructor) to ensure SparkEnv is
available.
+ private volatile Optional<CryptoHandler> cryptoHandler = null;
+
+ private Optional<CryptoHandler> getCryptoHandler() {
+ if (cryptoHandler == null) {
+ // Only cache when SparkEnv is ready. If it is transiently null (e.g.
called before
+ // the executor env is initialized), return empty without caching so the
next call retries.
+ if (SparkEnv.get() != null) {
+ cryptoHandler = SparkCommonUtils.getCryptoHandler(conf);
+ } else {
+ return Optional.empty();
+ }
+ }
+ return cryptoHandler;
+ }
+
public SparkShuffleManager(SparkConf conf, boolean isDriver) {
if (conf.getBoolean(SQLConf.LOCAL_SHUFFLE_READER_ENABLED().key(), true)) {
logger.warn(
@@ -288,7 +307,8 @@ public class SparkShuffleManager implements ShuffleManager {
h.lifecycleManagerPort(),
celebornConf,
h.userIdentifier(),
- h.extension());
+ h.extension(),
+ getCryptoHandler());
if (h.stageRerunEnabled()) {
SparkUtils.addFailureListenerIfBarrierTask(shuffleClient, context,
h);
}
@@ -445,7 +465,8 @@ public class SparkShuffleManager implements ShuffleManager {
context,
celebornConf,
metrics,
- shuffleIdTracker);
+ shuffleIdTracker,
+ getCryptoHandler());
} else {
return new CelebornShuffleReader<>(
h,
@@ -456,7 +477,8 @@ public class SparkShuffleManager implements ShuffleManager {
context,
celebornConf,
metrics,
- shuffleIdTracker);
+ shuffleIdTracker,
+ getCryptoHandler());
}
}
diff --git
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkUtils.java
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkUtils.java
index 613ac7f999..89cb7c3792 100644
---
a/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkUtils.java
+++
b/client-spark/spark-3/src/main/java/org/apache/spark/shuffle/celeborn/SparkUtils.java
@@ -24,6 +24,7 @@ import java.io.ObjectOutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -66,6 +67,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.celeborn.client.ShuffleClient;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.exception.CelebornIOException;
import org.apache.celeborn.common.exception.CelebornRuntimeException;
@@ -282,7 +284,8 @@ public class SparkUtils {
TaskContext.class,
CelebornConf.class,
ShuffleReadMetricsReporter.class,
- ExecutorShuffleIdTracker.class)
+ ExecutorShuffleIdTracker.class,
+ Optional.class)
.build();
}
@@ -295,7 +298,8 @@ public class SparkUtils {
TaskContext context,
CelebornConf conf,
ShuffleReadMetricsReporter metrics,
- ExecutorShuffleIdTracker shuffleIdTracker) {
+ ExecutorShuffleIdTracker shuffleIdTracker,
+ Optional<CryptoHandler> cryptoHandler) {
return ColumnarShuffleReaderConstructorHolder.INSTANCE.invoke(
null,
handle,
@@ -306,7 +310,8 @@ public class SparkUtils {
context,
conf,
metrics,
- shuffleIdTracker);
+ shuffleIdTracker,
+ cryptoHandler);
}
// Added in SPARK-32920, for Spark 3.2 and above
diff --git
a/client-spark/spark-3/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReader.scala
b/client-spark/spark-3/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReader.scala
index 155fc08861..32863c6a54 100644
---
a/client-spark/spark-3/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReader.scala
+++
b/client-spark/spark-3/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReader.scala
@@ -18,7 +18,7 @@
package org.apache.spark.shuffle.celeborn
import java.io.IOException
-import java.util.{ArrayList => JArrayList, HashMap => JHashMap, Map => JMap,
Set => JSet}
+import java.util.{ArrayList => JArrayList, HashMap => JHashMap, Map => JMap,
Optional, Set => JSet}
import java.util.concurrent.{ConcurrentHashMap, ThreadPoolExecutor,
TimeoutException, TimeUnit}
import java.util.concurrent.atomic.AtomicReference
import java.util.function.BiFunction
@@ -40,6 +40,7 @@ import org.apache.spark.util.collection.ExternalSorter
import org.apache.celeborn.client.{ClientUtils, ShuffleClient}
import org.apache.celeborn.client.ShuffleClientImpl.ReduceFileGroups
import org.apache.celeborn.client.read.{CelebornInputStream, MetricsCallback}
+import org.apache.celeborn.client.security.CryptoHandler
import org.apache.celeborn.common.CelebornConf
import org.apache.celeborn.common.exception.{CelebornBroadcastException,
CelebornIOException, CelebornRuntimeException, PartitionUnRetryAbleException}
import org.apache.celeborn.common.network.client.TransportClient
@@ -59,7 +60,8 @@ class CelebornShuffleReader[K, C](
conf: CelebornConf,
metrics: ShuffleReadMetricsReporter,
shuffleIdTracker: ExecutorShuffleIdTracker,
- needDecompress: Boolean)
+ needDecompress: Boolean,
+ cryptoHandler: Optional[CryptoHandler])
extends ShuffleReader[K, C] with Logging {
def this(
@@ -81,7 +83,31 @@ class CelebornShuffleReader[K, C](
conf,
metrics,
shuffleIdTracker,
- true)
+ true,
+ Optional.empty())
+
+ def this(
+ handle: CelebornShuffleHandle[K, _, C],
+ startPartition: Int,
+ endPartition: Int,
+ startMapIndex: Int,
+ endMapIndex: Int,
+ context: TaskContext,
+ conf: CelebornConf,
+ metrics: ShuffleReadMetricsReporter,
+ shuffleIdTracker: ExecutorShuffleIdTracker,
+ cryptoHandler: Optional[CryptoHandler]) = this(
+ handle,
+ startPartition,
+ endPartition,
+ startMapIndex,
+ endMapIndex,
+ context,
+ conf,
+ metrics,
+ shuffleIdTracker,
+ true,
+ cryptoHandler)
private val dep = handle.dependency
@@ -92,7 +118,8 @@ class CelebornShuffleReader[K, C](
handle.lifecycleManagerPort,
conf,
handle.userIdentifier,
- handle.extension)
+ handle.extension,
+ cryptoHandler)
private val exceptionRef = new AtomicReference[IOException]
private val stageRerunEnabled = handle.stageRerunEnabled
diff --git
a/client-spark/spark-3/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReaderSuite.scala
b/client-spark/spark-3/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReaderSuite.scala
index d2cec3abf8..901d7797b5 100644
---
a/client-spark/spark-3/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReaderSuite.scala
+++
b/client-spark/spark-3/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornShuffleReaderSuite.scala
@@ -60,7 +60,7 @@ class CelebornShuffleReaderSuite extends AnyFunSuite {
val tmpFile = Files.createTempFile("test", ".tmp").toFile
mockStatic(classOf[ShuffleClient]).when(() =>
- ShuffleClient.get(any(), any(), any(), any(), any(), any())).thenReturn(
+ ShuffleClient.get(any(), any(), any(), any(), any(), any(),
any())).thenReturn(
new DummyShuffleClient(conf, tmpFile))
val shuffleReader =
diff --git
a/client-spark/spark-4-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
b/client-spark/spark-4-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
index fd888fb9dc..c32dfaf2eb 100644
---
a/client-spark/spark-4-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
+++
b/client-spark/spark-4-columnar-shuffle/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReader.scala
@@ -17,12 +17,15 @@
package org.apache.spark.shuffle.celeborn
+import java.util.Optional
+
import org.apache.spark.{ShuffleDependency, TaskContext}
import org.apache.spark.serializer.SerializerInstance
import org.apache.spark.shuffle.ShuffleReadMetricsReporter
import org.apache.spark.sql.execution.UnsafeRowSerializer
import org.apache.spark.sql.execution.columnar.{CelebornBatchBuilder,
CelebornColumnarBatchSerializer}
+import org.apache.celeborn.client.security.CryptoHandler
import org.apache.celeborn.common.CelebornConf
class CelebornColumnarShuffleReader[K, C](
@@ -34,7 +37,8 @@ class CelebornColumnarShuffleReader[K, C](
context: TaskContext,
conf: CelebornConf,
metrics: ShuffleReadMetricsReporter,
- shuffleIdTracker: ExecutorShuffleIdTracker)
+ shuffleIdTracker: ExecutorShuffleIdTracker,
+ cryptoHandler: Optional[CryptoHandler] = Optional.empty())
extends CelebornShuffleReader[K, C](
handle,
startPartition,
@@ -44,7 +48,8 @@ class CelebornColumnarShuffleReader[K, C](
context,
conf,
metrics,
- shuffleIdTracker) {
+ shuffleIdTracker,
+ cryptoHandler) {
override def newSerializerInstance(dep: ShuffleDependency[K, _, C]):
SerializerInstance = {
val schema = CustomShuffleDependencyUtils.getSchema(dep)
diff --git
a/client-spark/spark-4-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
b/client-spark/spark-4-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
index d0f4462be3..edc67d9833 100644
---
a/client-spark/spark-4-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
+++
b/client-spark/spark-4-columnar-shuffle/src/test/scala/org/apache/spark/shuffle/celeborn/CelebornColumnarShuffleReaderSuite.scala
@@ -17,6 +17,8 @@
package org.apache.spark.shuffle.celeborn
+import java.util.Optional
+
import org.apache.spark.{ShuffleDependency, SparkConf, TaskContext}
import org.apache.spark.serializer.{KryoSerializer, KryoSerializerInstance}
import org.apache.spark.sql.execution.UnsafeRowSerializer
@@ -58,7 +60,8 @@ class CelebornColumnarShuffleReaderSuite {
taskContext,
new CelebornConf(),
null,
- new ExecutorShuffleIdTracker())
+ new ExecutorShuffleIdTracker(),
+ Optional.empty())
assert(shuffleReader.getClass ==
classOf[CelebornColumnarShuffleReader[Int, String]])
} finally {
if (shuffleClient != null) {
@@ -92,7 +95,8 @@ class CelebornColumnarShuffleReaderSuite {
taskContext,
new CelebornConf(),
null,
- new ExecutorShuffleIdTracker())
+ new ExecutorShuffleIdTracker(),
+ Optional.empty())
val shuffleDependency = Mockito.mock(classOf[ShuffleDependency[Int,
String, String]])
Mockito.when(shuffleDependency.shuffleId).thenReturn(0)
Mockito.when(shuffleDependency.serializer).thenReturn(new KryoSerializer(
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 8390a9c1b5..86e6a2d191 100644
--- a/client/src/main/java/org/apache/celeborn/client/DummyShuffleClient.java
+++ b/client/src/main/java/org/apache/celeborn/client/DummyShuffleClient.java
@@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -41,6 +42,7 @@ import org.slf4j.LoggerFactory;
import org.apache.celeborn.client.read.CelebornInputStream;
import org.apache.celeborn.client.read.MetricsCallback;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.exception.CelebornIOException;
import org.apache.celeborn.common.network.client.TransportClientFactory;
@@ -79,6 +81,9 @@ public class DummyShuffleClient extends ShuffleClient {
this.shuffleIntegrityCheckEnabled =
conf.clientShuffleIntegrityCheckEnabled();
}
+ @Override
+ public void setupCryptoHandler(Optional<CryptoHandler> cryptoHandler) {}
+
@Override
public void setupLifecycleManagerRef(String host, int port) {}
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 7035478ebf..8bc1a8911c 100644
--- a/client/src/main/java/org/apache/celeborn/client/ShuffleClient.java
+++ b/client/src/main/java/org/apache/celeborn/client/ShuffleClient.java
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
import org.apache.celeborn.client.read.CelebornInputStream;
import org.apache.celeborn.client.read.MetricsCallback;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.exception.CelebornIOException;
import org.apache.celeborn.common.identity.UserIdentifier;
@@ -90,6 +91,18 @@ public abstract class ShuffleClient {
CelebornConf conf,
UserIdentifier userIdentifier,
byte[] extension) {
+ return ShuffleClient.get(
+ appUniqueId, driverHost, port, conf, userIdentifier, extension,
Optional.empty());
+ }
+
+ public static ShuffleClient get(
+ String appUniqueId,
+ String driverHost,
+ int port,
+ CelebornConf conf,
+ UserIdentifier userIdentifier,
+ byte[] extension,
+ Optional<CryptoHandler> cryptoHandler) {
if (null == _instance || !initialized) {
synchronized (ShuffleClient.class) {
if (null == _instance) {
@@ -102,16 +115,25 @@ public abstract class ShuffleClient {
_instance = new ShuffleClientImpl(appUniqueId, conf, userIdentifier);
_instance.setupLifecycleManagerRef(driverHost, port);
_instance.setExtension(extension);
+ _instance.setupCryptoHandler(cryptoHandler);
initialized = true;
} else if (!initialized) {
_instance.shutdown();
_instance = new ShuffleClientImpl(appUniqueId, conf, userIdentifier);
_instance.setupLifecycleManagerRef(driverHost, port);
_instance.setExtension(extension);
+ _instance.setupCryptoHandler(cryptoHandler);
initialized = true;
}
}
}
+ // Apply the crypto handler even when the singleton is already
initialized. This handles
+ // the case where SparkEnv was transiently unavailable during the first
init call (causing
+ // an empty handler to be stored), so that encryption is correctly applied
on retry.
+ // setupCryptoHandler is a volatile write and safe to call without the
lock.
+ if (cryptoHandler != null && cryptoHandler.isPresent()) {
+ _instance.setupCryptoHandler(cryptoHandler);
+ }
return _instance;
}
@@ -150,6 +172,8 @@ public abstract class ShuffleClient {
String.format("%.2f", (localReadCount * 1.0d / totalReadCount) * 100));
}
+ public abstract void setupCryptoHandler(Optional<CryptoHandler>
cryptoHandler);
+
public abstract void setupLifecycleManagerRef(String host, int port);
public abstract void setupLifecycleManagerRef(RpcEndpointRef endpointRef);
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 358bc227a1..46c1189ca4 100644
--- a/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
+++ b/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
@@ -43,6 +43,7 @@ import org.slf4j.LoggerFactory;
import org.apache.celeborn.client.compress.Compressor;
import org.apache.celeborn.client.read.CelebornInputStream;
import org.apache.celeborn.client.read.MetricsCallback;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.exception.CelebornBroadcastException;
import org.apache.celeborn.common.exception.CelebornIOException;
@@ -101,6 +102,8 @@ public class ShuffleClientImpl extends ShuffleClient {
protected byte[] extension;
+ private volatile Optional<CryptoHandler> cryptoHandler = Optional.empty();
+
// key: appShuffleIdentifier, value: shuffleId
protected Map<String, Tuple2<Integer, Boolean>> shuffleIdCache =
JavaUtils.newConcurrentHashMap();
@@ -1054,6 +1057,24 @@ public class ShuffleClientImpl extends ShuffleClient {
length = compressor.getCompressedTotalSize();
}
+ // Snapshot volatile field once to avoid a TOCTOU race between isPresent()
and get().
+ Optional<CryptoHandler> handler = cryptoHandler;
+ if (handler.isPresent()) {
+ byte[] encrypted = handler.get().encrypt(data, offset, length);
+ if (logger.isDebugEnabled()) {
+ logger.debug(
+ "Encrypted shuffle data for shuffle {} map {} partition {}: {}
bytes -> {} bytes.",
+ shuffleId,
+ mapId,
+ partitionId,
+ length,
+ encrypted.length);
+ }
+ data = encrypted;
+ offset = 0;
+ length = encrypted.length;
+ }
+
final byte[] body = new byte[BATCH_HEADER_SIZE + length];
Platform.putInt(body, Platform.BYTE_ARRAY_OFFSET, mapId);
Platform.putInt(body, Platform.BYTE_ARRAY_OFFSET + 4, attemptId);
@@ -2039,7 +2060,8 @@ public class ShuffleClientImpl extends ShuffleClient {
partitionId,
exceptionMaker,
metricsCallback,
- needDecompress);
+ needDecompress,
+ cryptoHandler);
}
}
@@ -2104,6 +2126,14 @@ public class ShuffleClientImpl extends ShuffleClient {
this.extension = extension;
}
+ @Override
+ public void setupCryptoHandler(Optional<CryptoHandler> cryptoHandler) {
+ this.cryptoHandler = cryptoHandler != null ? cryptoHandler :
Optional.empty();
+ if (this.cryptoHandler.isPresent()) {
+ logger.info("IO encryption enabled for shuffle data (encryption at
rest).");
+ }
+ }
+
boolean mapperEnded(int shuffleId, int mapId) {
return (mapperEndMap.containsKey(shuffleId) &&
mapperEndMap.get(shuffleId).contains(mapId))
|| isStageEnded(shuffleId);
diff --git
a/client/src/main/java/org/apache/celeborn/client/read/CelebornInputStream.java
b/client/src/main/java/org/apache/celeborn/client/read/CelebornInputStream.java
index 37e0be3e37..d76b1eb9d3 100644
---
a/client/src/main/java/org/apache/celeborn/client/read/CelebornInputStream.java
+++
b/client/src/main/java/org/apache/celeborn/client/read/CelebornInputStream.java
@@ -39,6 +39,7 @@ import org.apache.celeborn.client.ClientUtils;
import org.apache.celeborn.client.ShuffleClient;
import org.apache.celeborn.client.compress.Decompressor;
import
org.apache.celeborn.client.read.checkpoint.PartitionReaderCheckpointMetadata;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.CommitMetadata;
import org.apache.celeborn.common.exception.CelebornIOException;
@@ -74,7 +75,8 @@ public abstract class CelebornInputStream extends InputStream
{
int partitionId,
ExceptionMaker exceptionMaker,
MetricsCallback metricsCallback,
- boolean needDecompress)
+ boolean needDecompress,
+ Optional<CryptoHandler> cryptoHandler)
throws IOException {
if (locations == null || locations.isEmpty()) {
return emptyInputStream;
@@ -106,7 +108,8 @@ public abstract class CelebornInputStream extends
InputStream {
metricsCallback,
needDecompress,
startMapIndex,
- endMapIndex);
+ endMapIndex,
+ cryptoHandler);
} else {
return new CelebornInputStreamImpl(
conf,
@@ -131,7 +134,8 @@ public abstract class CelebornInputStream extends
InputStream {
metricsCallback,
needDecompress,
-1,
- -1);
+ -1,
+ cryptoHandler);
}
}
}
@@ -188,6 +192,7 @@ public abstract class CelebornInputStream extends
InputStream {
private final Map<String, LocationPushFailedBatches> failedBatches;
+ private byte[] encryptedBuf;
private byte[] compressedBuf;
private byte[] rawDataBuf;
private Decompressor decompressor;
@@ -223,6 +228,7 @@ public abstract class CelebornInputStream extends
InputStream {
private int shuffleId;
private int partitionId;
private ExceptionMaker exceptionMaker;
+ private Optional<CryptoHandler> cryptoHandler;
private boolean closed = false;
private boolean integrityChecked = false;
private final CommitMetadata aggregatedActualCommitMetadata = new
CommitMetadata();
@@ -250,7 +256,8 @@ public abstract class CelebornInputStream extends
InputStream {
MetricsCallback metricsCallback,
boolean needDecompress,
int numberOfSubPartitions,
- int currentIndexOfSubPartition)
+ int currentIndexOfSubPartition,
+ Optional<CryptoHandler> cryptoHandler)
throws IOException {
this(
conf,
@@ -275,7 +282,8 @@ public abstract class CelebornInputStream extends
InputStream {
metricsCallback,
needDecompress,
numberOfSubPartitions,
- currentIndexOfSubPartition);
+ currentIndexOfSubPartition,
+ cryptoHandler);
}
CelebornInputStreamImpl(
@@ -301,7 +309,8 @@ public abstract class CelebornInputStream extends
InputStream {
MetricsCallback metricsCallback,
boolean needDecompress,
int numberOfSubPartitions,
- int currentIndexOfSubPartition)
+ int currentIndexOfSubPartition,
+ Optional<CryptoHandler> cryptoHandler)
throws IOException {
this.conf = conf;
this.clientFactory = clientFactory;
@@ -337,6 +346,7 @@ public abstract class CelebornInputStream extends
InputStream {
this.retryWaitMs =
conf.networkIoRetryWaitMs(TransportModuleConstants.DATA_MODULE);
this.callback = metricsCallback;
this.exceptionMaker = exceptionMaker;
+ this.cryptoHandler = cryptoHandler;
this.partitionId = partitionId;
this.appShuffleId = appShuffleId;
this.shuffleId = shuffleId;
@@ -730,6 +740,7 @@ public abstract class CelebornInputStream extends
InputStream {
compressedBuf = null;
rawDataBuf = null;
+ encryptedBuf = null;
batchesRead = null;
locations = null;
attempts = null;
@@ -790,9 +801,15 @@ public abstract class CelebornInputStream extends
InputStream {
private void init() {
int bufferSize = conf.clientFetchBufferSize();
+ int headerLen = shouldDecompress ?
Decompressor.getCompressionHeaderLength(conf) : 0;
+ if (cryptoHandler.isPresent()) {
+ // Pre-size to include the crypto overhead (e.g. SparkCryptoHandler
adds a 4-byte length
+ // prefix and a 16-byte IV = 20 bytes) so the buffer is large enough
for the first batch
+ // without an immediate reallocation.
+ encryptedBuf = new byte[bufferSize + headerLen + 64];
+ }
if (shouldDecompress) {
- int headerLen = Decompressor.getCompressionHeaderLength(conf);
bufferSize += headerLen;
compressedBuf = new byte[bufferSize];
decompressor = Decompressor.getDecompressor(conf);
@@ -823,65 +840,104 @@ public abstract class CelebornInputStream extends
InputStream {
int batchId = Platform.getInt(sizeBuf, Platform.BYTE_ARRAY_OFFSET +
8);
int size = Platform.getInt(sizeBuf, Platform.BYTE_ARRAY_OFFSET + 12);
- if (shouldDecompress) {
+ // encryptedSize is the on-wire byte count (used for metrics); size
will be
+ // reassigned to the decrypted length after decryption.
+ int encryptedSize = size;
+
+ // Perform dedup/stale-attempt checks before decrypting to avoid
paying the
+ // crypto cost for batches that will be discarded anyway.
+ if (attemptId != attempts[mapId]) {
+ currentChunk.skipBytes(size);
+ continue;
+ }
+ if (readSkewPartitionWithoutMapRange) {
+ LocationPushFailedBatches locationPushFailedBatches =
+
this.failedBatches.get(currentReader.getLocation().getUniqueId());
+ if (null != locationPushFailedBatches) {
+ if (locationPushFailedBatches.contains(mapId, attemptId,
batchId)) {
+ logger.warn(
+ "Skip duplicated batch: mapId={}, attemptId={},
batchId={}",
+ mapId,
+ attemptId,
+ batchId);
+ currentChunk.skipBytes(size);
+ continue;
+ }
+ }
+ }
+ Set<Integer> batchSet = batchesRead.computeIfAbsent(mapId, k -> new
HashSet<>());
+ if (batchSet.contains(batchId)) {
+ callback.incDuplicateBytesRead(BATCH_HEADER_SIZE + encryptedSize);
+ logger.debug(
+ "Skip duplicated batch: mapId {}, attemptId {}, batchId {}.",
+ mapId,
+ attemptId,
+ batchId);
+ currentChunk.skipBytes(size);
+ continue;
+ }
+
+ // Batch is unique and from the correct attempt — now read and
optionally decrypt.
+ if (cryptoHandler.isPresent()) {
+ if (size > encryptedBuf.length) {
+ encryptedBuf = new byte[size];
+ }
+ currentChunk.readBytes(encryptedBuf, 0, size);
+ byte[] decrypted = cryptoHandler.get().decrypt(encryptedBuf, 0,
size);
+ if (logger.isDebugEnabled()) {
+ logger.debug(
+ "Decrypted shuffle data for shuffle {} partition {}: {}
bytes -> {} bytes.",
+ shuffleId,
+ partitionId,
+ size,
+ decrypted.length);
+ }
+ size = decrypted.length;
+ if (shouldDecompress) {
+ if (decrypted.length <
Decompressor.getCompressionHeaderLength(conf)) {
+ throw new IOException(
+ "Decrypted batch too short to contain compression header: "
+ + decrypted.length
+ + " bytes (shuffleId="
+ + shuffleId
+ + ", partitionId="
+ + partitionId
+ + ")");
+ }
+ compressedBuf = decrypted;
+ } else {
+ rawDataBuf = decrypted;
+ }
+ } else if (shouldDecompress) {
if (size > compressedBuf.length) {
compressedBuf = new byte[size];
}
-
currentChunk.readBytes(compressedBuf, 0, size);
} else {
if (size > rawDataBuf.length) {
rawDataBuf = new byte[size];
}
-
currentChunk.readBytes(rawDataBuf, 0, size);
}
- // de-duplicate
- if (attemptId == attempts[mapId]) {
- if (readSkewPartitionWithoutMapRange) {
- LocationPushFailedBatches locationPushFailedBatches =
-
this.failedBatches.get(currentReader.getLocation().getUniqueId());
- if (null != locationPushFailedBatches) {
- if (locationPushFailedBatches.contains(mapId, attemptId,
batchId)) {
- logger.warn(
- "Skip duplicated batch: mapId={}, attemptId={},
batchId={}",
- mapId,
- attemptId,
- batchId);
- continue;
- }
- }
- }
- Set<Integer> batchSet = batchesRead.computeIfAbsent(mapId, k ->
new HashSet<>());
- if (!batchSet.contains(batchId)) {
- batchSet.add(batchId);
- callback.incBytesRead(BATCH_HEADER_SIZE + size);
- if (shouldDecompress) {
- // decompress data
- int originalLength =
decompressor.getOriginalLen(compressedBuf);
- if (rawDataBuf.length < originalLength) {
- rawDataBuf = new byte[originalLength];
- }
- limit = decompressor.decompress(compressedBuf, rawDataBuf, 0);
- } else {
- limit = size;
- }
- if (shuffleIntegrityCheckEnabled) {
-
aggregatedActualCommitMetadata.addDataWithOffsetAndLength(rawDataBuf, 0, limit);
- }
- position = 0;
- hasData = true;
- break;
- } else {
- callback.incDuplicateBytesRead(BATCH_HEADER_SIZE + size);
- logger.debug(
- "Skip duplicated batch: mapId {}, attemptId {}, batchId {}.",
- mapId,
- attemptId,
- batchId);
+ batchSet.add(batchId);
+ callback.incBytesRead(BATCH_HEADER_SIZE + encryptedSize);
+ if (shouldDecompress) {
+ // decompress data
+ int originalLength = decompressor.getOriginalLen(compressedBuf);
+ if (rawDataBuf.length < originalLength) {
+ rawDataBuf = new byte[originalLength];
}
+ limit = decompressor.decompress(compressedBuf, rawDataBuf, 0);
+ } else {
+ limit = size;
+ }
+ if (shuffleIntegrityCheckEnabled) {
+
aggregatedActualCommitMetadata.addDataWithOffsetAndLength(rawDataBuf, 0, limit);
}
+ position = 0;
+ hasData = true;
+ break;
}
if (!hasData) {
diff --git
a/client/src/main/java/org/apache/celeborn/client/security/CryptoHandler.java
b/client/src/main/java/org/apache/celeborn/client/security/CryptoHandler.java
new file mode 100644
index 0000000000..38da34fa79
--- /dev/null
+++
b/client/src/main/java/org/apache/celeborn/client/security/CryptoHandler.java
@@ -0,0 +1,26 @@
+/*
+ * 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.celeborn.client.security;
+
+import java.io.IOException;
+
+public interface CryptoHandler {
+ byte[] encrypt(byte[] input, int offset, int length) throws IOException;
+
+ byte[] decrypt(byte[] input, int offset, int length) throws IOException;
+}
diff --git
a/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamCryptoRoundTripSuiteJ.java
b/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamCryptoRoundTripSuiteJ.java
new file mode 100644
index 0000000000..06655b887b
--- /dev/null
+++
b/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamCryptoRoundTripSuiteJ.java
@@ -0,0 +1,334 @@
+/*
+ * 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.celeborn.client.read;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.apache.celeborn.client.ShuffleClient;
+import org.apache.celeborn.client.compress.Compressor;
+import org.apache.celeborn.client.security.CryptoHandler;
+import org.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.CommitMetadata;
+import org.apache.celeborn.common.network.buffer.NettyManagedBuffer;
+import org.apache.celeborn.common.network.client.ChunkReceivedCallback;
+import org.apache.celeborn.common.network.client.TransportClient;
+import org.apache.celeborn.common.network.client.TransportClientFactory;
+import org.apache.celeborn.common.network.protocol.TransportMessage;
+import org.apache.celeborn.common.protocol.MessageType;
+import org.apache.celeborn.common.protocol.PartitionLocation;
+import org.apache.celeborn.common.protocol.PbStreamHandler;
+import org.apache.celeborn.common.protocol.StorageInfo;
+import org.apache.celeborn.common.unsafe.Platform;
+
+/**
+ * Integration-style round-trip tests for EAR (Encryption At Rest) wiring in
{@link
+ * CelebornInputStream}. These tests verify that the encrypt-on-write /
decrypt-on-read path works
+ * end-to-end, including interactions with compression and the shuffle
integrity check.
+ */
+public class CelebornInputStreamCryptoRoundTripSuiteJ {
+
+ private static final int BATCH_HEADER_SIZE = 16;
+ private static final String SHUFFLE_KEY = "app-1-1";
+
+ /**
+ * A minimal CryptoHandler for testing: the encrypted format is [4-byte
plaintext length
+ * (int)][XOR-encrypted payload]. This matches the structural contract of
SparkCryptoHandler so
+ * the bounds check (decryptedLength > length - 4) is also exercised.
+ */
+ static class XorCryptoHandler implements CryptoHandler {
+ private final byte key;
+
+ XorCryptoHandler(byte key) {
+ this.key = key;
+ }
+
+ @Override
+ public byte[] encrypt(byte[] input, int offset, int length) throws
IOException {
+ // Prefix with 4-byte plaintext length, then XOR-encrypt the payload
+ byte[] out = new byte[4 + length];
+ Platform.putInt(out, Platform.BYTE_ARRAY_OFFSET, length);
+ for (int i = 0; i < length; i++) {
+ out[4 + i] = (byte) (input[offset + i] ^ key);
+ }
+ return out;
+ }
+
+ @Override
+ public byte[] decrypt(byte[] input, int offset, int length) throws
IOException {
+ // Validate the buffer is large enough to hold the 4-byte length prefix
+ if (length < 4) {
+ throw new IOException("Encrypted buffer too short: " + length);
+ }
+ // Read the plaintext length from the 4-byte prefix
+ int decryptedLength = Platform.getInt(input, Platform.BYTE_ARRAY_OFFSET
+ offset);
+ // Validate bounds: the 4-byte prefix must fit inside the encrypted
buffer
+ if (decryptedLength < 0 || decryptedLength > length - 4) {
+ throw new IOException(
+ "Invalid decrypted length: " + decryptedLength + ", encrypted
length: " + length);
+ }
+ byte[] out = new byte[decryptedLength];
+ for (int i = 0; i < decryptedLength; i++) {
+ out[i] = (byte) (input[offset + 4 + i] ^ key);
+ }
+ return out;
+ }
+ }
+
+ /**
+ * Build a single batch ByteBuf as ShuffleClientImpl.pushOrMergeData does:
optionally compress,
+ * optionally encrypt, then prepend the 16-byte batch header.
+ */
+ private ByteBuf buildBatch(
+ byte[] plaintext, boolean compress, CryptoHandler cryptoHandler,
CelebornConf conf)
+ throws IOException {
+ byte[] data = plaintext;
+ int offset = 0;
+ int length = plaintext.length;
+
+ // Step 1: optionally compress (compress-then-encrypt ordering matches
ShuffleClientImpl)
+ if (compress) {
+ Compressor compressor = Compressor.getCompressor(conf);
+ compressor.compress(data, offset, length);
+ data = compressor.getCompressedBuffer();
+ offset = 0;
+ length = compressor.getCompressedTotalSize();
+ }
+
+ // Step 2: optionally encrypt the (possibly compressed) payload
+ if (cryptoHandler != null) {
+ data = cryptoHandler.encrypt(data, offset, length);
+ offset = 0;
+ length = data.length;
+ }
+
+ // Step 3: prepend the 16-byte batch header
[mapId|attemptId|batchId|payloadLen]
+ byte[] body = new byte[BATCH_HEADER_SIZE + length];
+ Platform.putInt(body, Platform.BYTE_ARRAY_OFFSET, 0); // mapId
+ Platform.putInt(body, Platform.BYTE_ARRAY_OFFSET + 4, 0); // attemptId
+ Platform.putInt(body, Platform.BYTE_ARRAY_OFFSET + 8, 0); // batchId
+ Platform.putInt(body, Platform.BYTE_ARRAY_OFFSET + 12, length); // payload
length
+ System.arraycopy(data, offset, body, BATCH_HEADER_SIZE, length);
+ return Unpooled.wrappedBuffer(body);
+ }
+
+ /**
+ * Create a CelebornInputStream backed by a mock TransportClient that serves
the given batchBuf as
+ * a single chunk.
+ */
+ private CelebornInputStream createStream(
+ ByteBuf batchBuf,
+ boolean needDecompress,
+ Optional<CryptoHandler> cryptoHandler,
+ CelebornConf conf)
+ throws IOException, InterruptedException {
+ return createStreamWithClient(
+ batchBuf, needDecompress, cryptoHandler, conf,
mock(ShuffleClient.class));
+ }
+
+ /**
+ * Like {@link #createStream} but with a caller-supplied ShuffleClient mock,
so tests can verify
+ * interactions such as {@code readReducerPartitionEnd}.
+ */
+ private CelebornInputStream createStreamWithClient(
+ ByteBuf batchBuf,
+ boolean needDecompress,
+ Optional<CryptoHandler> cryptoHandler,
+ CelebornConf conf,
+ ShuffleClient shuffleClient)
+ throws IOException, InterruptedException {
+ TransportClient client = mock(TransportClient.class);
+ PbStreamHandler pbHandler =
+ PbStreamHandler.newBuilder().setStreamId(1L).setNumChunks(1).build();
+ // Encode the stream handler into an RPC response that CelebornInputStream
expects
+ ByteBuffer rpcResponse =
+ new TransportMessage(MessageType.STREAM_HANDLER,
pbHandler.toByteArray()).toByteBuffer();
+ when(client.sendRpcSync(any(ByteBuffer.class),
anyLong())).thenReturn(rpcResponse);
+ doNothing().when(client).sendRpc(any(ByteBuffer.class));
+ doAnswer(
+ invocation -> {
+ ChunkReceivedCallback cb = invocation.getArgument(3);
+ // Serve the pre-built batch buffer immediately as chunk 0;
duplicate() shares
+ // the underlying data without incrementing the ref count, so
the stream's
+ // single release correctly frees the buffer.
+ cb.onSuccess(0, new NettyManagedBuffer(batchBuf.duplicate()));
+ return null;
+ })
+ .when(client)
+ .fetchChunk(anyLong(), anyInt(), anyLong(),
any(ChunkReceivedCallback.class));
+
+ TransportClientFactory clientFactory = mock(TransportClientFactory.class);
+ when(clientFactory.createClient(anyString(), anyInt())).thenReturn(client);
+
+ // PRIMARY location pointing to a single HDD partition
+ PartitionLocation location =
+ new PartitionLocation(
+ 0, 0, "host1", 9001, 9002, 9003, 9004,
PartitionLocation.Mode.PRIMARY);
+ location.setStorageInfo(new StorageInfo(StorageInfo.Type.HDD, true,
"/mnt/disk1"));
+
+ ArrayList<PartitionLocation> locations = new ArrayList<>();
+ locations.add(location);
+ ArrayList<PbStreamHandler> handlers = new ArrayList<>();
+
handlers.add(PbStreamHandler.newBuilder().setStreamId(1L).setNumChunks(1).build());
+
+ return CelebornInputStream.create(
+ conf,
+ clientFactory,
+ SHUFFLE_KEY,
+ locations,
+ handlers,
+ new int[] {0},
+ new HashMap<>(),
+ new HashMap<>(),
+ 0,
+ 1L,
+ 0,
+ 100,
+ new ConcurrentHashMap<>(),
+ shuffleClient,
+ 1,
+ 1,
+ 0,
+ null,
+ new MetricsCallback() {
+ @Override
+ public void incBytesRead(long bytes) {}
+
+ @Override
+ public void incReadTime(long time) {}
+ },
+ needDecompress,
+ cryptoHandler);
+ }
+
+ private byte[] readAll(CelebornInputStream stream) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buf = new byte[4096];
+ int n;
+ while ((n = stream.read(buf)) != -1) {
+ baos.write(buf, 0, n);
+ }
+ return baos.toByteArray();
+ }
+
+ @Test
+ public void testEncryptDecryptRoundTrip() throws IOException,
InterruptedException {
+ byte[] plaintext = "hello, EAR round-trip without compression".getBytes();
+ CelebornConf conf = new CelebornConf();
+ XorCryptoHandler handler = new XorCryptoHandler((byte) 0x5A);
+
+ // Build an encrypted batch and read it back through CelebornInputStream
+ ByteBuf batchBuf = buildBatch(plaintext, false, handler, conf);
+ try (CelebornInputStream stream = createStream(batchBuf, false,
Optional.of(handler), conf)) {
+ assertArrayEquals(plaintext, readAll(stream));
+ }
+ }
+
+ @Test
+ public void testNoEncryptionRoundTrip() throws IOException,
InterruptedException {
+ byte[] plaintext = "unencrypted shuffle data sanity check".getBytes();
+ CelebornConf conf = new CelebornConf();
+
+ // Baseline: with no CryptoHandler the data flows through unchanged
+ ByteBuf batchBuf = buildBatch(plaintext, false, null, conf);
+ try (CelebornInputStream stream = createStream(batchBuf, false,
Optional.empty(), conf)) {
+ assertArrayEquals(plaintext, readAll(stream));
+ }
+ }
+
+ @Test
+ public void testCompressThenEncryptRoundTrip() throws IOException,
InterruptedException {
+ // Reproduce the compress-then-encrypt ordering used in ShuffleClientImpl.
+ byte[] plaintext = "shuffle data with compression and encryption enabled
for EAR".getBytes();
+ CelebornConf conf = new CelebornConf();
+ // Use LZ4 (default)
+ conf.set(CelebornConf.SHUFFLE_COMPRESSION_CODEC().key(), "lz4");
+ XorCryptoHandler handler = new XorCryptoHandler((byte) 0x3C);
+
+ // Writer: LZ4-compress then XOR-encrypt; Reader: decrypt then decompress
+ ByteBuf batchBuf = buildBatch(plaintext, true, handler, conf);
+ try (CelebornInputStream stream = createStream(batchBuf, true,
Optional.of(handler), conf)) {
+ assertArrayEquals(plaintext, readAll(stream));
+ }
+ }
+
+ @Test
+ public void testEncryptWithIntegrityCheckEnabled() throws IOException,
InterruptedException {
+ // Verify that EAR + shuffle integrity check work together: the checksum
must be computed
+ // over the *decrypted* plaintext, not the ciphertext. We capture the
crc32/bytes passed to
+ // readReducerPartitionEnd and assert they match an independently-computed
plaintext checksum.
+ byte[] plaintext = "integrity check should pass after
decryption".getBytes();
+ CelebornConf conf = new CelebornConf();
+ conf.set(CelebornConf.CLIENT_SHUFFLE_INTEGRITY_CHECK_ENABLED().key(),
"true");
+ XorCryptoHandler handler = new XorCryptoHandler((byte) 0x7F);
+
+ // Independently compute the expected checksum over the plaintext bytes.
+ CommitMetadata expected = new CommitMetadata();
+ expected.addDataWithOffsetAndLength(plaintext, 0, plaintext.length);
+
+ ByteBuf batchBuf = buildBatch(plaintext, false, handler, conf);
+
+ // createStream passes shuffleId=1, partitionId=0, startMapIndex=0,
endMapIndex=100
+ ShuffleClient shuffleClient = mock(ShuffleClient.class);
+ try (CelebornInputStream stream =
+ createStreamWithClient(batchBuf, false, Optional.of(handler), conf,
shuffleClient)) {
+ assertArrayEquals(plaintext, readAll(stream));
+ }
+
+ // Verify readReducerPartitionEnd was called with the checksum over
plaintext, not ciphertext.
+ ArgumentCaptor<Integer> crcCaptor = ArgumentCaptor.forClass(Integer.class);
+ ArgumentCaptor<Long> bytesCaptor = ArgumentCaptor.forClass(Long.class);
+ verify(shuffleClient)
+ .readReducerPartitionEnd(
+ anyInt(), anyInt(), anyInt(), anyInt(), crcCaptor.capture(),
bytesCaptor.capture());
+ assertEquals(
+ "checksum must be over plaintext", expected.getChecksum(), (int)
crcCaptor.getValue());
+ assertEquals(
+ "byte count must match plaintext length",
+ expected.getBytes(),
+ (long) bytesCaptor.getValue());
+ }
+
+ @Test
+ public void testLargePayloadEncryptDecrypt() throws IOException,
InterruptedException {
+ // 128 KB payload exercises buffer-boundary handling in fillBuffer()
+ byte[] plaintext = new byte[128 * 1024];
+ for (int i = 0; i < plaintext.length; i++) plaintext[i] = (byte) (i % 251);
+ CelebornConf conf = new CelebornConf();
+ XorCryptoHandler handler = new XorCryptoHandler((byte) 0xAB);
+
+ ByteBuf batchBuf = buildBatch(plaintext, false, handler, conf);
+ try (CelebornInputStream stream = createStream(batchBuf, false,
Optional.of(handler), conf)) {
+ assertArrayEquals(plaintext, readAll(stream));
+ }
+ }
+}
diff --git
a/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamPeerFailoverTest.java
b/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamPeerFailoverTest.java
index a7dd7db317..456486b9e8 100644
---
a/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamPeerFailoverTest.java
+++
b/client/src/test/java/org/apache/celeborn/client/read/CelebornInputStreamPeerFailoverTest.java
@@ -32,6 +32,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -39,6 +40,7 @@ import org.junit.Before;
import org.junit.Test;
import org.apache.celeborn.client.ShuffleClient;
+import org.apache.celeborn.client.security.CryptoHandler;
import org.apache.celeborn.common.CelebornConf;
import org.apache.celeborn.common.exception.CelebornIOException;
import org.apache.celeborn.common.network.client.TransportClient;
@@ -174,7 +176,8 @@ public class CelebornInputStreamPeerFailoverTest {
0,
null,
new TestMetricsCallback(),
- false);
+ false,
+ Optional.<CryptoHandler>empty());
}
private void createInputStream(String primaryHost, String replicaHost)
throws IOException {
@@ -209,7 +212,8 @@ public class CelebornInputStreamPeerFailoverTest {
0,
null,
new TestMetricsCallback(),
- false);
+ false,
+ Optional.<CryptoHandler>empty());
}
/**