This is an automated email from the ASF dual-hosted git repository.
zaynt4606 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new b34cd83a58 [CELEBORN-2470] Add backoff with jitter to push retries and
log hard split once per file
b34cd83a58 is described below
commit b34cd83a58d0dc0d7f579ea5476b568aeb56d5fb
Author: Venkata Krishnan Sowrirajan <[email protected]>
AuthorDate: Tue Oct 13 11:34:28 2026 +0800
[CELEBORN-2470] Add backoff with jitter to push retries and log hard split
once per file
JIRA: [CELEBORN-2470](https://issues.apache.org/jira/browse/CELEBORN-2470)
## Summary
Push retries currently run immediately and can synchronize across many
tasks, increasing connection and DNS load when a worker is unhealthy. Hard
splits can also emit the same INFO log repeatedly while pushes continue
targeting the old file.
This PR:
- adds exponential backoff with jitter to push retries
- logs the hard-split cause once per `PartitionDataWriter`, for both disk
and memory files
- moves the repeated merged-push hard-split message to DEBUG
## Configuration
| Config | Default |
|---|---|
| `celeborn.client.push.retry.backoff.initial` | `500ms` |
| `celeborn.client.push.retry.backoff.max` | `10s` |
| `celeborn.client.push.retry.backoff.jitter` | `0.5` |
Setting `celeborn.client.push.retry.backoff.initial` to `0` disables
backoff. `celeborn.client.push.retry.backoff.max` caps the final delay,
including jitter, so with the defaults a retry at the cap waits between 5s and
10s.
## Tests
- added coverage for exponential growth, maximum delay, jitter bounds
(including at the cap), disabled backoff, and large retry counts
- added callback-level coverage for scheduled single and merged
`HARD_SPLIT` retries and revive completion during backoff
- added concurrent coverage verifying the hard-split log latch triggers
once per writer
- added coverage that repeated memory-shuffle `HARD_SPLIT` checks log only
once
Closes #3856 from venkata91/celeborn-push-retry-backoff.
Authored-by: Venkata Krishnan Sowrirajan <[email protected]>
Signed-off-by: zhengtao <[email protected]>
AI-Contributed/Feature: 0/143
AI-Contributed/UT: 0/223
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../apache/celeborn/client/ShuffleClientImpl.java | 80 +++++++--
.../celeborn/client/ShuffleClientSuiteJ.java | 181 ++++++++++++++++++++-
.../org/apache/celeborn/common/CelebornConf.scala | 36 ++++
docs/configuration/client.md | 3 +
.../deploy/worker/storage/PartitionDataWriter.java | 9 +
.../service/deploy/worker/PushDataHandler.scala | 15 +-
.../local/DiskMapPartitionDataWriterSuiteJ.java | 33 ++++
.../deploy/worker/PushDataHandlerSuite.scala | 9 +-
8 files changed, 345 insertions(+), 21 deletions(-)
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 5e84f84ee4..f321924737 100644
--- a/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
+++ b/client/src/main/java/org/apache/celeborn/client/ShuffleClientImpl.java
@@ -22,7 +22,8 @@ import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import scala.Tuple2;
@@ -131,7 +132,10 @@ public class ShuffleClientImpl extends ShuffleClient {
private boolean pushReplicateEnabled;
private boolean fetchExcludeWorkerOnFailureEnabled;
- private final ExecutorService pushDataRetryPool;
+ private final ScheduledExecutorService pushDataRetryPool;
+ private final long pushRetryBackoffInitialMs;
+ private final long pushRetryBackoffMaxMs;
+ private final double pushRetryBackoffJitter;
private final Map<Integer, Set<Integer>> splitting =
JavaUtils.newConcurrentHashMap();
@@ -235,7 +239,11 @@ public class ShuffleClientImpl extends ShuffleClient {
initDataClientFactoryIfNeeded();
int pushDataRetryThreads = conf.clientPushRetryThreads();
pushDataRetryPool =
- ThreadUtils.newDaemonCachedThreadPool("celeborn-retry-sender",
pushDataRetryThreads, 60);
+ ThreadUtils.newDaemonThreadPoolScheduledExecutor(
+ "celeborn-retry-sender", pushDataRetryThreads);
+ pushRetryBackoffInitialMs = conf.clientPushRetryBackoffInitial();
+ pushRetryBackoffMaxMs = conf.clientPushRetryBackoffMax();
+ pushRetryBackoffJitter = conf.clientPushRetryBackoffJitter();
reviveManager = new ReviveManager(this, conf);
@@ -304,6 +312,40 @@ public class ShuffleClientImpl extends ShuffleClient {
}
}
+ /**
+ * Delay before the {@code retryTimes}-th push retry: an exponentially
growing base capped at
+ * {@code celeborn.client.push.retry.backoff.max}, spread by jitter so that
map tasks blocked on
+ * the same partition do not retry in lockstep. Returns 0 when backoff is
disabled.
+ */
+ @VisibleForTesting
+ long pushRetryBackoffMs(int retryTimes) {
+ if (pushRetryBackoffInitialMs <= 0 || retryTimes <= 0) {
+ return 0;
+ }
+ // Shift-loop rather than a power, because the hard split retry counter is
not bounded.
+ long delay = pushRetryBackoffInitialMs;
+ for (int i = 1; i < retryTimes && delay < pushRetryBackoffMaxMs; i++) {
+ delay = delay > pushRetryBackoffMaxMs / 2 ? pushRetryBackoffMaxMs :
delay << 1;
+ }
+ delay = Math.min(delay, pushRetryBackoffMaxMs);
+ long jitter = (long) (delay * pushRetryBackoffJitter);
+ if (jitter <= 0) {
+ return delay;
+ }
+ long minDelay = delay - jitter;
+ long maxDelay = delay > pushRetryBackoffMaxMs - jitter ?
pushRetryBackoffMaxMs : delay + jitter;
+ long jitterRange = maxDelay - minDelay;
+ if (jitterRange == Long.MAX_VALUE) {
+ return ThreadLocalRandom.current().nextLong(Long.MAX_VALUE);
+ }
+ return minDelay + ThreadLocalRandom.current().nextLong(jitterRange + 1);
+ }
+
+ @VisibleForTesting
+ void scheduleRetry(Runnable retry, long delayMs) {
+ pushDataRetryPool.schedule(retry, delayMs, TimeUnit.MILLISECONDS);
+ }
+
private void submitRetryPushData(
int shuffleId,
byte[] body,
@@ -532,7 +574,7 @@ public class ShuffleClientImpl extends ShuffleClient {
} else {
ReviveRequest[] requests =
addAndGetReviveRequests(shuffleId, mapId, attemptId,
reviveFailedBatchesMap, cause);
- pushDataRetryPool.submit(
+ scheduleRetry(
() ->
submitRetryPushMergedData(
pushState,
@@ -545,7 +587,8 @@ public class ShuffleClientImpl extends ShuffleClient {
requests,
remainReviveTimes - 1,
System.currentTimeMillis()
- +
conf.clientRpcRequestPartitionLocationAskTimeout().duration().toMillis()));
+ +
conf.clientRpcRequestPartitionLocationAskTimeout().duration().toMillis()),
+ pushRetryBackoffMs(maxReviveTimes - (remainReviveTimes - 1)));
}
}
@@ -1133,6 +1176,9 @@ public class ShuffleClientImpl extends ShuffleClient {
RpcResponseCallback wrappedCallback =
new PushDataRpcResponseCallback() {
int remainReviveTimes = maxReviveTimes;
+ // HARD_SPLIT retries deliberately do not consume
remainReviveTimes, so they need their
+ // own counter to grow the backoff.
+ int hardSplitRetryTimes = 0;
PartitionLocation latest = loc;
@Override
@@ -1199,7 +1245,7 @@ public class ShuffleClientImpl extends ShuffleClient {
+ conf.clientRpcRequestPartitionLocationAskTimeout()
.duration()
.toMillis();
- pushDataRetryPool.submit(
+ scheduleRetry(
() ->
submitRetryPushData(
shuffleId,
@@ -1209,7 +1255,8 @@ public class ShuffleClientImpl extends ShuffleClient {
pushState,
reviveRequest,
remainReviveTimes,
- dueTime));
+ dueTime),
+ pushRetryBackoffMs(++hardSplitRetryTimes));
} else if (reason ==
StatusCode.PUSH_DATA_SUCCESS_PRIMARY_CONGESTED.getValue()) {
logger.debug(
"Push data to {} primary congestion required for shuffle
{} map {} attempt {} partition {} batch {}.",
@@ -1291,7 +1338,7 @@ public class ShuffleClientImpl extends ShuffleClient {
long dueTime =
System.currentTimeMillis()
+
conf.clientRpcRequestPartitionLocationAskTimeout().duration().toMillis();
- pushDataRetryPool.submit(
+ scheduleRetry(
() ->
submitRetryPushData(
shuffleId,
@@ -1301,7 +1348,8 @@ public class ShuffleClientImpl extends ShuffleClient {
pushState,
reviveRequest,
remainReviveTimes,
- dueTime));
+ dueTime),
+ pushRetryBackoffMs(maxReviveTimes - remainReviveTimes));
} else {
pushState.removeBatch(nextBatchId, latest.hostAndPushPort());
logger.info(
@@ -1643,7 +1691,11 @@ public class ShuffleClientImpl extends ShuffleClient {
ReviveRequest[] requests =
addAndGetReviveRequests(
shuffleId, mapId, attemptId, batchesNeedResubmit,
StatusCode.HARD_SPLIT);
- pushDataRetryPool.submit(
+ // remainReviveTimes is a parameter here and is not
decremented on the HARD_SPLIT
+ // path, so there is no per-batch attempt counter to grow
from. Apply the initial
+ // delay with jitter, which is what decorrelates the herd;
growing this one too
+ // needs the counter threaded through doPushMergedData and is
left as follow-up.
+ scheduleRetry(
() ->
submitRetryPushMergedData(
pushState,
@@ -1658,7 +1710,8 @@ public class ShuffleClientImpl extends ShuffleClient {
System.currentTimeMillis()
+
conf.clientRpcRequestPartitionLocationAskTimeout()
.duration()
- .toMillis()));
+ .toMillis()),
+ pushRetryBackoffMs(1));
}
} else if (reason ==
StatusCode.PUSH_DATA_SUCCESS_PRIMARY_CONGESTED.getValue()) {
logger.debug(
@@ -1731,7 +1784,7 @@ public class ShuffleClientImpl extends ShuffleClient {
if (!mapperEnded(shuffleId, mapId)) {
ReviveRequest[] requests =
addAndGetReviveRequests(shuffleId, mapId, attemptId,
batches, cause);
- pushDataRetryPool.submit(
+ scheduleRetry(
() ->
submitRetryPushMergedData(
pushState,
@@ -1746,7 +1799,8 @@ public class ShuffleClientImpl extends ShuffleClient {
System.currentTimeMillis()
+
conf.clientRpcRequestPartitionLocationAskTimeout()
.duration()
- .toMillis()));
+ .toMillis()),
+ pushRetryBackoffMs(maxReviveTimes - (remainReviveTimes -
1)));
} else {
pushState.removeBatch(groupedBatchId, hostPort);
logger.info(
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 46d9a4f5b7..82da671a31 100644
--- a/client/src/test/java/org/apache/celeborn/client/ShuffleClientSuiteJ.java
+++ b/client/src/test/java/org/apache/celeborn/client/ShuffleClientSuiteJ.java
@@ -20,10 +20,16 @@ package org.apache.celeborn.client;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
@@ -33,6 +39,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import scala.reflect.ClassTag;
@@ -52,6 +59,7 @@ 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.RpcResponseCallback;
import org.apache.celeborn.common.network.client.TransportClient;
import org.apache.celeborn.common.network.client.TransportClientFactory;
import org.apache.celeborn.common.network.protocol.SerdeVersion;
@@ -59,6 +67,7 @@ import org.apache.celeborn.common.protocol.CompressionCodec;
import org.apache.celeborn.common.protocol.PartitionLocation;
import org.apache.celeborn.common.protocol.PbReadReducerPartitionEnd;
import org.apache.celeborn.common.protocol.PbReadReducerPartitionEndResponse;
+import org.apache.celeborn.common.protocol.ReviveRequest;
import
org.apache.celeborn.common.protocol.message.ControlMessages.GetReducerFileGroupResponse$;
import
org.apache.celeborn.common.protocol.message.ControlMessages.RegisterShuffleResponse$;
import org.apache.celeborn.common.protocol.message.StatusCode;
@@ -112,6 +121,168 @@ public class ShuffleClientSuiteJ {
private static final byte[] TEST_BUF1 = "hello
world".getBytes(StandardCharsets.UTF_8);
private final int BATCH_HEADER_SIZE = 4 * 4;
+ @Test
+ public void testPushRetryBackoff() {
+ CelebornConf conf = new CelebornConf();
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_INITIAL().key(), "100ms");
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_MAX().key(), "400ms");
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_JITTER().key(), "0");
+ ShuffleClientImpl client =
+ new ShuffleClientImpl(TEST_APPLICATION_ID, conf, new
UserIdentifier("mock", "mock"));
+ try {
+ assertEquals(0, client.pushRetryBackoffMs(0));
+ assertEquals(100, client.pushRetryBackoffMs(1));
+ assertEquals(200, client.pushRetryBackoffMs(2));
+ assertEquals(400, client.pushRetryBackoffMs(3));
+ assertEquals(400, client.pushRetryBackoffMs(Integer.MAX_VALUE));
+ } finally {
+ client.shutdown();
+ }
+
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_JITTER().key(), "0.5");
+ client = new ShuffleClientImpl(TEST_APPLICATION_ID, conf, new
UserIdentifier("mock", "mock"));
+ try {
+ Set<Long> delays = new java.util.HashSet<>();
+ for (int i = 0; i < 100; i++) {
+ long delay = client.pushRetryBackoffMs(1);
+ assertTrue(delay >= 50 && delay <= 150);
+ delays.add(delay);
+ }
+ assertTrue("jitter should produce different retry delays", delays.size()
> 1);
+
+ delays.clear();
+ for (int i = 0; i < 100; i++) {
+ long delay = client.pushRetryBackoffMs(3);
+ assertTrue(delay >= 200 && delay <= 400);
+ delays.add(delay);
+ }
+ assertTrue("jitter at the cap should produce different retry delays",
delays.size() > 1);
+ } finally {
+ client.shutdown();
+ }
+
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_INITIAL().key(), "0");
+ client = new ShuffleClientImpl(TEST_APPLICATION_ID, conf, new
UserIdentifier("mock", "mock"));
+ try {
+ assertEquals(0, client.pushRetryBackoffMs(1));
+ } finally {
+ client.shutdown();
+ }
+ }
+
+ @Test
+ public void testHardSplitPushSchedulesRetryAndClearsBatchAfterRevive()
throws Exception {
+ CelebornConf conf = retryIntegrationTestConf();
+ setupEnv(conf, CompressionCodec.NONE, StatusCode.SUCCESS, false);
+ shuffleClient = spy(shuffleClient);
+
+ AtomicReference<Runnable> scheduledRetry = new AtomicReference<>();
+ AtomicLong scheduledDelay = new AtomicLong(-1);
+ doAnswer(
+ invocation -> {
+ scheduledRetry.set(invocation.getArgument(0));
+ scheduledDelay.set(invocation.getArgument(1));
+ return null;
+ })
+ .when(shuffleClient)
+ .scheduleRetry(any(Runnable.class), anyLong());
+
+ shuffleClient.pushData(
+ TEST_SHUFFLE_ID,
+ TEST_MAP_ID,
+ TEST_ATTEMPT_ID,
+ TEST_REDUCRE_ID,
+ TEST_BUF1,
+ 0,
+ TEST_BUF1.length,
+ 1,
+ 1);
+
+ ArgumentCaptor<RpcResponseCallback> callbackCaptor =
+ ArgumentCaptor.forClass(RpcResponseCallback.class);
+ verify(client).pushData(any(), anyLong(), callbackCaptor.capture());
+ callbackCaptor
+ .getValue()
+ .onSuccess(ByteBuffer.wrap(new byte[]
{StatusCode.HARD_SPLIT.getValue()}));
+
+ assertEquals(100, scheduledDelay.get());
+ ReviveRequest reviveRequest = takeReviveRequest(shuffleClient);
+ reviveRequest.reviveStatus = StatusCode.SUCCESS.getValue();
+ scheduledRetry.get().run();
+
+ verify(client, times(2)).pushData(any(), anyLong(),
callbackCaptor.capture());
+ callbackCaptor
+ .getAllValues()
+ .get(callbackCaptor.getAllValues().size() - 1)
+ .onSuccess(ByteBuffer.allocate(0));
+
+ PushState pushState =
+ shuffleClient.getPushState(Utils.makeMapKey(TEST_SHUFFLE_ID,
TEST_MAP_ID, TEST_ATTEMPT_ID));
+ assertFalse(pushState.limitZeroInFlight());
+ }
+
+ @Test
+ public void testHardSplitMergedPushSchedulesRetryAfterRevive() throws
Exception {
+ CelebornConf conf = retryIntegrationTestConf();
+ setupEnv(conf, CompressionCodec.NONE, StatusCode.SUCCESS, false);
+ shuffleClient = spy(shuffleClient);
+
+ AtomicReference<Runnable> scheduledRetry = new AtomicReference<>();
+ AtomicLong scheduledDelay = new AtomicLong(-1);
+ doAnswer(
+ invocation -> {
+ scheduledRetry.set(invocation.getArgument(0));
+ scheduledDelay.set(invocation.getArgument(1));
+ return null;
+ })
+ .when(shuffleClient)
+ .scheduleRetry(any(Runnable.class), anyLong());
+
+ shuffleClient.mergeData(
+ TEST_SHUFFLE_ID,
+ TEST_MAP_ID,
+ TEST_ATTEMPT_ID,
+ TEST_REDUCRE_ID,
+ TEST_BUF1,
+ 0,
+ TEST_BUF1.length,
+ 1,
+ 1);
+ shuffleClient.pushMergedData(TEST_SHUFFLE_ID, TEST_MAP_ID,
TEST_ATTEMPT_ID);
+
+ ArgumentCaptor<RpcResponseCallback> callbackCaptor =
+ ArgumentCaptor.forClass(RpcResponseCallback.class);
+ verify(client).pushMergedData(any(), anyLong(), callbackCaptor.capture());
+ callbackCaptor
+ .getValue()
+ .onSuccess(ByteBuffer.wrap(new byte[]
{StatusCode.HARD_SPLIT.getValue()}));
+
+ assertEquals(100, scheduledDelay.get());
+ ReviveRequest reviveRequest = takeReviveRequest(shuffleClient);
+ reviveRequest.reviveStatus = StatusCode.SUCCESS.getValue();
+ scheduledRetry.get().run();
+
+ verify(client, times(2)).pushMergedData(any(), anyLong(), any());
+ }
+
+ private CelebornConf retryIntegrationTestConf() {
+ CelebornConf conf = new CelebornConf();
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_INITIAL().key(), "100ms");
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_MAX().key(), "400ms");
+ conf.set(CelebornConf.CLIENT_PUSH_RETRY_BACKOFF_JITTER().key(), "0");
+ conf.set(CelebornConf.CLIENT_PUSH_REVIVE_INTERVAL().key(), "1h");
+ return conf;
+ }
+
+ private ReviveRequest takeReviveRequest(ShuffleClientImpl client) throws
Exception {
+ Field reviveManagerField =
ShuffleClientImpl.class.getDeclaredField("reviveManager");
+ reviveManagerField.setAccessible(true);
+ ReviveManager reviveManager = (ReviveManager)
reviveManagerField.get(client);
+ ReviveRequest request = reviveManager.requestQueue.poll();
+ assertNotNull(request);
+ return request;
+ }
+
@Test
public void testPushData() throws IOException, InterruptedException {
for (CompressionCodec codec : CompressionCodec.values()) {
@@ -256,7 +427,15 @@ public class ShuffleClientSuiteJ {
private CelebornConf setupEnv(
CompressionCodec codec, StatusCode statusCode, boolean
interruptWhenPushData)
throws IOException, InterruptedException {
- CelebornConf conf = new CelebornConf();
+ return setupEnv(new CelebornConf(), codec, statusCode,
interruptWhenPushData);
+ }
+
+ private CelebornConf setupEnv(
+ CelebornConf conf,
+ CompressionCodec codec,
+ StatusCode statusCode,
+ boolean interruptWhenPushData)
+ throws IOException, InterruptedException {
conf.set(CelebornConf.SHUFFLE_COMPRESSION_CODEC().key(), codec.name());
conf.set(CelebornConf.CLIENT_PUSH_RETRY_THREADS().key(), "1");
conf.set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE().key(), "1K");
diff --git
a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
index 3391e3d169..22d1a066db 100644
--- a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
@@ -1097,6 +1097,9 @@ class CelebornConf(loadDefaults: Boolean) extends
Cloneable with Logging with Se
def clientPushSortRandomizePartitionIdEnabled: Boolean =
get(CLIENT_PUSH_SORT_RANDOMIZE_PARTITION_ENABLED)
def clientPushRetryThreads: Int = get(CLIENT_PUSH_RETRY_THREADS)
+ def clientPushRetryBackoffInitial: Long =
get(CLIENT_PUSH_RETRY_BACKOFF_INITIAL)
+ def clientPushRetryBackoffMax: Long = get(CLIENT_PUSH_RETRY_BACKOFF_MAX)
+ def clientPushRetryBackoffJitter: Double =
get(CLIENT_PUSH_RETRY_BACKOFF_JITTER)
def clientPushStageEndTimeout: Long = get(CLIENT_PUSH_STAGE_END_TIMEOUT)
def clientPushUnsafeRowFastWrite: Boolean =
get(CLIENT_PUSH_UNSAFEROW_FASTWRITE_ENABLED)
def clientRpcCacheExpireTime: Long = get(CLIENT_RPC_CACHE_EXPIRE_TIME)
@@ -5195,6 +5198,39 @@ object CelebornConf extends Logging {
.intConf
.createWithDefault(8)
+ val CLIENT_PUSH_RETRY_BACKOFF_MAX: ConfigEntry[Long] =
+ buildConf("celeborn.client.push.retry.backoff.max")
+ .categories("client")
+ .doc("Upper bound of the push data retry delay, including jitter.")
+ .version("0.7.1")
+ .timeConf(TimeUnit.MILLISECONDS)
+ .checkValue(_ > 0, "Value must be positive!")
+ .createWithDefaultString("10s")
+
+ val CLIENT_PUSH_RETRY_BACKOFF_INITIAL: ConfigEntry[Long] =
+ buildConf("celeborn.client.push.retry.backoff.initial")
+ .categories("client")
+ .doc("Initial delay before re-sending a push data request that was
rejected with " +
+ s"HARD_SPLIT or failed. The delay grows exponentially up to " +
+ s"`${CLIENT_PUSH_RETRY_BACKOFF_MAX.key}` so that map tasks blocked on
the same " +
+ "partition do not retry in lockstep. Set to 0 to disable backoff.")
+ .version("0.7.1")
+ .timeConf(TimeUnit.MILLISECONDS)
+ .checkValue(_ >= 0, "Value must be zero or positive!")
+ .createWithDefaultString("500ms")
+
+ val CLIENT_PUSH_RETRY_BACKOFF_JITTER: ConfigEntry[Double] =
+ buildConf("celeborn.client.push.retry.backoff.jitter")
+ .categories("client")
+ .doc("Fraction of the push data retry delay applied as random jitter. A
value of 0.5 " +
+ "spreads retries uniformly over [0.5, 1.5] times the computed delay,
capped at " +
+ s"`${CLIENT_PUSH_RETRY_BACKOFF_MAX.key}`. For example, once the
computed delay reaches " +
+ "the default maximum of 10s, retries are spread over [5s, 10s].")
+ .version("0.7.1")
+ .doubleConf
+ .checkValue(v => v >= 0.0 && v <= 1.0, "Value must be in [0.0, 1.0]!")
+ .createWithDefault(0.5)
+
val CLIENT_PUSH_TAKE_TASK_WAIT_INTERVAL: ConfigEntry[Long] =
buildConf("celeborn.client.push.takeTaskWaitInterval")
.categories("client")
diff --git a/docs/configuration/client.md b/docs/configuration/client.md
index dbb89561e4..c5957be652 100644
--- a/docs/configuration/client.md
+++ b/docs/configuration/client.md
@@ -66,6 +66,9 @@ license: |
| celeborn.client.push.maxReqsInFlight.total | 256 | false | Amount of total
Netty in-flight requests. The maximum memory is
`celeborn.client.push.maxReqsInFlight.total` *
`celeborn.client.push.buffer.max.size` * compression ratio(1 in worst case):
64KiB * 256 = 16MiB | 0.3.0 | celeborn.push.maxReqsInFlight |
| celeborn.client.push.queue.capacity | 512 | false | Push buffer queue size
for a task. The maximum memory is `celeborn.client.push.buffer.max.size` *
`celeborn.client.push.queue.capacity`, default: 64KiB * 512 = 32MiB | 0.3.0 |
celeborn.push.queue.capacity |
| celeborn.client.push.replicate.enabled | false | false | When true, Celeborn
worker will replicate shuffle data to another Celeborn worker asynchronously to
ensure the pushed shuffle data won't be lost after the node failure. It's
recommended to set `false` when `HDFS` is enabled in
`celeborn.storage.availableTypes`. | 0.3.0 | celeborn.push.replicate.enabled |
+| celeborn.client.push.retry.backoff.initial | 500ms | false | Initial delay
before re-sending a push data request that was rejected with HARD_SPLIT or
failed. The delay grows exponentially up to
`celeborn.client.push.retry.backoff.max` so that map tasks blocked on the same
partition do not retry in lockstep. Set to 0 to disable backoff. | 0.7.1 | |
+| celeborn.client.push.retry.backoff.jitter | 0.5 | false | Fraction of the
push data retry delay applied as random jitter. A value of 0.5 spreads retries
uniformly over [0.5, 1.5] times the computed delay, capped at
`celeborn.client.push.retry.backoff.max`. For example, once the computed delay
reaches the default maximum of 10s, retries are spread over [5s, 10s]. | 0.7.1
| |
+| celeborn.client.push.retry.backoff.max | 10s | false | Upper bound of the
push data retry delay, including jitter. | 0.7.1 | |
| celeborn.client.push.retry.threads | 8 | false | Thread number to process
shuffle re-send push data requests. | 0.3.0 | celeborn.push.retry.threads |
| celeborn.client.push.revive.batchSize | 2048 | false | Max number of
partitions in one Revive request. | 0.3.0 | |
| celeborn.client.push.revive.interval | 100ms | false | Interval for client
to trigger Revive to LifecycleManager. The number of partitions in one Revive
request is `celeborn.client.push.revive.batchSize`. | 0.3.0 | |
diff --git
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionDataWriter.java
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionDataWriter.java
index 720423829b..233b7a0efc 100644
---
a/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionDataWriter.java
+++
b/worker/src/main/java/org/apache/celeborn/service/deploy/worker/storage/PartitionDataWriter.java
@@ -19,6 +19,7 @@ package org.apache.celeborn.service.deploy.worker.storage;
import java.io.IOException;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import scala.Option;
@@ -51,6 +52,9 @@ public class PartitionDataWriter implements DeviceObserver {
private final PartitionSplitMode splitMode;
private final long memoryFileStorageMaxFileSize;
private final AtomicInteger numPendingWrites = new AtomicInteger(0);
+ // Latches on the first hard split of this file so the hard split cause is
logged once per
+ // split file instead of once per push. Dies with the writer, so it needs no
cleanup.
+ private final AtomicBoolean hardSplitLogged = new AtomicBoolean(false);
private final PartitionDataWriterContext writerContext;
protected final AbstractSource source; // metrics
private final String writerString;
@@ -263,6 +267,11 @@ public class PartitionDataWriter implements DeviceObserver
{
return splitMode;
}
+ /** Returns true only for the first hard split of this file, false for every
subsequent one. */
+ public boolean shouldLogHardSplit() {
+ return hardSplitLogged.compareAndSet(false, true);
+ }
+
@Override
public synchronized void notifyError(String mountPoint, DiskStatus
diskStatus) {
// A committed file is already flushed and visible to fetch requests,
diff --git
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/PushDataHandler.scala
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/PushDataHandler.scala
index f6c32e77f3..7c64593d25 100644
---
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/PushDataHandler.scala
+++
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/PushDataHandler.scala
@@ -589,8 +589,9 @@ class PushDataHandler(val workerSource: WorkerSource)
extends BaseMessageHandler
} else {
val splitStatus = checkDiskFullAndSplit(fileWriter, isPrimary)
if (splitStatus == StatusCode.HARD_SPLIT) {
- logWarning(
- s"return hard split for disk full with shuffle $shuffleKey map
$mapId attempt $attemptId")
+ logDebug(
+ s"return hard split for shuffle $shuffleKey map $mapId attempt
$attemptId; " +
+ s"see the CheckDiskFullAndSplit log for the cause")
workerSource.incCounter(WorkerSource.WRITE_DATA_HARD_SPLIT_COUNT)
pushMergedDataCallback.addSplitPartition(fileWriterIndex,
StatusCode.HARD_SPLIT)
} else if (splitStatus == StatusCode.SOFT_SPLIT) {
@@ -1441,8 +1442,10 @@ class PushDataHandler(val workerSource: WorkerSource)
extends BaseMessageHandler
fileWriter: PartitionDataWriter,
isPrimary: Boolean): StatusCode = {
if (fileWriter.needHardSplitForMemoryShuffleStorage()) {
- logInfo(
- s"Do hardSplit for memory shuffle file
fileLength:${fileWriter.getMemoryFileInfo.getFileLength}")
+ if (fileWriter.shouldLogHardSplit()) {
+ logInfo(
+ s"Do hardSplit for memory shuffle file
fileLength:${fileWriter.getMemoryFileInfo.getFileLength}")
+ }
return StatusCode.HARD_SPLIT
}
val diskFull = checkDiskFull(fileWriter)
@@ -1470,7 +1473,9 @@ class PushDataHandler(val workerSource: WorkerSource)
extends BaseMessageHandler
} else {
StatusCode.NO_SPLIT
}
- if (splitStatus == StatusCode.HARD_SPLIT) {
+ // Log the cause once per split file. A split file keeps failing the
threshold check until a
+ // new epoch propagates, so without the latch every concurrent push to
it logs again.
+ if (splitStatus == StatusCode.HARD_SPLIT &&
fileWriter.shouldLogHardSplit()) {
logInfo(
s"""
|CheckDiskFullAndSplit hardSplit
diff --git
a/worker/src/test/java/org/apache/celeborn/service/deploy/worker/storage/local/DiskMapPartitionDataWriterSuiteJ.java
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/storage/local/DiskMapPartitionDataWriterSuiteJ.java
index 5f750f8954..94b7eec5e9 100644
---
a/worker/src/test/java/org/apache/celeborn/service/deploy/worker/storage/local/DiskMapPartitionDataWriterSuiteJ.java
+++
b/worker/src/test/java/org/apache/celeborn/service/deploy/worker/storage/local/DiskMapPartitionDataWriterSuiteJ.java
@@ -26,6 +26,7 @@ import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.IntStream;
import scala.Function0;
import scala.collection.mutable.ListBuffer;
@@ -119,6 +120,38 @@ public class DiskMapPartitionDataWriterSuiteJ {
}
}
+ @Test
+ public void testShouldLogHardSplitOnlyOnce() throws IOException {
+ PartitionDataWriterContext context =
+ new PartitionDataWriterContext(
+ SPLIT_THRESHOLD,
+ splitMode,
+ false,
+ new PartitionLocation(
+ 1, 0, "host", 1111, 1112, 1113, 1114,
PartitionLocation.Mode.PRIMARY, null),
+ "app1-1",
+ 1,
+ userIdentifier,
+ PartitionType.MAP,
+ false,
+ false);
+ PartitionDataWriter fileWriter =
+ new PartitionDataWriter(
+ PartitionDataWriterSuiteUtils.prepareDiskFileTestEnvironment(
+ tempDir, userIdentifier, localFlusher, false, CONF,
storagePolicy, context),
+ source,
+ CONF,
+ DeviceMonitor$.MODULE$.EmptyMonitor(),
+ context);
+ try {
+ assertEquals(
+ 1,
+ IntStream.range(0, 100).parallel().filter(i ->
fileWriter.shouldLogHardSplit()).count());
+ } finally {
+ fileWriter.close();
+ }
+ }
+
@Test
public void testMultiThreadWrite() throws IOException {
PartitionLocation partitionLocation =
Mockito.mock(PartitionLocation.class);
diff --git
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/PushDataHandlerSuite.scala
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/PushDataHandlerSuite.scala
index 40639a2488..2e010de67c 100644
---
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/PushDataHandlerSuite.scala
+++
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/PushDataHandlerSuite.scala
@@ -148,10 +148,15 @@ class PushDataHandlerSuite extends AnyFunSuite {
test("returns HARD_SPLIT when memory shuffle storage needs hard split") {
val handler = buildHandler(partitionSplitEnabled = true)
val fileWriter = buildFileWriter(fileLength = 0L, needMemoryHardSplit =
true)
+ when(fileWriter.shouldLogHardSplit()).thenReturn(true, false)
- val result = handler.checkDiskFullAndSplit(fileWriter, isPrimary = true)
+ val firstResult = handler.checkDiskFullAndSplit(fileWriter, isPrimary =
true)
+ val secondResult = handler.checkDiskFullAndSplit(fileWriter, isPrimary =
true)
- assert(result === StatusCode.HARD_SPLIT)
+ assert(firstResult === StatusCode.HARD_SPLIT)
+ assert(secondResult === StatusCode.HARD_SPLIT)
+ verify(fileWriter, times(2)).shouldLogHardSplit()
+ verify(fileWriter, times(1)).getMemoryFileInfo
}
test("returns NO_SPLIT when workerPartitionSplitEnabled is false") {