-
Notifications
You must be signed in to change notification settings - Fork 450
[CELEBORN-2371] Bound Spark batch-open client creation retries and stop them on interruption #3746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -215,21 +215,23 @@ class CelebornShuffleReader[K, C]( | |
| partCnt += 1 | ||
| val hostPort = location.hostAndFetchPort | ||
| if (!workerRequestMap.containsKey(hostPort)) { | ||
| try { | ||
| val client = shuffleClient.getDataClientFactory().createClient( | ||
| location.getHost, | ||
| location.getFetchPort) | ||
| CelebornShuffleReader.tryCreateClient( | ||
| location, | ||
| 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 { | ||
|
|
@@ -600,6 +602,26 @@ 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: InterruptedException => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consistency: this catches only a bare Note the three interrupt-detection helpers this PR introduces already disagree: Related latent gap in |
||
| Thread.currentThread().interrupt() | ||
| throw ex | ||
| case ex: Exception => | ||
| onClientCreateFailure(ex) | ||
| None | ||
| } | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| private[celeborn] def createClientsInParallel( | ||
| locationsByHostPort: Seq[(String, Seq[PartitionLocation])], | ||
|
|
@@ -611,19 +633,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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| import scala.Tuple2; | ||
|
|
||
| import com.github.luben.zstd.ZstdException; | ||
| import com.google.common.base.Throwables; | ||
| import com.google.common.util.concurrent.Uninterruptibles; | ||
| import io.netty.buffer.ByteBuf; | ||
| import net.jpountz.lz4.LZ4Exception; | ||
|
|
@@ -48,6 +49,7 @@ | |
| 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; | ||
|
|
||
|
|
@@ -438,6 +440,23 @@ private boolean isExcluded(PartitionLocation location) { | |
| } | ||
| } | ||
|
|
||
| private static boolean isInterruption(Throwable throwable) { | ||
| if (Thread.currentThread().isInterrupted()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Concern: a genuine, retryable fetch failure whose cause chain happens to wrap an |
||
| return true; | ||
| } | ||
| for (Throwable cause : Throwables.getCausalChain(throwable)) { | ||
| if (cause instanceof InterruptedException) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| 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()); | ||
|
|
@@ -464,6 +483,9 @@ private PartitionReader createReaderWithRetry( | |
| checkpointMetadata); | ||
| return reader; | ||
| } catch (Exception e) { | ||
| if (isInterruption(e)) { | ||
| throw interruptedIOException(e); | ||
| } | ||
| lastException = e; | ||
| shuffleClient.excludeFailedFetchLocation(location.hostAndFetchPort(), e); | ||
| fetchChunkRetryCnt++; | ||
|
|
@@ -493,6 +515,9 @@ private PartitionReader createReaderWithRetry( | |
| .toByteArray()); | ||
| client.sendRpc(bufferStreamEnd.toByteBuffer()); | ||
| } catch (InterruptedException | IOException | RuntimeException ex) { | ||
| if (isInterruption(ex)) { | ||
| throw interruptedIOException(ex); | ||
| } | ||
| logger.warn( | ||
| "Close {} stream {} failed", | ||
| location.hostAndFetchPort(), | ||
|
|
@@ -528,6 +553,15 @@ private ByteBuf getNextChunk() throws IOException { | |
| } | ||
| 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++; | ||
|
|
@@ -890,6 +924,9 @@ private boolean fillBuffer() throws IOException { | |
| } | ||
| 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, | ||
|
|
@@ -918,6 +955,9 @@ private boolean fillBuffer() throws IOException { | |
| } | ||
| throw ioe; | ||
| } catch (Exception e) { | ||
| if (isInterruption(e)) { | ||
| throw interruptedIOException(e); | ||
| } | ||
| logger.error( | ||
| "Failed to fill buffer from chunk. AppShuffleId {}, shuffleId {}, partitionId {}, location {}", | ||
| appShuffleId, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The retry-bounding fix (and the
headOptionchange increateClientsInParallel) only covers the parallel client-creation path. The sequentialmakeOpenStreamListstill attempts creation per partition-location: whentryCreateClientreturnsNone,onClientCreateFailureonly excludes + warns and never populatesworkerRequestMap, so the next location sharing the samehostPortre-entersif (!workerRequestMap.containsKey(hostPort))(line 217) and callscreateClientagainst the same dead endpoint again — theN × maxRetriesblow-up this PR sets out to remove.So with
celeborn.client.spark.batch.openStream.parallelClientCreation.enabled=false, the stated goal ("Bound … client creation retries") isn't achieved. This isn't a regression (it matches the old behavior), but since the parallel path is now bounded it's an inconsistency worth either closing (e.g. record the failedhostPortso subsequent same-host locations short-circuit) or calling out as a known limitation.