Skip to content

Commit c15aa12

Browse files
jerrypengcloud-fan
authored andcommitted
[SPARK-58185][CORE] Define PipelinedShuffleDependency and route shuffles to a ShuffleManager by dependency type
### What changes were proposed in this pull request? This PR routes each shuffle to a `ShuffleManager` **by its dependency type**, so a `PipelinedShuffleDependency` (added in the previous PR) is served by an incremental shuffle implementation while every other shuffle continues to use the regular one. `SparkEnv` now holds two shuffle managers, keyed by **kind** rather than a default and an override: - `spark.shuffle.manager` — the **blocking** manager (a `BlockingShuffleManager`), serving all regular (materialized) shuffles and owning block-by-id resolution. Defaults to `sort`, as before. - `spark.shuffle.manager.incremental` — the **pipelined** manager (a `PipelinedShuffleManager`), serving pipelined (incrementally-readable) shuffle dependencies. Defaults to the built-in `streaming` manager, just as the blocking manager defaults to `sort`. Routing is a single pure function, `SparkEnv.shuffleManagerFor(dependency)`: a `PipelinedShuffleDependency` goes to the pipelined manager, every other `ShuffleDependency` to the blocking manager. Both managers always exist, so there is no fallback path. Because the decision is a pure function of the (serialized, deterministic) dependency, the driver at `registerShuffle` and every executor at `getWriter` / `getReader` agree without any shared routing state or config re-read. All shuffle-I/O sites route through it (`Dependency.shuffleHandle`, `ShuffleWriteProcessor`, `ShuffledRDD`, `CoGroupedRDD`, `SubtractedRDD`, `ShuffledRowRDD`, `ShuffleExchangeExec`). To make the manager APIs harder to misuse, `ShuffleManager` is split by kind: ``` ShuffleManager (declare a kind by extending one of the two subtypes) BlockingShuffleManager <- materialized output, served as block-manager blocks via a ShuffleBlockResolver (SortShuffleManager) PipelinedShuffleManager <- incrementally-readable, served out-of-band, no resolver (StreamingShuffleManager) ``` `shuffleBlockResolver` moves off `ShuffleManager` onto `BlockingShuffleManager`, so a manager's type now says whether it serves block-manager-addressed blocks at all -- instead of the old contract where every manager had a `shuffleBlockResolver` that some implementations threw from. Block-by-id resolution (reads, push-based merge, decommission migration, external-shuffle-service cleanup) is served through a new `SparkEnv.shuffleBlockResolver: Option[ShuffleBlockResolver]`, sourced from the blocking manager; it is `None` only before the shuffle manager is initialized. This is correct because a pipelined shuffle is served out-of-band and produces no block-manager blocks, so those paths only ever resolve regular shuffles. `initializeShuffleManager` validates each slot's kind and fails fast otherwise: the `spark.shuffle.manager` slot must be a `BlockingShuffleManager` and the `spark.shuffle.manager.incremental` slot a `PipelinedShuffleManager`. A manager that declares neither kind is rejected from both slots. This is the "declare your kind" enforcement, and it applies to reflectively-loaded managers too, so `ShuffleManager` itself is **not** sealed -- the SPARK-45762 third-party extension point (a user-jar manager implementing the interface) stays open. Supporting changes: - `SparkEnv.blockingShuffleManager` and `SparkEnv.pipelinedShuffleManager` are the explicit by-kind accessors (e.g. sort-shuffle detection in `ShuffleExchangeExec` reads `blockingShuffleManager`); the bare `SparkEnv.shuffleManager` accessor is deprecated in favor of `shuffleManagerFor` / `blockingShuffleManager` so the routing decision is explicit at each call site. - `spark.shuffle.manager.incremental` accepts the same style of short aliases as `spark.shuffle.manager` -- `streaming` (the default) resolves to `StreamingShuffleManager`, and `sort` / `tungsten-sort` resolve to `SortShuffleManager` (though a blocking manager is then rejected from this slot by the kind validation). - The `StreamingShuffleOutputTracker` is initialized when the pipelined manager is a `StreamingShuffleManager` (the default) or the blocking manager is a `MultiShuffleManager`. - `MultiShuffleManager` (the legacy cluster-level single-slot way to mix streaming and regular shuffle) is deprecated in favor of this per-dependency routing. The routing code does not use it; `SparkEnv` still recognizes it for tracker initialization (with a TODO to drop that once `MultiShuffleManager` is removed). - `spark.shuffle.manager.incremental` declares `ConfigBindingPolicy.NOT_APPLICABLE` (a shuffle manager is a physical-execution choice and does not change how a view/UDF body resolves). This is a follow-up in the stack that begins with `PipelinedShuffleDependency`; a later `DAGScheduler` change adds the concurrent (pipelined-group) stage scheduling that actually constructs these dependencies. ### Why are the changes needed? `PipelinedShuffleDependency` marks a shuffle whose consumer reads output incrementally, but a marker type alone does nothing until the shuffle layer can serve it with an incremental implementation while regular shuffles keep using the existing one. A cluster needs both to coexist and to be chosen per shuffle, not per cluster. This PR provides that: a dependency-typed routing point plus two coexisting managers keyed by kind, so batch (sort) and pipelined (e.g. streaming real-time mode) shuffles can run in the same application, each served by the right implementation. Splitting `ShuffleManager` by kind (`BlockingShuffleManager` / `PipelinedShuffleManager`) encodes in the type system the invariant the routing relies on -- that only a blocking manager serves block-manager-addressed blocks -- so callers can no longer accidentally reach a resolver that does not exist. ### Does this PR introduce _any_ user-facing change? No user-visible behavior change for existing workloads: regular shuffles are still served by `spark.shuffle.manager` (default `sort`) exactly as before, query results are unchanged, and the block-by-id paths (reads, push-based merge, decommission, ESS cleanup) are unchanged. Nothing constructs a `PipelinedShuffleDependency` yet, so no shuffle is routed to the pipelined manager in this PR. One internal behavior change worth calling out: `spark.shuffle.manager.incremental` now defaults to the built-in streaming manager (rather than being unset), so a `StreamingShuffleOutputTracker` is initialized in every `SparkEnv`. The new config, the `BlockingShuffleManager` / `PipelinedShuffleManager` traits, and the `SparkEnv` routing accessors are `private[spark]`; the only externally visible API change is that the `DeveloperApi` `SparkEnv.shuffleManager` accessor is now `deprecated` (still functional, pointing callers to the explicit accessors). ### How was this patch tested? New unit tests in `PipelinedShuffleRoutingSuite` (16 tests) verify the routing and the manager-kind model: - `shuffleManagerFor` routes a regular dependency to the blocking manager and a `PipelinedShuffleDependency` to the pipelined manager; - the handle a dependency mints comes from the routed manager (so driver and executor agree), and `blockingShuffleManager` is the plain configured manager (no wrapper installed in front of it); - the id-only cleanup path notifies both managers (and is a no-op, not an NPE, before the managers are initialized), and both managers are stopped on `SparkContext` stop; - `spark.shuffle.manager.incremental` resolves the same short aliases as `spark.shuffle.manager`, a bad incremental class name fails fast at startup, a blocking manager in the incremental slot is rejected, and a pipelined manager in the default slot is rejected; - a manager's type declares its kind (`SortShuffleManager` is a `BlockingShuffleManager`; `StreamingShuffleManager` is a `PipelinedShuffleManager` and not a `BlockingShuffleManager`), and `SparkEnv.shuffleBlockResolver` is defined for the blocking manager. `StreamingShuffleManagerSuite` verifies the streaming output tracker is initialized by default (the streaming incremental manager), for an explicit incremental `StreamingShuffleManager`, and for a `MultiShuffleManager` default. Existing coverage of the block-by-id and cleanup paths was updated and rerun: `BlockManagerSuite` (including the deferred-`ShuffleManager` init and unsupported-resolver paths), `ContextCleanerSuite` (the real `RemoveShuffle` RPC through `BlockManagerMasterEndpoint`), `SortShuffleSuite`, `SparkSubmitSuite` (the SPARK-45762 user-jar `ShuffleManager` plugin, now implementing `BlockingShuffleManager`), and `ShuffleDependencySuite` (which also asserts a `PipelinedShuffleDependency` never allows push-based shuffle merge). ``` build/sbt 'core/testOnly org.apache.spark.shuffle.PipelinedShuffleRoutingSuite' ... Tests: succeeded 16, failed 0 ``` ### Was this patch authored or co-authored using generative AI tooling? Co-authored: Claude Code (Opus 4.8) Closes #57286 from jerrypeng/stack/pipelined-shuffle-pr2-routing. Authored-by: Boyang Jerry Peng <jerry.peng@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
1 parent 2608b6b commit c15aa12

30 files changed

Lines changed: 878 additions & 158 deletions

core/src/main/scala/org/apache/spark/Dependency.scala

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
134134
// is enabled
135135
private[this] var _shuffleMergeAllowed = canShuffleMergeBeEnabled()
136136

137-
val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
137+
// Route to the manager for this dependency's type (the incremental manager for a
138+
// PipelinedShuffleDependency, the default manager otherwise). The handle is minted once here on
139+
// the driver, so executors that later read it are served by the same manager.
140+
val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManagerFor(this).registerShuffle(
138141
shuffleId, this)
139142

140143
private[spark] def setShuffleMergeAllowed(shuffleMergeAllowed: Boolean): Unit = {
@@ -258,6 +261,74 @@ class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
258261
}
259262

260263

264+
/**
265+
* :: DeveloperApi ::
266+
* A [[ShuffleDependency]] whose output can be read incrementally: a consumer stage may begin
267+
* reading the shuffle output while the producer stage is still running, rather than waiting for the
268+
* producer's full, materialized output.
269+
*
270+
* This is a subtype of [[ShuffleDependency]] -- and thus, like it, a first-class dependency kind
271+
* alongside [[NarrowDependency]] under [[Dependency]]. It is intended to be the marker the
272+
* `DAGScheduler` will use to decide that the producer and consumer
273+
* stages connected by this edge may run concurrently (a "pipelined group"), and that the shuffle
274+
* layer should serve this shuffle with an incremental shuffle implementation. A plain
275+
* [[ShuffleDependency]] keeps the existing semantics: its output is fully materialized before any
276+
* consumer reads it.
277+
*
278+
* Two behaviors are active from construction, driven by this type: shuffle registration routes to
279+
* the incremental (pipelined) shuffle manager via `SparkEnv.shuffleManagerFor` (a plain
280+
* [[ShuffleDependency]] goes to the blocking manager), and push-based shuffle merge is
281+
* unconditionally disabled (`setShuffleMergeAllowed(false)`; see below) because merge exposes
282+
* output only after a post-completion finalize step and would register merge results for a
283+
* transient shuffle. Beyond those, the concurrent scheduling behavior is added separately by the
284+
* `DAGScheduler` components that match on this type; code that only matches the parent
285+
* `ShuffleDependency` still treats it as an ordinary (materialized) shuffle for everything else.
286+
*
287+
* The name is *pipelined* rather than *streaming*: reading producer output as it is produced is a
288+
* general execution capability (software-pipelining of dependent stages), not specific to
289+
* streaming. Streaming / real-time mode is the first caller, but nothing here is
290+
* streaming-specific.
291+
*
292+
* Note: the parent's `checksumMismatchFullRetryEnabled` /
293+
* `checksumMismatchQueryLevelRollbackEnabled` parameters are intentionally not exposed here, so
294+
* they stay at their `false` defaults for a pipelined shuffle. Their checksum retry / query-level
295+
* rollback recomputes and re-runs succeeding stages after a mismatch; in a pipelined group any
296+
* failure aborts the whole group and the caller reruns from scratch, so that stage-level recompute
297+
* never fires -- the mechanism is moot by construction (it is also incompatible with a consumer
298+
* that has already read the output incrementally). Leaving the params unset keeps the idiom
299+
* unreachable.
300+
*/
301+
@DeveloperApi
302+
class PipelinedShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
303+
_rdd: RDD[_ <: Product2[K, V]],
304+
partitioner: Partitioner,
305+
serializer: Serializer = SparkEnv.get.serializer,
306+
keyOrdering: Option[Ordering[K]] = None,
307+
aggregator: Option[Aggregator[K, V, C]] = None,
308+
mapSideCombine: Boolean = false,
309+
shuffleWriterProcessor: ShuffleWriteProcessor = new ShuffleWriteProcessor,
310+
rowBasedChecksums: Array[RowBasedChecksum] = ShuffleDependency.EMPTY_ROW_BASED_CHECKSUMS)
311+
extends ShuffleDependency[K, V, C](
312+
_rdd,
313+
partitioner,
314+
serializer,
315+
keyOrdering,
316+
aggregator,
317+
mapSideCombine,
318+
shuffleWriterProcessor,
319+
rowBasedChecksums) {
320+
321+
// Push-based shuffle merge is incompatible with a pipelined (incrementally-readable) shuffle: it
322+
// exposes output only after a post-completion "finalize" step, the opposite of incremental reads,
323+
// and would register merge results in the MapOutputTracker for a transient shuffle that must not
324+
// outlive its group. ShuffleDependency enables merge by default whenever push-based shuffle is on
325+
// cluster-wide; disable it here so a pipelined producer never acquires merger locations. Without
326+
// this, ShuffleWriteProcessor.write would reach the push path and dereference the incremental
327+
// manager's shuffleBlockResolver, which the streaming manager does not support.
328+
setShuffleMergeAllowed(false)
329+
}
330+
331+
261332
/**
262333
* :: DeveloperApi ::
263334
* Represents a one-to-one dependency between partitions of the parent and child RDDs.

core/src/main/scala/org/apache/spark/SparkEnv.scala

Lines changed: 174 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import org.apache.spark.scheduler.{LiveListenerBus, OutputCommitCoordinator}
4545
import org.apache.spark.scheduler.OutputCommitCoordinator.OutputCommitCoordinatorEndpoint
4646
import org.apache.spark.security.CryptoStreamUtils
4747
import org.apache.spark.serializer.{JavaSerializer, Serializer, SerializerManager}
48-
import org.apache.spark.shuffle.ShuffleManager
48+
import org.apache.spark.shuffle.{BlockingShuffleManager, PipelinedShuffleManager}
49+
import org.apache.spark.shuffle.{ShuffleBlockResolver, ShuffleManager}
4950
import org.apache.spark.shuffle.streaming.{MultiShuffleManager, StreamingShuffleManager}
5051
import org.apache.spark.storage._
5152
import org.apache.spark.udf.worker.UDFWorkerSpecification
@@ -76,15 +77,105 @@ class SparkEnv (
7677
val outputCommitCoordinator: OutputCommitCoordinator,
7778
val conf: SparkConf) extends Logging {
7879

79-
// We initialize the ShuffleManager later in SparkContext and Executor to allow
80-
// user jars to define custom ShuffleManagers.
81-
@volatile private var _shuffleManager: ShuffleManager = _
80+
// The two shuffle managers are peers keyed by kind, not a default and an override: a shuffle is
81+
// routed to one or the other by its dependency type via `shuffleManagerFor`, so neither is ever
82+
// installed "behind" the other.
83+
//
84+
// The blocking manager (spark.shuffle.manager) serves all regular, materialized shuffles and owns
85+
// block-by-id resolution. It is always a BlockingShuffleManager -- initializeShuffleManager
86+
// rejects a non-blocking manager in this slot -- so block-resolution has a single, well-typed
87+
// source. We initialize it later in SparkContext and Executor to allow user jars to define custom
88+
// ShuffleManagers.
89+
@volatile private var _blockingShuffleManager: BlockingShuffleManager = _
90+
91+
// The pipelined manager (spark.shuffle.manager.incremental) serves pipelined
92+
// (incrementally-readable) shuffle dependencies. It defaults to the built-in
93+
// StreamingShuffleManager -- just as the blocking manager defaults to sort -- and is always a
94+
// PipelinedShuffleManager (initializeShuffleManager rejects a blocking manager in this slot), so
95+
// its output is never expected to be reachable through the block-manager resolver. Like the
96+
// blocking manager, it is initialized later in SparkContext and Executor to allow user jars.
97+
@volatile private var _pipelinedShuffleManager: PipelinedShuffleManager = _
8298

8399
// Latch to signal when the ShuffleManager has been initialized.
84100
// Used to allow callers to wait for initialization.
85101
private val shuffleManagerInitLatch = new CountDownLatch(1)
86102

87-
def shuffleManager: ShuffleManager = _shuffleManager
103+
/**
104+
* The `BlockingShuffleManager` (configured by spark.shuffle.manager), which serves all regular,
105+
* materialized shuffle dependencies and owns block-by-id resolution. Use this when the intent is
106+
* specifically the blocking manager -- e.g. inspecting its concrete type. To serve a specific
107+
* shuffle's reads/writes, use `shuffleManagerFor`, which routes by dependency type; to resolve a
108+
* block by id, use `shuffleBlockResolver`.
109+
*/
110+
private[spark] def blockingShuffleManager: BlockingShuffleManager = _blockingShuffleManager
111+
112+
/**
113+
* The `PipelinedShuffleManager` (configured by spark.shuffle.manager.incremental, defaulting to
114+
* the built-in streaming manager) that serves pipelined shuffle dependencies. A pipelined shuffle
115+
* is read incrementally and served out-of-band, so this manager never provides a
116+
* `ShuffleBlockResolver` of its own.
117+
*/
118+
private[spark] def pipelinedShuffleManager: PipelinedShuffleManager = _pipelinedShuffleManager
119+
120+
/**
121+
* The `ShuffleBlockResolver` used to resolve shuffle blocks by id (reads, push-merge, and
122+
* decommission migration), or `None` before the shuffle manager is initialized. Block resolution
123+
* is only ever needed for regular, materialized shuffles, so it always comes from the blocking
124+
* manager (the only manager that produces block-manager-addressed blocks); a pipelined shuffle is
125+
* served out-of-band and never resolved here. Callers that only need to resolve a block should
126+
* use this rather than reaching through a `ShuffleManager`.
127+
*/
128+
private[spark] def shuffleBlockResolver: Option[ShuffleBlockResolver] =
129+
Option(_blockingShuffleManager).map(_.shuffleBlockResolver)
130+
131+
/**
132+
* Retained for binary compatibility; returns the blocking manager. Prefer `shuffleManagerFor`
133+
* (route a shuffle by its dependency) or `blockingShuffleManager` (the blocking manager
134+
* explicitly), so the routing decision is explicit at the call site.
135+
*/
136+
@deprecated("use shuffleManagerFor(dependency) to route a shuffle by type, or " +
137+
"blockingShuffleManager for the blocking manager explicitly", "4.3.0")
138+
def shuffleManager: ShuffleManager = _blockingShuffleManager
139+
140+
/**
141+
* The `ShuffleManager` that serves the given shuffle, chosen by dependency type: a
142+
* [[PipelinedShuffleDependency]] is served by the pipelined manager
143+
* (spark.shuffle.manager.incremental, defaulting to the built-in streaming manager), every other
144+
* [[ShuffleDependency]] by the blocking manager (spark.shuffle.manager). This is the single
145+
* routing point for shuffle I/O; the decision is a pure function of the dependency, so the driver
146+
* (at `registerShuffle`) and every executor (at `getWriter` / `getReader`) agree without any
147+
* shared routing state or config re-read -- the dependency (a serialized field, deterministic on
148+
* driver and executors) is always in hand at these sites, so no shuffleId -> manager tracking is
149+
* needed.
150+
*
151+
* Whether a job may use a pipelined dependency at all is a separate, scheduler-level decision
152+
* (see the fail-fast checks in `DAGScheduler`); this method only picks the implementation.
153+
*/
154+
private[spark] def shuffleManagerFor(dependency: ShuffleDependency[_, _, _]): ShuffleManager =
155+
dependency match {
156+
case _: PipelinedShuffleDependency[_, _, _] =>
157+
_pipelinedShuffleManager
158+
case _ => _blockingShuffleManager
159+
}
160+
161+
/**
162+
* Unregisters the shuffle from every configured manager (default and, if present, incremental).
163+
* Used by the `RemoveShuffle` cleanup path, which holds only a shuffleId and cannot know which
164+
* manager owns it -- and must reach the owning manager on every node, including one that never
165+
* performed this shuffle's I/O. Notifying all managers is safe: `unregisterShuffle` for an
166+
* unknown id is a no-op. Returns true if any manager reports it removed metadata; false if no
167+
* manager is initialized yet (a RemoveShuffle can arrive before the deferred init runs).
168+
*/
169+
private[spark] def unregisterShuffleFromAllManagers(shuffleId: Int): Boolean = {
170+
// Both managers are null until initializeShuffleManager runs (deferred to allow user jars), so
171+
// guard them; a RemoveShuffle before init is a no-op, as it was before routing.
172+
val blockingResult =
173+
_blockingShuffleManager != null && _blockingShuffleManager.unregisterShuffle(shuffleId)
174+
// OR the pipelined result in without short-circuiting, so both are always notified.
175+
val pipelinedResult =
176+
_pipelinedShuffleManager != null && _pipelinedShuffleManager.unregisterShuffle(shuffleId)
177+
blockingResult || pipelinedResult
178+
}
88179

89180
/**
90181
* Wait for the ShuffleManager to be initialized within the specified timeout.
@@ -102,7 +193,7 @@ class SparkEnv (
102193
* @return true if the ShuffleManager is initialized, false otherwise
103194
*/
104195
private[spark] def isShuffleManagerInitialized: Boolean = {
105-
_shuffleManager != null
196+
_blockingShuffleManager != null
106197
}
107198

108199
// We initialize the MemoryManager later in SparkContext after DriverPlugin is loaded
@@ -185,8 +276,11 @@ class SparkEnv (
185276
udfDispatcherManager.foreach(_.close())
186277
mapOutputTracker.stop()
187278
_streamingShuffleOutputTracker.foreach(_.stop())
188-
if (shuffleManager != null) {
189-
shuffleManager.stop()
279+
if (_blockingShuffleManager != null) {
280+
_blockingShuffleManager.stop()
281+
}
282+
if (_pipelinedShuffleManager != null) {
283+
_pipelinedShuffleManager.stop()
190284
}
191285
broadcastManager.stop()
192286
blockManager.stop()
@@ -295,19 +389,52 @@ class SparkEnv (
295389
}
296390

297391
private[spark] def initializeShuffleManager(): Unit = {
298-
Preconditions.checkState(null == _shuffleManager,
299-
"Shuffle manager already initialized to %s", _shuffleManager)
392+
Preconditions.checkState(null == _blockingShuffleManager,
393+
"Shuffle manager already initialized to %s", _blockingShuffleManager)
300394
try {
301-
_shuffleManager = ShuffleManager.create(conf, SparkContext.isDriver(executorId))
395+
val isDriver = SparkContext.isDriver(executorId)
396+
// The blocking manager (spark.shuffle.manager) serves all regular, materialized shuffle
397+
// dependencies and owns block-by-id resolution, so it must be a BlockingShuffleManager. A
398+
// manager that declares neither kind, or a pipelined-only one, is rejected here rather than
399+
// silently failing block resolution later.
400+
_blockingShuffleManager = ShuffleManager.create(conf, isDriver) match {
401+
case blocking: BlockingShuffleManager => blocking
402+
case other =>
403+
throw new IllegalArgumentException(
404+
s"${config.SHUFFLE_MANAGER.key} must be a BlockingShuffleManager, but " +
405+
s"${other.getClass.getName} is not. A blocking manager serves regular, " +
406+
s"materialized shuffles and resolves blocks by id; configure a pipelined manager " +
407+
s"via ${config.SHUFFLE_MANAGER_INCREMENTAL.key} instead.")
408+
}
409+
// The pipelined manager (spark.shuffle.manager.incremental) serves pipelined shuffle
410+
// dependencies. It is a peer of the blocking manager -- see `shuffleManagerFor` -- not a
411+
// wrapper installed as the top-level manager, and is created the same way: it defaults to the
412+
// built-in streaming manager (just as the blocking manager defaults to sort) and must be a
413+
// PipelinedShuffleManager, since its output is read incrementally and served out-of-band and
414+
// so produces no block-manager blocks.
415+
_pipelinedShuffleManager = Utils.instantiateSerializerOrShuffleManager[ShuffleManager](
416+
// Resolve short aliases ("sort", "tungsten-sort", "streaming") the same way the blocking
417+
// manager does, so spark.shuffle.manager.incremental accepts the same values.
418+
ShuffleManager.resolveShortName(conf.get(config.SHUFFLE_MANAGER_INCREMENTAL)),
419+
conf, isDriver) match {
420+
case pipelined: PipelinedShuffleManager => pipelined
421+
case other =>
422+
throw new IllegalArgumentException(
423+
s"${config.SHUFFLE_MANAGER_INCREMENTAL.key} must be a PipelinedShuffleManager, but " +
424+
s"${other.getClass.getName} is not. Only a pipelined manager (output read " +
425+
s"incrementally and served out-of-band) belongs in this slot; a blocking manager " +
426+
s"belongs in ${config.SHUFFLE_MANAGER.key}.")
427+
}
302428
} finally {
303429
// Signal that the ShuffleManager has been initialized
304430
shuffleManagerInitLatch.countDown()
305431
}
306432
initializeStreamingShuffleOutputTracker()
307433
}
308434

309-
// Holds the streaming shuffle output tracker, which is only present when the configured
310-
// shuffle manager requires it (i.e., StreamingShuffleManager or MultiShuffleManager).
435+
// Holds the streaming shuffle output tracker, which is only present when the configured shuffle
436+
// managers require it (i.e., a StreamingShuffleManager as the pipelined manager, or a
437+
// MultiShuffleManager as the blocking manager).
311438
@volatile private var _streamingShuffleOutputTracker: Option[StreamingShuffleOutputTracker] =
312439
None
313440

@@ -323,28 +450,42 @@ class SparkEnv (
323450
return
324451
}
325452

326-
val shuffleManagerName = ShuffleManager.getShuffleManagerClassName(conf)
327-
if (shuffleManagerName == classOf[StreamingShuffleManager].getName
328-
|| shuffleManagerName == classOf[MultiShuffleManager].getName) {
329-
val tracker = if (SparkContext.isDriver(executorId)) {
330-
new StreamingShuffleOutputTrackerMaster(conf)
331-
} else {
332-
new StreamingShuffleOutputTrackerWorker(conf)
333-
}
453+
// The tracker is needed when the pipelined manager (spark.shuffle.manager.incremental) is a
454+
// StreamingShuffleManager -- which is the default. Inspect the already-instantiated manager
455+
// rather than re-reading the config; this runs at the end of initializeShuffleManager, so the
456+
// manager is non-null here.
457+
val incrementalIsStreaming =
458+
_pipelinedShuffleManager.isInstanceOf[StreamingShuffleManager]
459+
// It is also needed when a MultiShuffleManager is the blocking manager (spark.shuffle.manager):
460+
// it internally routes some shuffles to streaming. A bare StreamingShuffleManager cannot be the
461+
// blocking manager -- it is pipelined and rejected from that slot in initializeShuffleManager.
462+
// TODO: remove this MultiShuffleManager clause once MultiShuffleManager is removed and
463+
// streaming shuffles are served only through the incremental (pipelined) manager slot.
464+
val blockingIsMulti = _blockingShuffleManager.isInstanceOf[MultiShuffleManager]
465+
if (incrementalIsStreaming || blockingIsMulti) {
466+
createStreamingShuffleOutputTracker()
467+
}
468+
}
334469

335-
if (SparkContext.isDriver(executorId)) {
336-
tracker.trackerEndpoint = rpcEnv.setupEndpoint(
337-
StreamingShuffleOutputTracker.ENDPOINT_NAME,
338-
new StreamingShuffleOutputTrackerMasterEndpoint(
339-
rpcEnv,
340-
tracker.asInstanceOf[StreamingShuffleOutputTrackerMaster],
341-
conf))
342-
} else {
343-
tracker.trackerEndpoint = RpcUtils.makeDriverRef(
344-
StreamingShuffleOutputTracker.ENDPOINT_NAME, conf, rpcEnv)
345-
}
346-
_streamingShuffleOutputTracker = Some(tracker)
470+
private def createStreamingShuffleOutputTracker(): Unit = {
471+
val tracker = if (SparkContext.isDriver(executorId)) {
472+
new StreamingShuffleOutputTrackerMaster(conf)
473+
} else {
474+
new StreamingShuffleOutputTrackerWorker(conf)
475+
}
476+
477+
if (SparkContext.isDriver(executorId)) {
478+
tracker.trackerEndpoint = rpcEnv.setupEndpoint(
479+
StreamingShuffleOutputTracker.ENDPOINT_NAME,
480+
new StreamingShuffleOutputTrackerMasterEndpoint(
481+
rpcEnv,
482+
tracker.asInstanceOf[StreamingShuffleOutputTrackerMaster],
483+
conf))
484+
} else {
485+
tracker.trackerEndpoint = RpcUtils.makeDriverRef(
486+
StreamingShuffleOutputTracker.ENDPOINT_NAME, conf, rpcEnv)
347487
}
488+
_streamingShuffleOutputTracker = Some(tracker)
348489
}
349490

350491
private[spark] def initializeMemoryManager(
@@ -550,7 +691,6 @@ object SparkEnv extends Logging {
550691
None
551692
}, blockManagerInfo,
552693
mapOutputTracker.asInstanceOf[MapOutputTrackerMaster],
553-
_shuffleManager = null,
554694
isDriver)),
555695
registerOrLookupEndpoint(
556696
BlockManagerMaster.DRIVER_HEARTBEAT_ENDPOINT_NAME,

0 commit comments

Comments
 (0)