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 f0c8139621 [CELEBORN-2371] Bound Spark batch-open client creation
retries and stop them on interruption
f0c8139621 is described below
commit f0c8139621f3da73ccf30566fb344ffb2ff47cf9
Author: Chao Sun <[email protected]>
AuthorDate: Fri Jul 24 10:14:00 2026 +0800
[CELEBORN-2371] Bound Spark batch-open client creation retries and stop
them on interruption
## Why are the changes needed?
[CELEBORN-2371](https://issues.apache.org/jira/browse/CELEBORN-2371)
follows up on the parallel Spark batch-open client creation added by
[#3692](https://github.com/apache/celeborn/pull/3692).
Batch-open locations are grouped by `host:fetchPort`, but each worker task
previously walked multiple `PartitionLocation` entries. Since every
`createClient` invocation already receives `TransportClientFactory`'s full
retry budget, that outer loop could multiply connection latency without
targeting a different endpoint.
Cancellation also needs to terminate consistently. A direct or wrapped
`InterruptedException` must stop reader setup and fetch retry flow before it is
recorded as a worker failure, added to shared exclusion state, retried against
a peer, or reported as a shuffle fetch failure. The unlimited-timeout transport
branch also needed explicit cleanup on cancelled or failed connection futures.
## What changes were proposed in this PR?
### Keep one retry budget per worker endpoint
Each grouped `host:fetchPort` now uses one representative location for
client creation. `TransportClientFactory` remains the owner of connection
retries, controlled by the existing `celeborn.data.io.maxRetries`; there is no
second outer retry setting or multiplicative retry budget.
### Propagate cancellation through reader paths
`CelebornShuffleReader` now uses a shared interrupt-aware client-creation
helper in both sequential and parallel batch-open paths. It checks a
pre-existing interrupt flag, restores the flag when `createClient` throws
`InterruptedException`, and exits without invoking the ordinary failure
callback.
`CelebornInputStream` now detects interruption throughout reader creation,
failed-stream cleanup, reconnect, and buffer-fill paths. It exits before
exclusion, retry, peer failover, or shuffle-fetch-failure reporting. If reader
cleanup itself fails while propagating cancellation, the cleanup failure is
retained as a suppressed exception without masking the original interruption.
### Preserve interruption and cleanup in transport bootstrap
`TransportClientFactory` uses Guava's `Throwables.getCausalChain()` to
detect wrapped `InterruptedException`, restores the interrupt flag, and stops
retrying immediately. TCP-connect and TLS-handshake waits close the in-progress
channel before propagating interruption. The unlimited-timeout connection path
now also closes cancelled and failed channel futures explicitly.
## How was this PR tested?
Formatting was applied with:
```text
./build/mvn --no-transfer-progress -DskipTests spotless:apply
```
The focused transport tests passed (11 tests):
```text
./build/mvn --no-transfer-progress -pl common -am \
-Dtest=TransportClientFactorySuiteJ,TransportClientFactoryInterruptSuiteJ
\
-DwildcardSuites=none clean test
```
The focused input-stream peer-failover and interruption tests passed (6
tests):
```text
./build/mvn --no-transfer-progress -pl client -am \
-Dtest=CelebornInputStreamPeerFailoverTest \
-DwildcardSuites=none clean test
```
The Spark 3.5 reader suite passed (7 tests):
```text
./build/mvn --no-transfer-progress -Pspark-3.5 -pl client-spark/spark-3 -am
\
-Dtest=none \
-DwildcardSuites=org.apache.spark.shuffle.celeborn.CelebornShuffleReaderSuite \
clean test
```
Spark 4.0 / Scala 2.13 production and test compilation also passed:
```text
./build/mvn --no-transfer-progress -Pspark-4.0 -pl client-spark/spark-3 -am
\
-DskipTests clean test
```
Closes #3746 from
sunchao/dev/chao/codex/bound-batch-open-client-retries-oss.
Authored-by: Chao Sun <[email protected]>
Signed-off-by: Nicholas Jiang <[email protected]>
---
.../shuffle/celeborn/CelebornShuffleReader.scala | 92 +++++++++----
.../celeborn/CelebornShuffleReaderSuite.scala | 151 ++++++++++++++++-----
.../celeborn/client/read/CelebornInputStream.java | 32 +++++
.../read/CelebornInputStreamPeerFailoverTest.java | 119 +++++++++++++++-
.../network/client/TransportClientFactory.java | 41 +++++-
.../celeborn/common/util/ExceptionUtils.java | 11 ++
.../network/TransportClientFactorySuiteJ.java | 23 ++++
.../TransportClientFactoryInterruptSuiteJ.java | 57 ++++++++
8 files changed, 458 insertions(+), 68 deletions(-)
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 32863c6a54..87bf9ffd6e 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,8 +18,8 @@
package org.apache.spark.shuffle.celeborn
import java.io.IOException
-import java.util.{ArrayList => JArrayList, HashMap => JHashMap, Map => JMap,
Optional, Set => JSet}
-import java.util.concurrent.{ConcurrentHashMap, ThreadPoolExecutor,
TimeoutException, TimeUnit}
+import java.util.{ArrayList => JArrayList, HashMap => JHashMap, HashSet =>
JHashSet, Map => JMap, Optional, Set => JSet}
+import java.util.concurrent.{ConcurrentHashMap, ExecutionException,
ThreadPoolExecutor, TimeoutException, TimeUnit}
import java.util.concurrent.atomic.AtomicReference
import java.util.function.BiFunction
@@ -48,7 +48,7 @@ import
org.apache.celeborn.common.network.protocol.TransportMessage
import org.apache.celeborn.common.protocol._
import org.apache.celeborn.common.protocol.message.{ControlMessages,
StatusCode}
import
org.apache.celeborn.common.protocol.message.ControlMessages.GetReducerFileGroupResponse
-import org.apache.celeborn.common.util.{JavaUtils, ThreadUtils, Utils}
+import org.apache.celeborn.common.util.{ExceptionUtils, JavaUtils,
ThreadUtils, Utils}
class CelebornShuffleReader[K, C](
handle: CelebornShuffleHandle[K, _, C],
@@ -236,27 +236,31 @@ class CelebornShuffleReader[K, C](
val parallelClientCreationEnabled =
conf.batchOpenStreamParallelClientCreationEnabled
val locationsByHostPort =
new mutable.LinkedHashMap[String, JArrayList[PartitionLocation]]()
+ val attemptedClientHostPorts = new JHashSet[String]()
def makeOpenStreamList(locations: JSet[PartitionLocation]): Unit = {
locations.asScala.foreach { location =>
partCnt += 1
val hostPort = location.hostAndFetchPort
if (!workerRequestMap.containsKey(hostPort)) {
- try {
- val client = shuffleClient.getDataClientFactory().createClient(
- location.getHost,
- location.getFetchPort)
+ CelebornShuffleReader.tryCreateClientOncePerEndpoint(
+ location,
+ attemptedClientHostPorts,
+ loc =>
+ shuffleClient.getDataClientFactory().createClient(
+ loc.getHost,
+ loc.getFetchPort),
+ ex => {
+ shuffleClient.excludeFailedFetchLocation(hostPort, ex)
+ logWarning(
+ s"Failed to create client for $shuffleKey-${location.getId}
from host: ${hostPort}. " +
+ s"Shuffle reader will try its replica if exists.")
+ }).foreach { client =>
val pbOpenStreamList = PbOpenStreamList.newBuilder()
pbOpenStreamList.setShuffleKey(shuffleKey)
workerRequestMap.put(
hostPort,
(client, new JArrayList[PartitionLocation], pbOpenStreamList))
- } catch {
- case ex: Exception =>
- shuffleClient.excludeFailedFetchLocation(hostPort, ex)
- logWarning(
- s"Failed to create client for $shuffleKey-${location.getId}
from host: ${hostPort}. " +
- s"Shuffle reader will try its replica if exists.")
}
}
workerRequestMap.get(hostPort) match {
@@ -627,6 +631,42 @@ class CelebornShuffleReader[K, C](
object CelebornShuffleReader {
var streamCreatorPool: ThreadPoolExecutor = null
+ @VisibleForTesting
+ private[celeborn] def tryCreateClient(
+ location: PartitionLocation,
+ createClient: PartitionLocation => TransportClient,
+ onClientCreateFailure: Exception => Unit): Option[TransportClient] = {
+ if (Thread.currentThread().isInterrupted) {
+ throw new InterruptedException("Client creation interrupted")
+ }
+ try {
+ Some(createClient(location))
+ } catch {
+ case ex: Exception =>
+ val interruptedException = ExceptionUtils.findInterruptedException(ex)
+ if (interruptedException != null) {
+ Thread.currentThread().interrupt()
+ throw interruptedException
+ } else {
+ onClientCreateFailure(ex)
+ None
+ }
+ }
+ }
+
+ @VisibleForTesting
+ private[celeborn] def tryCreateClientOncePerEndpoint(
+ location: PartitionLocation,
+ attemptedClientHostPorts: JSet[String],
+ createClient: PartitionLocation => TransportClient,
+ onClientCreateFailure: Exception => Unit): Option[TransportClient] = {
+ if (attemptedClientHostPorts.add(location.hostAndFetchPort)) {
+ tryCreateClient(location, createClient, onClientCreateFailure)
+ } else {
+ None
+ }
+ }
+
@VisibleForTesting
private[celeborn] def createClientsInParallel(
locationsByHostPort: Seq[(String, Seq[PartitionLocation])],
@@ -638,19 +678,12 @@ object CelebornShuffleReader {
val futures = locationsByHostPort.map { case (hostPort, locations) =>
streamCreatorPool.submit(new Runnable {
override def run(): Unit = {
- val locationsIterator = locations.iterator
- var clientCreated = false
- while (!clientCreated && locationsIterator.hasNext) {
- val location = locationsIterator.next()
- try {
- clientsByHostPort.put(hostPort, createClient(location))
- clientCreated = true
- } catch {
- case ex: InterruptedException =>
- Thread.currentThread().interrupt()
- throw ex
- case ex: Exception =>
- onClientCreateFailure(hostPort, location, ex)
+ locations.headOption.foreach { location =>
+ tryCreateClient(
+ location,
+ createClient,
+ ex => onClientCreateFailure(hostPort, location, ex)).foreach {
client =>
+ clientsByHostPort.put(hostPort, client)
}
}
}
@@ -664,6 +697,13 @@ object CelebornShuffleReader {
case ex: InterruptedException =>
Thread.currentThread().interrupt()
throw ex
+ case ex: ExecutionException =>
+ val interruptedException = ExceptionUtils.findInterruptedException(ex)
+ if (interruptedException != null) {
+ Thread.currentThread().interrupt()
+ throw interruptedException
+ }
+ throw ex
} finally {
if (!waitCompleted) {
futures.foreach(_.cancel(true))
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 901d7797b5..acd68bfff7 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
@@ -19,8 +19,9 @@ package org.apache.spark.shuffle.celeborn
import java.io.IOException
import java.nio.file.Files
+import java.util.HashSet
import java.util.concurrent.{CountDownLatch, TimeoutException, TimeUnit}
-import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}
+import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger,
AtomicReference}
import org.apache.spark.{Dependency, ShuffleDependency, TaskContext}
import org.apache.spark.shuffle.ShuffleReadMetricsReporter
@@ -164,39 +165,131 @@ class CelebornShuffleReaderSuite extends AnyFunSuite {
}
}
- test("retry failed batch open stream client creation for the same worker") {
- val failedLocation = newLocation(0, "worker-0", 19098)
- val retryLocation = newLocation(1, "worker-0", 19098)
- val client = Mockito.mock(classOf[TransportClient])
+ test("attempt batch open stream client creation once per worker") {
+ val locations = (0 until 100).map(id => newLocation(id, "worker-0", 19098))
val streamCreatorPool =
ThreadUtils.newDaemonCachedThreadPool("test-create-client", 1, 60)
- var failureCount = 0
+ val attempts = new AtomicInteger()
+ val failures = new AtomicInteger()
try {
val clients = CelebornShuffleReader.createClientsInParallel(
- Seq(failedLocation.hostAndFetchPort -> Seq(failedLocation,
retryLocation)),
+ Seq(locations.head.hostAndFetchPort -> locations),
streamCreatorPool,
- location => {
- if (location eq failedLocation) throw new IOException("boom")
- client
+ _ => {
+ attempts.incrementAndGet()
+ throw new IOException("boom")
},
- (_, _, _) => failureCount += 1)
+ (_, _, _) => failures.incrementAndGet())
+
+ assert(clients.isEmpty)
+ assert(attempts.get() === 1)
+ assert(failures.get() === 1)
+ } finally {
+ streamCreatorPool.shutdownNow()
+ }
+ }
+
+ test("attempt sequential batch open stream client creation once per worker
endpoint") {
+ val firstLocation = newLocation(0, "worker-0", 19098)
+ val duplicateEndpoint = newLocation(1, "worker-0", 19098)
+ val differentEndpoint = newLocation(2, "worker-0", 19099)
+ val attemptedClientHostPorts = new HashSet[String]()
+ val attempts = new AtomicInteger()
+ val failures = new AtomicInteger()
+
+ Seq(firstLocation, duplicateEndpoint, differentEndpoint).foreach {
location =>
+ val client = CelebornShuffleReader.tryCreateClientOncePerEndpoint(
+ location,
+ attemptedClientHostPorts,
+ _ => {
+ attempts.incrementAndGet()
+ throw new IOException("boom")
+ },
+ _ => failures.incrementAndGet())
+ assert(client.isEmpty)
+ }
+
+ assert(attempts.get() === 2)
+ assert(failures.get() === 2)
+ }
+
+ test("propagate wrapped client creation interruption without reporting a
worker failure") {
+ val location = newLocation(0, "worker-0", 19098)
+ val failureReported = new AtomicBoolean(false)
+ val interruptedException = new InterruptedException("test")
+
+ try {
+ val exception = intercept[InterruptedException] {
+ CelebornShuffleReader.tryCreateClient(
+ location,
+ _ => throw new IOException("wrapped", interruptedException),
+ _ => failureReported.set(true))
+ }
+
+ assert(exception eq interruptedException)
+ assert(Thread.currentThread().isInterrupted)
+ assert(!failureReported.get())
+ } finally {
+ Thread.interrupted()
+ }
+ }
+
+ test("propagate interruption across parallel client creation future
boundary") {
+ val location = newLocation(0, "worker-0", 19098)
+ val interruptedException = new InterruptedException("test")
+ val failureReported = new AtomicBoolean(false)
+ val streamCreatorPool =
ThreadUtils.newDaemonCachedThreadPool("test-create-client", 1, 60)
- assert(failureCount === 1)
- assert(clients(failedLocation.hostAndFetchPort) eq client)
+ try {
+ val exception = intercept[InterruptedException] {
+ CelebornShuffleReader.createClientsInParallel(
+ Seq(location.hostAndFetchPort -> Seq(location)),
+ streamCreatorPool,
+ _ => throw new IOException("wrapped", interruptedException),
+ (_, _, _) => failureReported.set(true))
+ }
+
+ assert(exception eq interruptedException)
+ assert(Thread.currentThread().isInterrupted)
+ assert(!failureReported.get())
} finally {
+ Thread.interrupted()
streamCreatorPool.shutdownNow()
}
}
+ test("skip client creation when the thread is already interrupted") {
+ val location = newLocation(0, "worker-0", 19098)
+ val clientCreationAttempted = new AtomicBoolean(false)
+ val failureReported = new AtomicBoolean(false)
+
+ try {
+ Thread.currentThread().interrupt()
+ intercept[InterruptedException] {
+ CelebornShuffleReader.tryCreateClient(
+ location,
+ _ => {
+ clientCreationAttempted.set(true)
+ Mockito.mock(classOf[TransportClient])
+ },
+ _ => failureReported.set(true))
+ }
+
+ assert(Thread.currentThread().isInterrupted)
+ assert(!clientCreationAttempted.get())
+ assert(!failureReported.get())
+ } finally {
+ Thread.interrupted()
+ }
+ }
+
test("cancel batch open stream client creation when waiting thread is
interrupted") {
val blockedLocation = newLocation(0, "worker-0", 19098)
- val retryLocation = newLocation(1, "worker-0", 19098)
- val retryClient = Mockito.mock(classOf[TransportClient])
+ val client = Mockito.mock(classOf[TransportClient])
val streamCreatorPool =
ThreadUtils.newDaemonCachedThreadPool("test-create-client", 1, 60)
val clientStarted = new CountDownLatch(1)
val releaseClient = new CountDownLatch(1)
val clientInterrupted = new CountDownLatch(1)
- val retried = new AtomicBoolean(false)
val failureReported = new AtomicBoolean(false)
val callerInterrupted = new AtomicBoolean(false)
val callerFailure = new AtomicReference[Throwable]()
@@ -205,22 +298,17 @@ class CelebornShuffleReaderSuite extends AnyFunSuite {
override def run(): Unit = {
try {
CelebornShuffleReader.createClientsInParallel(
- Seq(blockedLocation.hostAndFetchPort -> Seq(blockedLocation,
retryLocation)),
+ Seq(blockedLocation.hostAndFetchPort -> Seq(blockedLocation)),
streamCreatorPool,
- location => {
- if (location eq blockedLocation) {
- clientStarted.countDown()
- try {
- releaseClient.await()
- retryClient
- } catch {
- case ex: InterruptedException =>
- clientInterrupted.countDown()
- throw ex
- }
- } else {
- retried.set(true)
- retryClient
+ _ => {
+ clientStarted.countDown()
+ try {
+ releaseClient.await()
+ client
+ } catch {
+ case ex: InterruptedException =>
+ clientInterrupted.countDown()
+ throw ex
}
},
(_, _, _) => failureReported.set(true))
@@ -245,7 +333,6 @@ class CelebornShuffleReaderSuite extends AnyFunSuite {
assert(streamCreatorPool.awaitTermination(5, TimeUnit.SECONDS))
assert(callerFailure.get() == null)
assert(callerInterrupted.get())
- assert(!retried.get())
assert(!failureReported.get())
} finally {
releaseClient.countDown()
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 d76b1eb9d3..614ce5926f 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
@@ -49,6 +49,7 @@ import
org.apache.celeborn.common.network.protocol.TransportMessage;
import org.apache.celeborn.common.protocol.*;
import org.apache.celeborn.common.unsafe.Platform;
import org.apache.celeborn.common.util.ExceptionMaker;
+import org.apache.celeborn.common.util.ExceptionUtils;
import org.apache.celeborn.common.util.Utils;
import org.apache.celeborn.common.write.LocationPushFailedBatches;
@@ -448,6 +449,16 @@ public abstract class CelebornInputStream extends
InputStream {
}
}
+ private static boolean isInterruption(Throwable throwable) {
+ return Thread.currentThread().isInterrupted()
+ || ExceptionUtils.findInterruptedException(throwable) != null;
+ }
+
+ private static IOException interruptedIOException(Exception exception) {
+ Thread.currentThread().interrupt();
+ return ExceptionUtils.wrapThrowableToIOException(exception);
+ }
+
private PartitionReader createReaderWithRetry(
PartitionLocation location, PbStreamHandler pbStreamHandler) throws
IOException {
return createReaderWithRetry(location, pbStreamHandler,
Optional.empty());
@@ -474,6 +485,9 @@ public abstract class CelebornInputStream extends
InputStream {
checkpointMetadata);
return reader;
} catch (Exception e) {
+ if (isInterruption(e)) {
+ throw interruptedIOException(e);
+ }
lastException = e;
shuffleClient.excludeFailedFetchLocation(location.hostAndFetchPort(), e);
fetchChunkRetryCnt++;
@@ -503,6 +517,9 @@ public abstract class CelebornInputStream extends
InputStream {
.toByteArray());
client.sendRpc(bufferStreamEnd.toByteBuffer());
} catch (InterruptedException | IOException | RuntimeException
ex) {
+ if (isInterruption(ex)) {
+ throw interruptedIOException(ex);
+ }
logger.warn(
"Close {} stream {} failed",
location.hostAndFetchPort(),
@@ -538,6 +555,15 @@ public abstract class CelebornInputStream extends
InputStream {
}
return currentReader.next();
} catch (Exception e) {
+ if (isInterruption(e)) {
+ IOException interrupted = interruptedIOException(e);
+ try {
+ currentReader.close();
+ } catch (RuntimeException closeException) {
+ interrupted.addSuppressed(closeException);
+ }
+ throw interrupted;
+ }
shuffleClient.excludeFailedFetchLocation(
currentReader.getLocation().hostAndFetchPort(), e);
fetchChunkRetryCnt++;
@@ -946,6 +972,9 @@ public abstract class CelebornInputStream extends
InputStream {
}
return hasData;
} catch (LZ4Exception | ZstdException | IOException e) {
+ if (isInterruption(e)) {
+ throw interruptedIOException(e);
+ }
logger.error(
"Failed to fill buffer from chunk. AppShuffleId {}, shuffleId {},
partitionId {}, location {}",
appShuffleId,
@@ -974,6 +1003,9 @@ public abstract class CelebornInputStream extends
InputStream {
}
throw ioe;
} catch (Exception e) {
+ if (isInterruption(e)) {
+ throw interruptedIOException(e);
+ }
logger.error(
"Failed to fill buffer from chunk. AppShuffleId {}, shuffleId {},
partitionId {}, location {}",
appShuffleId,
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 456486b9e8..ec1003b7d0 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
@@ -17,6 +17,10 @@
package org.apache.celeborn.client.read;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -24,7 +28,10 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -50,8 +57,9 @@ 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.util.ExceptionMaker;
-/** Tests for CelebornInputStream peer failover when stream cleanup throws
RuntimeException. */
+/** Tests for CelebornInputStream peer failover and interruption handling. */
public class CelebornInputStreamPeerFailoverTest {
private static final String SHUFFLE_KEY = "appid-1-1";
@@ -144,6 +152,103 @@ public class CelebornInputStreamPeerFailoverTest {
}
}
+ @Test
+ public void testInterruptedClientCreationDoesNotRetryOrFailOver() throws
Exception {
+ InterruptedException interruptedException = new
InterruptedException("cancelled");
+ IOException wrappedException = new IOException("wrapped",
interruptedException);
+ when(clientFactory.createClient(anyString(),
anyInt())).thenThrow(wrappedException);
+
+ try {
+ IOException thrown =
+ assertThrows(IOException.class, () ->
createInputStream(PRIMARY_HOST, REPLICA_HOST));
+
+ assertSame(wrappedException, thrown);
+ assertTrue(Thread.currentThread().isInterrupted());
+ verify(clientFactory, times(1)).createClient(anyString(), anyInt());
+ verify(shuffleClient, never()).excludeFailedFetchLocation(anyString(),
any());
+ } finally {
+ Thread.interrupted();
+ }
+ }
+
+ @Test
+ public void testInterruptedCleanupDoesNotFailOver() throws Exception {
+ AtomicInteger primaryAttempts = new AtomicInteger();
+ AtomicInteger replicaAttempts = new AtomicInteger();
+ InterruptedException interruptedException = new
InterruptedException("cancelled");
+ IOException cleanupException = new IOException("cleanup interrupted",
interruptedException);
+
+ when(clientFactory.createClient(anyString(), anyInt()))
+ .thenAnswer(
+ invocation -> {
+ String host = invocation.getArgument(0);
+ if (PRIMARY_HOST.equals(host)) {
+ if (primaryAttempts.incrementAndGet() == 1) {
+ throw new IOException("Worker Not Registered!");
+ }
+ throw cleanupException;
+ }
+ replicaAttempts.incrementAndGet();
+ return mockReplicaClient();
+ });
+
+ try {
+ IOException thrown =
+ assertThrows(IOException.class, () ->
createInputStream(PRIMARY_HOST, REPLICA_HOST));
+
+ assertSame(cleanupException, thrown);
+ assertTrue(Thread.currentThread().isInterrupted());
+ assertEquals(2, primaryAttempts.get());
+ assertEquals(0, replicaAttempts.get());
+ } finally {
+ Thread.interrupted();
+ }
+ }
+
+ @Test
+ public void testInterruptedReconnectDoesNotReportFetchFailureOrFailOver()
throws Exception {
+ AtomicInteger primaryAttempts = new AtomicInteger();
+ AtomicInteger replicaAttempts = new AtomicInteger();
+ TransportClient inactiveClient = mock(TransportClient.class);
+ when(inactiveClient.isActive()).thenReturn(false, true);
+ InterruptedException interruptedException = new
InterruptedException("cancelled");
+ IOException reconnectException = new IOException("reconnect interrupted",
interruptedException);
+ RuntimeException closeException = new RuntimeException("close failed");
+
doThrow(closeException).when(inactiveClient).sendRpc(any(ByteBuffer.class));
+
+ when(clientFactory.createClient(anyString(), anyInt()))
+ .thenAnswer(
+ invocation -> {
+ String host = invocation.getArgument(0);
+ if (PRIMARY_HOST.equals(host)) {
+ if (primaryAttempts.incrementAndGet() == 1) {
+ return inactiveClient;
+ }
+ throw reconnectException;
+ }
+ replicaAttempts.incrementAndGet();
+ return mockReplicaClient();
+ });
+ ExceptionMaker exceptionMaker = mock(ExceptionMaker.class);
+
+ try {
+ CelebornInputStream inputStream =
+ createInputStream(PRIMARY_HOST, REPLICA_HOST, exceptionMaker);
+ IOException thrown = assertThrows(IOException.class, inputStream::read);
+
+ assertSame(reconnectException, thrown.getCause());
+ assertEquals(1, thrown.getSuppressed().length);
+ assertSame(closeException, thrown.getSuppressed()[0]);
+ assertTrue(Thread.currentThread().isInterrupted());
+ assertEquals(2, primaryAttempts.get());
+ assertEquals(0, replicaAttempts.get());
+ verify(shuffleClient, never()).excludeFailedFetchLocation(anyString(),
any());
+ verify(shuffleClient, never()).reportShuffleFetchFailure(anyInt(),
anyInt(), anyLong());
+ } finally {
+ Thread.interrupted();
+ }
+ }
+
/** Tests that all retries are exhausted and an exception is thrown when
there is no peer. */
@Test(expected = CelebornIOException.class)
public void testFailureWithoutPeer() throws Exception {
@@ -180,7 +285,13 @@ public class CelebornInputStreamPeerFailoverTest {
Optional.<CryptoHandler>empty());
}
- private void createInputStream(String primaryHost, String replicaHost)
throws IOException {
+ private CelebornInputStream createInputStream(String primaryHost, String
replicaHost)
+ throws IOException {
+ return createInputStream(primaryHost, replicaHost, null);
+ }
+
+ private CelebornInputStream createInputStream(
+ String primaryHost, String replicaHost, ExceptionMaker exceptionMaker)
throws IOException {
PartitionLocation primary = createPartitionLocation(primaryHost);
PartitionLocation replica = createPartitionLocation(replicaHost);
primary.setPeer(replica);
@@ -192,7 +303,7 @@ public class CelebornInputStreamPeerFailoverTest {
ArrayList<PbStreamHandler> streamHandlers = new ArrayList<>();
streamHandlers.add(PbStreamHandler.newBuilder().setStreamId(123L).setNumChunks(10).build());
- CelebornInputStream.create(
+ return CelebornInputStream.create(
conf,
clientFactory,
SHUFFLE_KEY,
@@ -210,7 +321,7 @@ public class CelebornInputStreamPeerFailoverTest {
1,
1,
0,
- null,
+ exceptionMaker,
new TestMetricsCallback(),
false,
Optional.<CryptoHandler>empty());
diff --git
a/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
b/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
index 08c0d277b3..b79383277a 100644
---
a/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
+++
b/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
@@ -28,6 +28,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
@@ -47,6 +48,7 @@ import org.apache.celeborn.common.network.TransportContext;
import
org.apache.celeborn.common.network.sasl.registration.RegistrationClientBootstrap;
import org.apache.celeborn.common.network.server.TransportChannelHandler;
import org.apache.celeborn.common.network.util.*;
+import org.apache.celeborn.common.util.ExceptionUtils;
import org.apache.celeborn.common.util.JavaUtils;
import org.apache.celeborn.common.util.Utils;
@@ -160,9 +162,10 @@ public class TransportClientFactory implements Closeable {
try {
return createClient(remoteHost, remotePort, partitionId,
supplier.get());
} catch (Exception e) {
- if (e instanceof InterruptedException) {
+ InterruptedException interruptedException =
ExceptionUtils.findInterruptedException(e);
+ if (interruptedException != null) {
Thread.currentThread().interrupt();
- throw e;
+ throw interruptedException;
}
numTries++;
logger.warn(
@@ -315,14 +318,21 @@ public class TransportClientFactory implements Closeable {
long preConnect = System.nanoTime();
ChannelFuture cf = bootstrap.connect(address);
if (connectTimeoutMs <= 0) {
- cf.await();
+ awaitWithChannelCleanup(
+ () -> {
+ cf.await();
+ return true;
+ },
+ cf);
assert cf.isDone();
if (cf.isCancelled()) {
+ closeChannel(cf);
throw new IOException(String.format("Connecting to %s cancelled",
address));
} else if (!cf.isSuccess()) {
+ closeChannel(cf);
throw new IOException(String.format("Failed to connect to %s",
address), cf.cause());
}
- } else if (!cf.await(connectTimeoutMs)) {
+ } else if (!awaitWithChannelCleanup(() -> cf.await(connectTimeoutMs), cf))
{
closeChannel(cf);
throw new CelebornIOException(
String.format("Connecting to %s timed out (%s ms)", address,
connectTimeoutMs));
@@ -351,7 +361,7 @@ public class TransportClientFactory implements Closeable {
}
}
});
- if (!future.await(connectionTimeoutMs)) {
+ if (!awaitWithChannelCleanup(() -> future.await(connectionTimeoutMs),
cf)) {
closeChannel(cf);
throw new IOException(
String.format("Failed to connect to %s within connection timeout",
address));
@@ -403,6 +413,25 @@ public class TransportClientFactory implements Closeable {
return client;
}
+ @FunctionalInterface
+ @VisibleForTesting
+ interface InterruptibleAwait {
+ boolean await() throws InterruptedException;
+ }
+
+ @VisibleForTesting
+ static boolean awaitWithChannelCleanup(
+ InterruptibleAwait interruptibleAwait, ChannelFuture channelFuture)
+ throws InterruptedException {
+ try {
+ return interruptibleAwait.await();
+ } catch (InterruptedException e) {
+ closeChannel(channelFuture);
+ Thread.currentThread().interrupt();
+ throw e;
+ }
+ }
+
/** Close all connections in the connection pool, and shutdown the worker
thread pool. */
@Override
public void close() {
@@ -428,7 +457,7 @@ public class TransportClientFactory implements Closeable {
return context;
}
- private void closeChannel(ChannelFuture channelFuture) {
+ private static void closeChannel(ChannelFuture channelFuture) {
try {
channelFuture.channel().close();
} catch (Exception e) {
diff --git
a/common/src/main/java/org/apache/celeborn/common/util/ExceptionUtils.java
b/common/src/main/java/org/apache/celeborn/common/util/ExceptionUtils.java
index 8f00251771..39cf14d617 100644
--- a/common/src/main/java/org/apache/celeborn/common/util/ExceptionUtils.java
+++ b/common/src/main/java/org/apache/celeborn/common/util/ExceptionUtils.java
@@ -21,6 +21,8 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import com.google.common.base.Throwables;
+
import org.apache.celeborn.common.exception.CelebornIOException;
import org.apache.celeborn.common.exception.PartitionUnRetryAbleException;
@@ -52,6 +54,15 @@ public class ExceptionUtils {
}
}
+ public static InterruptedException findInterruptedException(Throwable
throwable) {
+ for (Throwable cause : Throwables.getCausalChain(throwable)) {
+ if (cause instanceof InterruptedException) {
+ return (InterruptedException) cause;
+ }
+ }
+ return null;
+ }
+
public static String stringifyException(Throwable exception) {
if (exception == null) {
return "(null)";
diff --git
a/common/src/test/java/org/apache/celeborn/common/network/TransportClientFactorySuiteJ.java
b/common/src/test/java/org/apache/celeborn/common/network/TransportClientFactorySuiteJ.java
index 7ec7190aa0..3c51a175d3 100644
---
a/common/src/test/java/org/apache/celeborn/common/network/TransportClientFactorySuiteJ.java
+++
b/common/src/test/java/org/apache/celeborn/common/network/TransportClientFactorySuiteJ.java
@@ -260,4 +260,27 @@ public class TransportClientFactorySuiteJ {
factory.retryCreateClient("xxx", 10, 1, TransportFrameDecoder::new);
Assert.assertEquals(transportClient, client);
}
+
+ @Test
+ public void doNotRetryCreateClientWhenInterruptedExceptionIsWrapped() throws
Exception {
+ TransportClientFactory factory =
Mockito.spy(context.createClientFactory());
+ InterruptedException interruptedException = new
InterruptedException("test");
+ Mockito.doThrow(new IOException("wrapped", interruptedException))
+ .when(factory)
+ .createClient(anyString(), anyInt(), anyInt(), any());
+
+ try {
+ InterruptedException thrown =
+ assertThrows(
+ InterruptedException.class,
+ () -> factory.retryCreateClient("xxx", 10, 1,
TransportFrameDecoder::new));
+
+ assertSame(interruptedException, thrown);
+ assertTrue(Thread.currentThread().isInterrupted());
+ Mockito.verify(factory, Mockito.times(1))
+ .createClient(anyString(), anyInt(), anyInt(), any());
+ } finally {
+ Thread.interrupted();
+ }
+ }
}
diff --git
a/common/src/test/java/org/apache/celeborn/common/network/client/TransportClientFactoryInterruptSuiteJ.java
b/common/src/test/java/org/apache/celeborn/common/network/client/TransportClientFactoryInterruptSuiteJ.java
new file mode 100644
index 0000000000..e6735b27fa
--- /dev/null
+++
b/common/src/test/java/org/apache/celeborn/common/network/client/TransportClientFactoryInterruptSuiteJ.java
@@ -0,0 +1,57 @@
+/*
+ * 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.common.network.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import org.junit.Test;
+
+public class TransportClientFactoryInterruptSuiteJ {
+
+ @Test
+ public void closeChannelWhenClientCreationIsInterrupted() throws Exception {
+ ChannelFuture channelFuture = mock(ChannelFuture.class);
+ Channel channel = mock(Channel.class);
+ when(channelFuture.channel()).thenReturn(channel);
+
+ try {
+ InterruptedException exception =
+ assertThrows(
+ InterruptedException.class,
+ () ->
+ TransportClientFactory.awaitWithChannelCleanup(
+ () -> {
+ throw new InterruptedException("test");
+ },
+ channelFuture));
+
+ assertEquals("test", exception.getMessage());
+ assertTrue(Thread.currentThread().isInterrupted());
+ verify(channel).close();
+ } finally {
+ Thread.interrupted();
+ }
+ }
+}