146 lines
5.4 KiB
Markdown
146 lines
5.4 KiB
Markdown
# UNDF: UNDF-2026-000000534
|
|
# spark-0002 — DAGScheduler BFS queues: ListBuffer.remove(0) is O(N) → O(N²) total
|
|
|
|
**Severity:** HIGH
|
|
**CWE:** CWE-407 (Algorithmic Complexity — Inefficient Algorithmic Complexity)
|
|
**File:** `core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala`
|
|
**Introduced:** Long-standing; present in all recent Spark versions
|
|
|
|
## Defect
|
|
|
|
Six BFS-style graph traversal functions in `DAGScheduler` use `scala.collection.mutable.ListBuffer`
|
|
as the work queue and dequeue with `waitingForVisit.remove(0)`.
|
|
|
|
`ListBuffer.remove(0)` is **O(N)** — it shifts every remaining element left by one position.
|
|
Called inside a `while (waitingForVisit.nonEmpty)` loop over N RDD nodes, this makes the overall
|
|
traversal **O(N²)** instead of O(N).
|
|
|
|
For a DAG with hundreds of RDD partitions or deep lineage chains (e.g. iterative ML workloads,
|
|
complex SQL plans), this is the dominant scheduling cost.
|
|
|
|
### Affected functions and exact lines (Spark master, 2026-03)
|
|
|
|
| Function | `waitingForVisit` init | `remove(0)` line |
|
|
|---|---|---|
|
|
| `getMissingAncestorShuffleDependencies` | 694 | 696 |
|
|
| `getShuffleDependenciesAndResourceProfiles` | 730 | 732 |
|
|
| `traverseParentRDDsWithinStage` | 754 | 756 |
|
|
| `getMissingParentStages` | 779 | 816 |
|
|
| `eagerlyComputePartitionsForRddAndAncestors` | 828 | 844 |
|
|
| `stageDependsOn` | 3381 | 3399 |
|
|
|
|
### Code pattern (repeated 6 times)
|
|
|
|
```scala
|
|
// BEFORE — O(N²) BFS
|
|
val waitingForVisit = new ListBuffer[RDD[_]]
|
|
waitingForVisit += rdd
|
|
while (waitingForVisit.nonEmpty) {
|
|
val toVisit = waitingForVisit.remove(0) // ← O(N) shift every iteration
|
|
...
|
|
waitingForVisit.prepend(dependency.rdd) // O(1) prepend, but dequeue dominates
|
|
}
|
|
```
|
|
|
|
## Fix
|
|
|
|
Replace `ListBuffer` with `scala.collection.mutable.ArrayDeque`, which provides
|
|
**O(1) amortized** prepend (`prepend`) and dequeue (`removeHead()`).
|
|
|
|
```scala
|
|
// AFTER — O(N) BFS
|
|
val waitingForVisit = new mutable.ArrayDeque[RDD[_]]()
|
|
waitingForVisit += rdd
|
|
while (waitingForVisit.nonEmpty) {
|
|
val toVisit = waitingForVisit.removeHead() // ← O(1)
|
|
...
|
|
waitingForVisit.prepend(dependency.rdd) // ← O(1)
|
|
}
|
|
```
|
|
|
|
`ArrayDeque` was added to the Scala standard library in 2.13. Spark already targets Scala 2.13+,
|
|
so no new dependency is introduced.
|
|
|
|
The `ListBuffer` import can be removed from `DAGScheduler.scala` once all six sites are migrated
|
|
(it is not used for any other purpose in the file).
|
|
|
|
## Patch
|
|
|
|
```diff
|
|
--- a/core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala
|
|
+++ b/core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala
|
|
@@ -29,7 +29,7 @@ import scala.collection.mutable
|
|
-import scala.collection.mutable.{HashMap, HashSet, ListBuffer}
|
|
+import scala.collection.mutable.{ArrayDeque, HashMap, HashSet}
|
|
|
|
// getMissingAncestorShuffleDependencies (~line 688)
|
|
- val waitingForVisit = new ListBuffer[RDD[_]]
|
|
+ val waitingForVisit = new ArrayDeque[RDD[_]]()
|
|
waitingForVisit += rdd
|
|
while (waitingForVisit.nonEmpty) {
|
|
- val toVisit = waitingForVisit.remove(0)
|
|
+ val toVisit = waitingForVisit.removeHead()
|
|
|
|
// getShuffleDependenciesAndResourceProfiles (~line 724)
|
|
- val waitingForVisit = new ListBuffer[RDD[_]]
|
|
+ val waitingForVisit = new ArrayDeque[RDD[_]]()
|
|
waitingForVisit += rdd
|
|
while (waitingForVisit.nonEmpty) {
|
|
- val toVisit = waitingForVisit.remove(0)
|
|
+ val toVisit = waitingForVisit.removeHead()
|
|
|
|
// traverseParentRDDsWithinStage (~line 749)
|
|
- val waitingForVisit = new ListBuffer[RDD[_]]
|
|
+ val waitingForVisit = new ArrayDeque[RDD[_]]()
|
|
waitingForVisit += rdd
|
|
while (waitingForVisit.nonEmpty) {
|
|
- val toVisit = waitingForVisit.remove(0)
|
|
+ val toVisit = waitingForVisit.removeHead()
|
|
|
|
// getMissingParentStages (~line 779)
|
|
- val waitingForVisit = new ListBuffer[RDD[_]]
|
|
+ val waitingForVisit = new ArrayDeque[RDD[_]]()
|
|
waitingForVisit += stage.rdd
|
|
// ... (visit function defined inline)
|
|
while (waitingForVisit.nonEmpty) {
|
|
- visit(waitingForVisit.remove(0))
|
|
+ visit(waitingForVisit.removeHead())
|
|
|
|
// eagerlyComputePartitionsForRddAndAncestors (~line 828)
|
|
- val waitingForVisit = new ListBuffer[RDD[_]]
|
|
+ val waitingForVisit = new ArrayDeque[RDD[_]]()
|
|
waitingForVisit += rdd
|
|
// ... (visit function defined inline)
|
|
while (waitingForVisit.nonEmpty) {
|
|
- visit(waitingForVisit.remove(0))
|
|
+ visit(waitingForVisit.removeHead())
|
|
|
|
// stageDependsOn (~line 3377)
|
|
- val waitingForVisit = new ListBuffer[RDD[_]]
|
|
+ val waitingForVisit = new ArrayDeque[RDD[_]]()
|
|
waitingForVisit += stage.rdd
|
|
// ... (visit function defined inline)
|
|
while (waitingForVisit.nonEmpty) {
|
|
- visit(waitingForVisit.remove(0))
|
|
+ visit(waitingForVisit.removeHead())
|
|
```
|
|
|
|
## Complexity
|
|
|
|
| Metric | Before | After |
|
|
|---|---|---|
|
|
| BFS over N RDD nodes | O(N²) | O(N) |
|
|
| `remove(0)` / `removeHead()` | O(N) per call | O(1) amortized |
|
|
| Memory | O(N) | O(N) |
|
|
|
|
## Impact
|
|
|
|
- Affects every Spark job submission: `getMissingParentStages` is called on every `submitStage`
|
|
- `stageDependsOn` called on stage resubmission after failure — worst case is already degraded
|
|
- Iterative ML pipelines (Spark MLlib) with deep RDD lineage chains: `eagerlyComputePartitionsForRddAndAncestors` called per action
|
|
- Complex SQL joins: `getShuffleDependenciesAndResourceProfiles` called per query plan
|
|
|
|
## Test
|
|
|
|
`defects/spark/unit/SparkDAGSchedulerTest.java` — runs slow (ListBuffer simulation) vs fast
|
|
(ArrayDeque simulation), verifies O(N²) vs O(N) operation counts.
|