[Spark] Make cancel() cancel the Spark jobs and stop only a session the runner created - #40103
Conversation
131d037 to
159e6c0
Compare
|
Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment |
159e6c0 to
71e7061
Compare
|
Assigning reviewers: R: @kennknowles added as fallback since no labels match configuration Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
|
I understand this change tries to fix a few important gaps/bugs, however the behavior change part is likely undesiable. mainly cancel() becomes synchronous, and the reference counting may still not eliminate races of parallel jobs. The original diagnosis is spot-on:
In the Beam model, Reference counting: the choice of synchrnous Reference counting itself does not solve the race. It is the synchrnous Take a step back: it is a Spark limitation limiting us running jobs in parallel that would need different pipeline options won't have all configurations honored. This stems from the fact that only one active SparkContext is allowed throughout JVM. Need to think more about this. It's likely hard to find a proper fix. Would it possible to have a fix of miinimum behavior change (most notable the synchronous cancel) that could largely reduce the likelihood of race? |
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.runners.spark.structuredstreaming; | ||
|
|
There was a problem hiding this comment.
These tests may be useful, however, a generic idea is to keep test and main source balanced and test concise, as nowadays it's much easier to write boilerplate tests than before
| return SparkSession.active(); | ||
| } | ||
| return sessionBuilder(options.getSparkMaster(), options).getOrCreate(); | ||
| // Spark 3 also returns stopped sessions. |
There was a problem hiding this comment.
Please add a few comments noting why we need to manage active sessions ourselves now.
There was a problem hiding this comment.
Will do: getOrCreate adopts an existing session, a pipeline must not stop one it did not create, and the next pipeline needs the previous one's session gone to get its own config.
|
Agreed on both, the synchronous cancel and the reference count go. Before I push the rework, the shape I have in mind:
Tests down to two unit plus two live cases, plus one that starts a pipeline right after a cancel and asserts it got a new SparkContext, which is the CI failure case. A short rationale block goes into Does that match what you have in mind? |
Disallow more than one pipeline submitting at the same time in JVM would be another regression I feel the agent (if used) tend to add locks is a tendency, as it is a generic (lazy) fix for concurrent issues. We should define what would be the expectation. Considering current behavior
After isolating each issue and requirements, here is my proposal:
Based on the observation in #40101, it's translating on a stopped session (or stopped midway) causing crash. If there are two jobs both running, and the first one ends and stopped its session, the second one can still run and ends. It's only job management get affected (cancel request won't reach job?). If this is true, we can make use of this fact to separate the effort of fixing crash and a proper long term fix, thus make life easier To fix crash, here is some idea
By doing this we still need only one synchronized acquire, and mostly keep and simplified the current PR's structure |
|
Also async job.cancel() shouldn't set the job status to CANCELLED immediately per spec. It is subsequent job.status() will query for status and set it to cancelled if so; or subsequent job.waitUntilFinish() blocks until job actually cancelled on (mini)cluster. This can be follow ups. |
|
Thanks, that framing is clearer than mine and I will follow it. Shape of the rework:
Your point on the state after an asynchronous |
fcbd007 to
9492ef3
Compare
…he runner created cancel() interrupted the execution thread and stopped the SparkSession from the caller thread. An interrupt does not cancel a Spark job, so with useActiveSparkSession a batch pipeline was never cancelled, and the session stop could land under a thread that was still translating. The execution thread now runs under a job group, cancel() stops the evaluation, cancels the group and returns. SparkSessionFactory counts the pipelines per session it created and stops the session on the execution thread when the last one releases it, sessions it did not create are never stopped. Batch EvaluationContext.stop() ends the leaf loop. A pipeline that ends after a cancel request reports CANCELLED from waitUntilFinish(). Fixes apache#40101.
Share the stopped flag between EvaluationContext and StreamingEvaluationContext. Log the exception of an execution that fails after cancel at WARN. Drop redundant isStopped guards, clearJobGroup on a single use thread and the release on MetricsAccumulator failure. Remove the unused getOrCreateSession. Blocking DoFn in the state test exits on task kill, no release latch. Touch the SparkStructuredStreaming and Spark4 ValidatesRunner trigger files.
9492ef3 to
3f85f3f
Compare
|
Thank you! |
Fixes #40101. Found in review of #40090. Shape agreed with @Abacn in the thread below.
Bug
cancel()interrupted the execution thread withFuture.cancel(true)and then stopped the SparkSession from the caller thread. An interrupt does not cancel a Spark job, so withuseActiveSparkSession=truea batch pipeline was never cancelled. The session stop landed under a thread that was still translating, which is theNoClassDefFoundErrorcascade seen in the Spark Versions PreCommit on #40090.Fix, shared code compiled for Spark 3 and Spark 4
cancel()stops the evaluation, cancels the job group and returns. It does not wait.waitUntilFinish()reports CANCELLED once an execution that was cancelled ends, normally or with an exception, and logs the exception at WARN. BatchEvaluationContext.stop()ends the leaf loop,StreamingEvaluationContextreuses that flag and keeps stopping its queries.interruptOnCancel=true, which is what Spark's ownStreamExecutionuses for streaming queries. Without it a cancel waits for every running task to finish its partition. The legacy runner stops the whole SparkContext on cancel, which is harsher.SparkSessionFactory.acquireandreleasereplacegetOrCreateSession. Sessions the runner creates are counted per pipeline, the last pipeline to release stops the session, on the execution thread after evaluation. A session the runner did not create is never stopped. Pipelines that run one after another therefore each get a fresh session with their own configuration, as before. A pipeline that starts while another one still holds the session shares it and logs that its configuration is not applied, which is today's behavior. Nothing waits for another pipeline. The lock covers the stop itself, so a pipeline starting during a stop creates a new session rather than adopting a stopping one.getOrCreateSessionhad no other callers and is removed.Tests
SparkStructuredStreamingPipelineResultTest: the job cancel hook runs once,cancel()returns before the execution ended andwaitUntilFinish()then reports CANCELLED.StructuredStreamingPipelineStateTest: a running batch job is cancelled and its session is gone afterwaitUntilFinish(), a session created outside the runner survives, and a pipeline started right aftercancel()without waiting finishes DONE and leaves no session behind, the shape of the CI failure. The legacy running, cancelled and timeout cases join aftercancel(), the session stop runs on the execution thread and a test that walks away leaks it into the next test class.Follow up: #40120, the state after an asynchronous
cancel()should be observed from the execution rather than set bycancel()itself.R: @Abacn