[SPARK-58505][CORE] Attach a bounded per-consumer memory breakdown to UNABLE_TO_ACQUIRE_MEMORY errors - #57708
[SPARK-58505][CORE] Attach a bounded per-consumer memory breakdown to UNABLE_TO_ACQUIRE_MEMORY errors#57708ganeshashree wants to merge 3 commits into
Conversation
… UNABLE_TO_ACQUIRE_MEMORY errors
### What changes were proposed in this pull request?
When a task fails to acquire execution memory, the `UNABLE_TO_ACQUIRE_MEMORY`
error only reported the requested and received byte counts. The per-consumer
breakdown that `TaskMemoryManager.showMemoryUsage()` computes was written solely
to the executor logs, so it did not travel with the task failure reason to the
driver or the Spark UI.
This change attaches that per-consumer attribution to the error message itself:
- New `TaskMemoryManager.getMemoryConsumptionBreakdown()` returns a compact,
largest-consumer-first breakdown string (empty when no consumer holds memory).
- Both `showMemoryUsage()` and the new method derive their output from a single
`snapshotMemoryUsage()` taken under the manager's monitor, so the log dump and
the error message can never disagree and the sort cannot trip TimSort's
comparison-contract check on the failure path.
- The breakdown embedded in the error is bounded by the new internal config
`spark.memory.oomErrorConsumerBreakdownLimit` (default 5): the largest
consumers are listed individually, the rest are collapsed into a single
summary line, and 0 omits the breakdown from the error entirely. The full,
uncapped breakdown is still written to the executor logs.
- A `consumerBreakdown` parameter is added to the `UNABLE_TO_ACQUIRE_MEMORY`
message template and threaded through both construction sites
(`SparkCoreErrors.outOfMemoryError` and
`QueryExecutionErrors.cannotAcquireMemoryForWindowAggregateError`).
### Why are the changes needed?
OOM debugging is a top operational pain point. Recovering the per-consumer
breakdown from executor logs after a failure is often impractical (rotated /
aggregated logs, lost executor) and impossible for the driver or for automated
diagnosis. Surfacing which consumers were competing for memory at the moment of
failure, directly in the error, makes these failures far easier to diagnose.
### Does this PR introduce any user-facing change?
Yes. The `UNABLE_TO_ACQUIRE_MEMORY` error message now includes a per-consumer
memory breakdown, for example:
Unable to acquire 8388608 bytes of memory, got 2097152.
Memory used by task 4211 grouped by consumer:
org.apache.spark...UnsafeExternalSorter@1a2b: 456.0 MiB
org.apache.spark...BytesToBytesMap@3c4d: 12.0 MiB
(37 more consumers): 44.0 MiB
(not attributed to a specific consumer): 3.0 MiB
The breakdown exposes only consumer class names, identity hashes, and aggregate
byte counts -- the same data already written to the executor logs.
### How was this patch tested?
Added unit tests in `TaskMemoryManagerSuite` covering ordering, the cap and
summary line, the limit-0 path, unattributed memory, the empty case, and
end-to-end propagation into the thrown `SparkOutOfMemoryError`. Existing
`ShuffleExternalSorterSuite`, `WindowSegmentTreeMemorySuite`, and
`SparkThrowableSuite` continue to pass.
Co-authored-by: Isaac
uros-b
left a comment
There was a problem hiding this comment.
Left just one comment, otherwise looks good - thank you @ganeshashree!
Address review feedback: mark spark.memory.oomErrorConsumerBreakdownLimit with ConfigBindingPolicy.NOT_APPLICABLE, since it controls only diagnostic error-message formatting and has no session-binding semantics.
cloud-fan
left a comment
There was a problem hiding this comment.
0 blocking, 0 non-blocking, 1 nit.
The implementation is coherent and well covered; one inaccurate Javadoc sentence should be corrected.
Nits: 1 minor item (see inline comments).
Verification
I traced both OOM construction paths through the shared renderer and checked the config's default, non-negative validation, zero sentinel, bounded summary, and error-parameter propagation. The focused tests cover ordering, truncation, unattributed memory, omission at zero, and the generic allocation failure path.
… report The javadoc on `getMemoryConsumptionBreakdown()` said the result is empty "when no consumer is holding memory", but `renderConsumerBreakdown()` emits the `(not attributed to a specific consumer)` line whenever the task has unattributed memory, whether or not any consumer holds memory. Restate the contract as the empty case being neither attributed nor unattributed memory to report, and say explicitly that unattributed-only memory still yields a breakdown. Behavior is unchanged; the test that covers the empty case now also asserts the task holds no execution memory at all.
|
@ganeshashree can you make the CI happy? |
Created this PR to self-heal the PR build check when the notify workflow fails to create it. |
What changes were proposed in this pull request?
When a task fails to acquire execution memory, the
UNABLE_TO_ACQUIRE_MEMORYerrorpreviously reported only the requested and received byte counts:
The per-consumer breakdown that
TaskMemoryManageralready computes was written only to the executor logs (showMemoryUsage()), so it did not travel with the task failure reason to the driver or the Spark UI.This PR attaches that per-consumer attribution to the error message itself:
TaskMemoryManager.getMemoryConsumptionBreakdown()renders a compact, largest-consumer-first breakdown (empty when no consumer holds memory).snapshotMemoryUsage()taken under the manager's monitor. On the OOM path,MemoryConsumer.throwOom()calls the newlogMemoryUsageAndGetBreakdown(), which snapshots once and produces both the full executor-log dump and the bounded error breakdown from that one snapshot, so the two can never disagree. Snapshotting under the lock (rather than sorting livegetUsed()values) also avoids trippingTimSort's "Comparison method violates its general contract!" check on the failure path.spark.memory.oomErrorConsumerBreakdownLimit(default 5): the largest consumers are listed individually, the remainder are collapsed into a single summary line that preserves total byte accounting, and0omits the breakdown from the error entirely. The full, uncapped breakdown is still written to the executor logs.consumerBreakdownparameter is added to theUNABLE_TO_ACQUIRE_MEMORYmessage template and threaded through both construction sites(
SparkCoreErrors.outOfMemoryErrorandQueryExecutionErrors.cannotAcquireMemoryForWindowAggregateError).Why are the changes needed?
OOM debugging is a top operational pain point. When a task dies with
UNABLE_TO_ACQUIRE_MEMORY, the byte counts alone give no signal about which operator was holding the memory. The information exists, but only in the executor logs, which are often impractical to recover after a failure (rotated / aggregated logs, lost executor) and are not accessible to the driver or to automated / agent-based diagnosis. Surfacing which consumers were competing for memory at the moment of failure, directly in the error that propagates to the driver and the UI, makes these failures far easier to diagnose.Does this PR introduce any user-facing change?
Yes. The
UNABLE_TO_ACQUIRE_MEMORYerror message now includes a bounded per-consumermemory breakdown. For example:
The breakdown exposes only consumer class names, identity hashes, and aggregate byte counts
-- the same data already written to the executor logs; no keys, values, row data, or paths.
This is a change relative to released Spark versions. The new config
spark.memory.oomErrorConsumerBreakdownLimitis internal.How was this patch tested?
New unit tests in
TaskMemoryManagerSuitecover:SparkOutOfMemoryError, andlogMemoryUsageAndGetBreakdown()returning the same breakdown as a standalone render.Existing
ShuffleExternalSorterSuite,WindowSegmentTreeMemorySuite, andSparkThrowableSuitecontinue to pass (the last confirms the error-conditions JSON stays well-formed and round-trips).Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)