This is an automated email from the ASF dual-hosted git repository.
peter-toth pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 5915191b4dd6 [SPARK-58323][SQL] Materialize AliasAware output ordering
and partitioning to avoid StackOverflowError
5915191b4dd6 is described below
commit 5915191b4dd6405a4c846e742b2b17cdab41bcbb
Author: Peter Toth <[email protected]>
AuthorDate: Sat Jul 25 11:22:36 2026 +0200
[SPARK-58323][SQL] Materialize AliasAware output ordering and partitioning
to avoid StackOverflowError
`AliasAwareQueryOutputOrdering.outputOrdering` and
`PartitioningPreservingUnaryExecNode.outputPartitioning` (and
`BroadcastHashJoinExec`'s output-partitioning expansion) build their results
with `multiTransform`, which returns a `LazyList`, and store the bounded result
unforced. This forces them into strict collections with `.toList`, right after
the existing `.take(...)` so the lazy short-circuit during candidate generation
is preserved.
Note: for `BroadcastHashJoinExec`, using a strict `List` also makes the
existing `case p :: Nil => p` unwrap in `outputPartitioning` reachable — so a
single-element expansion now returns the `HashPartitioning` directly instead of
a one-element `PartitioningCollection` (that branch was previously dead because
the expansion was an unforced `LazyList`, which a `List`-cons pattern never
matches). This is the cleaner, intended form; the corresponding
`BroadcastJoinSuite` assertion is updat [...]
Each plan node re-wraps the child ordering/partitioning's unforced
`LazyList`, so across a deep projection chain the deferred nesting grows
unbounded and overflows the stack when the ordering/partitioning is later
serialized or deeply traversed. Concretely, task serialization of a
`SortedMergeCoalescedRDD` (SPARK-55715) that captures the child
`outputOrdering` was observed to fail on the driver with a `StackOverflowError`
at `DAGScheduler.submitMissingTasks` on a large bucketed `MERGE [...]
No.
New test in `ProjectedOrderingAndPartitioningSuite` asserting
`sameOrderExpressions` and the projected `PartitioningCollection.partitionings`
are strict (not `LazyList`); it fails before this change (they were `LazyList`)
and passes after. `BroadcastJoinSuite` / `BroadcastJoinSuiteAE` updated for the
single-element output-partitioning form and pass. Existing
`ProjectedOrderingAndPartitioningSuite` / `PlannerSuite` /
`EnsureRequirementsSuite` pass.
Generated-by: Claude Opus 4.8
Closes #57502 from
peter-toth/SPARK-58323-aliasaware-strict-ordering-partitioning.
Authored-by: Peter Toth <[email protected]>
Signed-off-by: Peter Toth <[email protected]>
(cherry picked from commit faa1d1ecad4c64dc528ee83be6eb19bb5291c439)
Signed-off-by: Peter Toth <[email protected]>
---
.../plans/AliasAwareOutputExpression.scala | 6 +++++
.../execution/joins/BroadcastHashJoinExec.scala | 5 +++++
.../ProjectedOrderingAndPartitioningSuite.scala | 26 ++++++++++++++++++++++
.../sql/execution/joins/BroadcastJoinSuite.scala | 8 ++++++-
4 files changed, 44 insertions(+), 1 deletion(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
index e1a9e8b5ea81..e7d2ab8538a4 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
@@ -107,6 +107,12 @@ trait AliasAwareQueryOutputOrdering[T <: QueryPlan[T]]
.flatMap(projectExpression)
.filter(e => orderingSet.add(e.canonicalized))
.take(aliasCandidateLimit)
+ // Materialize the bounded result into a strict collection. The
`LazyList` above is only
+ // needed so `take` can short-circuit candidate generation; storing
`sameOrderExpressions`
+ // as an unforced `LazyList` lets each plan node re-wrap the child
ordering's lazy list,
+ // and across a deep projection chain that nesting overflows the
stack when the ordering
+ // is later serialized or deeply traversed.
+ .toList
if (sameOrderings.nonEmpty) {
Some(sortOrder.copy(child = sameOrderings.head,
sameOrderExpressions = sameOrderings.tail))
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
index e4f18c9144dd..70b5efa3e82f 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
@@ -121,6 +121,11 @@ case class BroadcastHashJoinExec private(
e +: streamedKeyToBuildKeyMapping(e.canonicalized)
}.asInstanceOf[LazyList[Partitioning]]
.take(conf.broadcastHashJoinOutputPartitioningExpandLimit)
+ // Materialize the bounded expansion into a strict `List` -- never
an unforced `LazyList`,
+ // which chained across nodes can overflow the stack when serialized
or deeply traversed.
+ // A `List` also lets the `case p :: Nil` in `outputPartitioning`
unwrap a single-element
+ // expansion to the partitioning itself, not a one-element
`PartitioningCollection`.
+ .toList
case other => Seq(other)
}
}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
index ec13d48d45f8..e67ce5f973ca 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
@@ -84,6 +84,32 @@ class ProjectedOrderingAndPartitioningSuite
}
}
+ test("SPARK-58323: AliasAware output ordering and partitioning are strict,
not lazy") {
+ // A multi-alias projection makes `sameOrderExpressions` and the projected
+ // `PartitioningCollection` non-trivial. Both must be strict collections:
the underlying
+ // `multiTransform` produces a `LazyList`, and storing it unforced lets
each plan node re-wrap
+ // the child's lazy list. Across a deep projection chain that nesting
overflows the stack when
+ // the ordering/partitioning is later serialized or deeply traversed.
+ withSQLConf(SQLConf.EXPRESSION_PROJECTION_CANDIDATE_LIMIT.key -> "5") {
+ // Ordering (AliasAwareQueryOutputOrdering): id -> {x, y, z} gives
sameOrderExpressions.
+ val orderDf = spark.range(2).orderBy($"id").selectExpr("id as x", "id as
y", "id as z")
+ val outputOrdering = orderDf.queryExecution.optimizedPlan.outputOrdering
+ assert(outputOrdering.head.sameOrderExpressions.nonEmpty)
+ outputOrdering.foreach { so =>
+ assert(!so.sameOrderExpressions.isInstanceOf[LazyList[_]],
+ s"sameOrderExpressions must be strict, was
${so.sameOrderExpressions.getClass.getName}")
+ }
+
+ // Partitioning (PartitioningPreservingUnaryExecNode): repartition(id)
-> {x, y, z}.
+ val partDf = spark.range(2).repartition($"id").selectExpr("id as x", "id
as y", "id as z")
+ val outputPartitioning =
stripAQEPlan(partDf.queryExecution.executedPlan).outputPartitioning
+ val partitionings =
outputPartitioning.asInstanceOf[PartitioningCollection].partitionings
+ assert(partitionings.size == 3)
+ assert(!partitionings.isInstanceOf[LazyList[_]],
+ s"partitionings must be strict, was ${partitionings.getClass.getName}")
+ }
+ }
+
test("SPARK-42049: Improve AliasAwareOutputExpression - ordering -
multi-references") {
val df = spark.range(2).selectExpr("id as a", "id as b")
.orderBy($"a" + $"b").selectExpr("a as x", "b as y")
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/joins/BroadcastJoinSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/joins/BroadcastJoinSuite.scala
index 52746720eba1..459919be4f80 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/joins/BroadcastJoinSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/joins/BroadcastJoinSuite.scala
@@ -638,7 +638,13 @@ abstract class BroadcastJoinSuiteBase extends QueryTest
None,
left = DummySparkPlan(outputPartitioning = HashPartitioning(Seq(l1,
l2), 1)),
right = DummySparkPlan())
- assert(bhj.outputPartitioning ===
PartitioningCollection(expected.take(limit)))
+ // A single expanded partitioning is returned directly; multiple are
wrapped in a
+ // `PartitioningCollection`.
+ val expectedPartitioning = expected.take(limit) match {
+ case Seq(p) => p
+ case ps => PartitioningCollection(ps)
+ }
+ assert(bhj.outputPartitioning === expectedPartitioning)
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]