You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by yi...@apache.org on 2026/08/19 20:13:41 UTC
(spark) branch branch-4.x updated: [SPARK-58844][UDF] Fix a Cancel-after-Finish flake in the UDF worker protocol test client
This is an automated email from the ASF dual-hosted git repository.
Yicong-Huang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new af3df8f2f6fc [SPARK-58844][UDF] Fix a Cancel-after-Finish flake in the UDF worker protocol test client
af3df8f2f6fc is described below
commit af3df8f2f6fc34d394dbf8c3bb6ac4de68f6e780
Author: Mohamed Gallab <mo...@databricks.com>
AuthorDate: Wed Aug 19 20:13:11 2026 +0000
[SPARK-58844][UDF] Fix a Cancel-after-Finish flake in the UDF worker protocol test client
### What changes were proposed in this pull request?
`EchoProtocolSuite`'s `EngineClient` test helper touches its request `StreamObserver` from two threads without mutual exclusion. `sendCancel` does a check-then-act:
- test thread: `if (requestCompleted.get()) return` reads `false`, then calls `requestObserver.onNext(Cancel)`;
- meanwhile the response-observer callback thread runs `completeRequestStream()` (`requestObserver.onCompleted()`), half-closing the request stream.
If the half-close lands between the `get()` and the `onNext`, the `onNext` throws, because gRPC does not permit concurrent calls on a `StreamObserver`.
This adds a `requestLock` — mirroring the existing `responseLock`, which already serializes writes to the *response* observer — and guards both `sendCancel` (check + send) and `completeRequestStream` (CAS + half-close) so they can no longer interleave. Test-only change.
### Why are the changes needed?
`EchoProtocolSuite`'s `cancel: engine sends Cancel after Finish` case sends a trailing `Cancel` exactly as the `FinishResponse` terminator arrives, so it hits the race above and intermittently fails with `IllegalStateException: call was half-closed` (or `Stream is already completed, no further calls are allowed`). It passes in isolation and only fails under scheduling load, so it can fail CI unpredictably.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Existing `EchoProtocolSuite` cases. The race window is tiny, so to confirm the cause deterministically I temporarily widened it (a `Thread.sleep` between the `get()` and the `onNext` in `sendCancel`): with the widening the `cancel: engine sends Cancel after Finish` case failed on every run, and with `requestLock` in place it passed on every run with the same widening — confirming the lock closes the exact window. The temporary widening was then removed.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Anthropic)
Closes #58079 from mohamed-gallab-db/minor-udf-worker-cancel-after-finish-flake.
Authored-by: Mohamed Gallab <mo...@databricks.com>
Signed-off-by: Yicong-Huang <17...@users.noreply.github.com>
(cherry picked from commit de14ec8ba53532602a4645584e1673f8f11a09d2)
Signed-off-by: Yicong-Huang <17...@users.noreply.github.com>
---
.../spark/udf/worker/grpc/EchoProtocolSuite.scala | 27 +++++++++++++---------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/EchoProtocolSuite.scala b/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/EchoProtocolSuite.scala
index eae6ef1a639a..4de834220839 100644
--- a/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/EchoProtocolSuite.scala
+++ b/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/EchoProtocolSuite.scala
@@ -469,6 +469,10 @@ class EchoProtocolSuite extends AnyFunSuite with BeforeAndAfterEach {
@volatile var executionError: Option[ExecutionError] = None
@volatile var streamError: Option[Throwable] = None
private val requestCompleted = new AtomicBoolean(false)
+ // gRPC forbids concurrent calls to the request StreamObserver too: the response
+ // observer half-closes it (completeRequestStream) on a callback thread while the
+ // test thread may still be sending a trailing Cancel. Serialize both through this.
+ private val requestLock = new Object
// Counted down on InitResponse (success or failure) or on terminal error.
// The engine MUST wait for this before sending any DataRequest or Finish.
private val initResponseLatch = new CountDownLatch(1)
@@ -637,22 +641,23 @@ class EchoProtocolSuite extends AnyFunSuite with BeforeAndAfterEach {
}
}
- def sendCancel(reason: String = ""): Unit = {
- // If a terminator already arrived (FinishResponse / CancelResponse),
- // the request stream has been half-closed and Cancel arrives too
- // late -- silently ignore, matching the proto's Cancel-after-Finish
- // contract.
- if (requestCompleted.get()) return
- requestObserver.onNext(UdfRequest.newBuilder()
- .setControl(UdfControlRequest.newBuilder()
- .setCancel(Cancel.newBuilder().setReason(reason).build())
+ def sendCancel(reason: String = ""): Unit = requestLock.synchronized {
+ // If a terminator already arrived (FinishResponse / CancelResponse), the request
+ // stream has been half-closed and Cancel arrives too late -- silently ignore,
+ // matching the proto's Cancel-after-Finish contract. requestLock makes this check
+ // and the send atomic against completeRequestStream's half-close.
+ if (!requestCompleted.get()) {
+ requestObserver.onNext(UdfRequest.newBuilder()
+ .setControl(UdfControlRequest.newBuilder()
+ .setCancel(Cancel.newBuilder().setReason(reason).build())
+ .build())
.build())
- .build())
+ }
// Request stream stays open until the response terminator arrives;
// completeRequestStream() is called by the response observer.
}
- def completeRequestStream(): Unit = {
+ def completeRequestStream(): Unit = requestLock.synchronized {
if (requestCompleted.compareAndSet(false, true)) {
requestObserver.onCompleted()
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org