Repository navigation
Conversation
andygrove
left a comment
There was a problem hiding this comment.
This looks like the same fix as #6310 from @dwsmith1983. That PR retries the missing-entry exception in CometTaskMemoryManager.acquireMemory and closes #6224, which is the same bug as #6812. It's approved and its CI is green. When #6403 merged yesterday, its description said #6310 would move both call sites onto one shared helper, and this PR does that move. MissingTaskEntryRetry and the fixtures in TaskMemoryTestUtils are mostly #6403's code moved out of the allocator and its suite.
Could you and @dwsmith1983 agree on which PR carries the fix? If it's this one, could it also close #6224, with #6812 closed as a duplicate, and could the description credit #6310 and #6403?
| * under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark; |
There was a problem hiding this comment.
CI is red on every build job, even though all the suites pass. The jobs fail afterwards in check-jar-contents, because org/apache/spark/MissingTaskEntryRetry.class isn't on the allowlist in dev/ensure-jars-have-correct-contents.sh. The helper doesn't use anything private to Spark, so could it move under org.apache.comet? If it needs to stay in org.apache.spark, the allowlist needs an entry and a reason.
@viirya #6310 has been open since September 28 with CI green on its current head, and #5613 builds on it. I'd land #6310 as it is and have #6813 follow as the change that moves both call sites onto one helper, which #6403 said the second PR would do and #6310 didn't. #6812 would then close as a duplicate of #6224. The other order works too: #6310 takes the move, and #6813 closes. |
comphead
left a comment
There was a problem hiding this comment.
The retry and the zero grant look safe for both native pools from reading the code. The inline comments cover the duplicated race tests, the harness that copies the shuffle allocator suite, and a doc paragraph that a merge with #6310 would silently duplicate.
| } | ||
| } | ||
|
|
||
| test("an acquire waiting in Spark survives a JVM consumer releasing the task's last bytes") { |
There was a problem hiding this comment.
These two race tests reach Spark through the same call. From reading Spark 3.4.3 and 4.1.3, CometTaskMemoryManager.releaseMemory and MemoryConsumer.freeMemory both end in TaskMemoryManager.releaseExecutionMemory against the task id, so I expect the releaser's type not to change what Spark or acquireMemory does. Could we keep one of the two race tests and the refusal test?
| * which removes the task's entry from Spark's pool. Once the other task frees its memory the | ||
| * native request must be granted in full. | ||
| */ | ||
| private def checkWaitingAcquireSurvives( |
There was a problem hiding this comment.
checkWaitingAcquireSurvives is close to a copy of checkWaitingAllocationSurvives in CometUnifiedShuffleMemoryAllocatorSuite. The pool setup, the other task's 90 bytes, the waiting thread, awaitWaitingInSpark, the cleanup in finally and the single INFO event check are the same. Only the holder, the request and the final used checks differ. Since this PR adds TaskMemoryTestUtils to share these fixtures, could the harness move there too, taking the holder's acquire and release and the request as functions?
| granted. While overcommit is outstanding, `try_grow` asks Spark for it on top of the request and | ||
| is refused unless Spark can cover both, so operators spill until the debt is repaid. Both pools' | ||
| `Display` output and their `try_grow` errors report the current overcommit. | ||
| - A request that waits in Spark can lose the task's entry in Spark's execution pool when another |
There was a problem hiding this comment.
A note for whichever of this PR and #6310 lands second. git merge-tree reports conflicts only in CometTaskMemoryManager.java and CometTaskMemoryManagerSuite.scala. memory_management.md merges cleanly and keeps both #6310's "A parked acquire can wake up to a missing task entry" paragraph and this bullet, which describe the same retry. That paragraph also says the allocatePage gap is tracked in #6304, which #6403 has since closed. Could the rebase keep only one of the two?
Sounds good to me. Thanks @dwsmith1983 |
sunchao
left a comment
There was a problem hiding this comment.
Reviewed the entire seven-file diff from 00a4b422f0ed6560ef76dce004b94c3697613108 to 55a6f70fa69f0a00638d68b013d4247e8e516aa7. The PR is open and not a draft. Read the existing reviews, conversation comments, inline comments and threads. Routed skills: review-comet-pr, review-comet-memory-pr and review-comet-shuffle-pr.
No additional introduced P1/P2 issues found within this review. The previously reported P2 packaging blocker remains unresolved.
Summary
- Prior state and problem: JVM shuffle allocations already retried Spark’s missing-task-entry race, but
CometTaskMemoryManager.acquireMemorydid not. A waiting native reservation could fail when another consumer released the task’s last bytes. - Design approach: Extract the existing retry policy into
MissingTaskEntryRetryand reuse it for native reservations. Keep the caller-specific refusal behavior: zero bytes for native memory andSparkOutOfMemoryErrorfor shuffle allocations. - Correctness: Checked Spark’s registration, wait, grant and release paths. The matching exception occurs before that pool request grants memory, and a legitimate outstanding grant prevents the task balance from reaching zero. Retrying does not double-count a grant. Comet updates
usedonly after a successful return. Both native pools interpret zero through their existing refusal and overcommit paths. Unrelated exceptions still propagate. No additional P1/P2 correctness issue identified. - Compatibility analysis: Compared
ExecutionMemoryPool,TaskMemoryManagerandMemoryConsumersources for Spark 3.4.3, 3.5.9, 4.0.4, 4.1.3 and 4.2.0. The relevant race and retry assumptions hold across these versions. Spark 4.2’s atomic consumer accounting does not change this conclusion. No SQL expression semantics, configuration defaults or shuffle formats change. - Key design decisions: Limit requests to three attempts, identify the exception using both its message prefix and an
ExecutionMemoryPoolstack frame, preserve the last failure for diagnostics, and leave the refusal policy with each caller. These choices keep the workaround narrowly scoped. - Implementation sketch: Add the shared Java helper, wrap the native acquire, delegate the existing shuffle retry to it, extract
TaskMemoryTestUtils, add five native tests, and updatememory_management.md. The existing shuffle tests retain their behavioral checks. - Performance: Successful requests still call Spark once. Exception-stack inspection and retry logging occur only after the matching failure. The generic callback and boxed return introduce some implementation overhead, but no measured P1/P2 performance regression was established. No benchmark was run.
- Design: The fix belongs at the shared Spark-memory boundary and preserves Spark’s arbitration. It changes reservation failure handling without claiming to solve container RSS limits or native reclamation. The different native and shuffle refusal contracts remain explicit.
- Abstraction & complexity: One helper appropriately shares the exception classifier, retry bound and logging across two callers returning different types. The shared test fixture centralizes task setup, failure injection and logger cleanup. No additional abstraction concern meets the P1/P2 bar.
- Behavioral changes worth calling out: Compared the touched paths with
branch-1.1ate9efd9f764ee0a59b7898ff028d6985d4a7a28e1. Native requests now retry this race and can return a normal refusal instead of propagating it. Spillable operators can consequently spill, whilegrowretains its overcommit policy. Shuffle retry behavior differs from that release because of the earlier #6403 change already present in this PR’s base. This PR preserves that behavior. - Suggested improvements: Resolve the already-reported packaging blocker before merge. No additional P1/P2 changes are requested.
Exact-head CI: 24 checks succeeded, 24 failed and 37 were skipped. Failures include the Spark-profile build shards, both TPC build jobs and their aggregate gates. Inspected execution logs for all five Spark profiles show all ten CometTaskMemoryManagerSuite tests passing, followed by check-jar-contents rejecting org/apache/spark/MissingTaskEntryRetry.class. The default shuffle log likewise shows its five allocator tests passing before the same rejection. This substantiates the existing unresolved packaging comment, so it is not repeated as a new finding.
Validation: Freshly compiled the relevant head Java classes and both Scala suites in a disposable directory, using Spark 4.1.3 and JDK 21. All 15 tests passed. Replacing only CometTaskMemoryManager with the base version made both new race tests fail with NoSuchElementException: key not found: 0. The repository’s JAR-content checker also rejected a temporary JAR containing the freshly compiled helper, reproducing the existing blocker.
Validation limits: Local testing was isolated JVM validation, not a full Maven/native build or an end-to-end native spill/replay run. Spark SQL, Iceberg and macOS checks were skipped in exact-head CI. Project source was left unchanged, and nothing was published.
|
Thanks both. Per the plan with @dwsmith1983, I'll rebase this onto #6310 once it lands, turning it into the follow-up that moves both call sites onto one helper. In that rebase I'll move the helper under |
…cator and the native pool apache#6403 added a retry for Spark's missing task entry (SPARK-59444) to CometUnifiedShuffleMemoryAllocator, and apache#6310 added its own copy to CometTaskMemoryManager.acquireMemory. Move both into MissingTaskEntryRetry under org.apache.comet. CometTaskMemoryManager passes its task id, which keeps apache#6310's exact "key not found: <id>" match; the shuffle allocator keeps apache#6403's prefix match. After the retries run out the native pool still gets a zero grant and the shuffle allocator still throws SparkOutOfMemoryError, and the warning now carries the last exception for both. The fixtures and the waiting-request harness of both suites move to TaskMemoryTestUtils, and CometTaskMemoryManagerSuite keeps one race test next to the refusal and rethrow tests. memory_management.md describes the retry once for both call sites, says what a zero grant does to try_grow and grow, and drops the stale apache#6304 reference. Co-authored-by: Isaac <no-reply@databricks.com>
releasePlan drops a plan's stream when the task ends, while a Tokio worker may still wait in Spark's acquireMemory for the memory that stream holds. When the release empties the task's balance, the worker loses the task's entry. Before apache#6310 it failed at once; with the retry it registered the ended task again and, below its minimum share, waited for other tasks to release memory while holding the TaskMemoryManager monitor that the executor's cleanUpAllAllocatedMemory needs. MissingTaskEntryRetry now refuses without retrying when the task has completed or been killed: CometTaskMemoryManager returns a zero grant and the shuffle allocator throws SparkOutOfMemoryError. Both callers pass the TaskContext they were created in, since native threads have none of their own. Co-authored-by: Isaac <no-reply@databricks.com>
55a6f70 to
55e62af
Compare
|
Rebased onto #6310 and addressed the review:
One behavior change was added, deferred from the #6310 review: the retry no longer runs once the task has completed or been killed. A test reproduces the issue. Releasing the task's last bytes after the task ended made the retried acquire register the task again and wait in Spark while holding the |
Which issue does this PR close?
None. Follow-up to #6310 and #6403, which added the two retries this PR merges.
Rationale for this change
#6403 made
CometUnifiedShuffleMemoryAllocatorretry an allocation that Spark fails withNoSuchElementException: key not found: <taskAttemptId>after the task's entry was dropped fromExecutionMemoryPool(SPARK-59444, fix pending in apache/spark#58747). #6310 added its own copy toCometTaskMemoryManager.acquireMemory. As #6403's description said, the two call sites should share one helper.The shared retry also has a gap at task teardown. When a task ends,
releasePlandrops a plan's stream while a Tokio worker may still be waiting in Spark'sacquireMemoryfor the memory that stream holds. If the release empties the task's balance, the worker loses the task's entry. Before #6310 it failed at once. With the retry, it registers the ended task again and, below its minimum share, waits for other tasks to release memory. While it waits it holds theTaskMemoryManagermonitor that the executor'scleanUpAllAllocatedMemoryneeds.What changes are included in this PR?
MissingTaskEntryRetryinorg.apache.cometholds the retry and the check for Spark's missing-entry exception, moved out ofCometUnifiedShuffleMemoryAllocator(fix: retry a shuffle page allocation that Spark failed after dropping the task's entry #6403) andCometTaskMemoryManager(fix: retry a native acquire that Spark failed after dropping the task's entry #6310).CometTaskMemoryManagerpasses its task id and keeps fix: retry a native acquire that Spark failed after dropping the task's entry #6310's exactkey not found: <id>match. The shuffle allocator keeps fix: retry a shuffle page allocation that Spark failed after dropping the task's entry #6403's prefix match.SparkOutOfMemoryError. The warning now carries the last exception for both.TaskContext.isCompleted()/isInterrupted()): it refuses at once, with a zero grant orSparkOutOfMemoryErroras above. Both callers pass theTaskContextthey were created in, because native threads have none of their own. Spark runs completion listeners without holding the context's monitor, so a worker callingisCompleted()does not wait onreleasePlan.TaskMemoryTestUtilstrait.memory_management.mddescribes the retry once for both call sites. It says a zero grant is a refusal fortry_grow(the operator spills) and overcommit forgrow, covers the teardown case, and drops the stale A JVM consumer's parked page allocation fails when another consumer of the task empties its balance #6304 reference.How are these changes tested?
UnifiedMemoryManager.CometTaskMemoryManagerSuitekeeps one race test, the refusal test (which now also checks that the warning carries the exception), and fix: retry a native acquire that Spark failed after dropping the task's entry #6310's rethrow test for another task's id.cleanUpAllAllocatedMemorycompletes.CometTaskMemoryManagerSuite,CometUnifiedShuffleMemoryAllocatorSuite, andCometUnboundedShuffleMemoryAllocatorSuitepass locally on Spark 4.1. Therun-all-spark-profileslabel covers the other profiles.