fix(amber): advance region executions in a later coordinator round, not inside portCompleted - #6960
Conversation
Automated Reviewer SuggestionsBased on the
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 407 | 0.248 | 24,360/34,024/34,024 us | 🔴 +9.3% / 🔴 +115.7% |
| 🔴 | bs=100 sw=10 sl=64 | 794 | 0.485 | 120,214/162,180/162,180 us | 🔴 +28.0% / 🔴 +51.1% |
| ⚪ | bs=1000 sw=10 sl=64 | 916 | 0.559 | 1,088,995/1,122,727/1,122,727 us | ⚪ within ±5% / 🔴 -11.2% |
Baseline details
Latest main ff82157 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 407 tuples/sec | 431 tuples/sec | 786.12 tuples/sec | -5.6% | -48.2% |
| bs=10 sw=10 sl=64 | MB/s | 0.248 MB/s | 0.263 MB/s | 0.48 MB/s | -5.7% | -48.3% |
| bs=10 sw=10 sl=64 | p50 | 24,360 us | 22,297 us | 12,305 us | +9.3% | +98.0% |
| bs=10 sw=10 sl=64 | p95 | 34,024 us | 33,075 us | 15,774 us | +2.9% | +115.7% |
| bs=10 sw=10 sl=64 | p99 | 34,024 us | 33,075 us | 18,978 us | +2.9% | +79.3% |
| bs=100 sw=10 sl=64 | throughput | 794 tuples/sec | 851 tuples/sec | 999.71 tuples/sec | -6.7% | -20.6% |
| bs=100 sw=10 sl=64 | MB/s | 0.485 MB/s | 0.519 MB/s | 0.61 MB/s | -6.6% | -20.5% |
| bs=100 sw=10 sl=64 | p50 | 120,214 us | 117,526 us | 100,616 us | +2.3% | +19.5% |
| bs=100 sw=10 sl=64 | p95 | 162,180 us | 126,723 us | 107,356 us | +28.0% | +51.1% |
| bs=100 sw=10 sl=64 | p99 | 162,180 us | 126,723 us | 113,255 us | +28.0% | +43.2% |
| bs=1000 sw=10 sl=64 | throughput | 916 tuples/sec | 934 tuples/sec | 1,031 tuples/sec | -1.9% | -11.2% |
| bs=1000 sw=10 sl=64 | MB/s | 0.559 MB/s | 0.57 MB/s | 0.63 MB/s | -1.9% | -11.2% |
| bs=1000 sw=10 sl=64 | p50 | 1,088,995 us | 1,070,312 us | 980,328 us | +1.7% | +11.1% |
| bs=1000 sw=10 sl=64 | p95 | 1,122,727 us | 1,118,659 us | 1,027,528 us | +0.4% | +9.3% |
| bs=1000 sw=10 sl=64 | p99 | 1,122,727 us | 1,118,659 us | 1,054,298 us | +0.4% | +6.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,491.37,200,128000,407,0.248,24360.21,34023.93,34023.93
1,100,10,64,20,2517.70,2000,1280000,794,0.485,120213.95,162179.54,162179.54
2,1000,10,64,20,21824.28,20000,12800000,916,0.559,1088995.08,1122727.08,1122727.08
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6960 +/- ##
============================================
+ Coverage 79.20% 79.27% +0.06%
- Complexity 3786 3796 +10
============================================
Files 1160 1161 +1
Lines 46118 46139 +21
Branches 5114 5119 +5
============================================
+ Hits 36529 36575 +46
+ Misses 7969 7944 -25
Partials 1620 1620
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
### What changes were proposed in this PR? The backport auto-label workflow (`.github/workflows/backport-auto-label.yml`) decides, per actively-supported release branch, whether to add a `release/*` label to a `fix:` PR. Until now every skip was silent — visible only in the Actions log. Its cheap "is this feature on the branch?" check compares **exact file paths**, so a file moved or renamed after the branch was cut reads as *absent* and the label is dropped with no signal to the author. apache#6960 hit exactly this: its files live under `coordinator/` on `main` but `controller/` on `release/v1.2`, so a fix that genuinely applied to v1.2 was never labeled. This PR makes the workflow write its reasoning back to the PR as a single report comment: - **One row per actively-supported release branch**, with the decision and why: - ✅ **labeled** — change detected on the branch; label added (and who was requested for review); -⚠️ **skipped** — none of the modified files exist on the branch (the files are listed), with a prompt to check and add the label by hand if the fix should be backported; - 🚫 **declined** — a previously removed label (opt-out), not re-added. - The comment is **upserted in place** via a hidden marker, so `edited` re-runs update the same comment instead of stacking new ones (editing a comment sends no notification, so re-runs stay quiet). - It links the auto-label run, and is authored by `github-actions[bot]` like the existing review request (no label PAT needed). - Inactive branches and non-`fix:` PRs are unchanged (no comment); a `no-backport-needed` PR gets a one-line note instead of the table. Labeling behavior itself is unchanged — this only surfaces the decisions that were already being made. ### Any related issues, documentation, discussions? Resolves apache#6961. Follow-up to the backport lifecycle automation (apache#6941, apache#6959). Motivated by apache#6960, which was silently not labeled for `release/v1.2`. ### How was this PR tested? - YAML parses and the embedded github-script passes `node --check` (async-wrapped). - Walked the branch/label/timeline/file-status cases against apache#6960's real data to confirm the rows and skip reasons render as intended. - Full end-to-end exercise needs a live `pull_request_target` event with the org PAT/secrets, which only runs once merged. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 4.8) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot inside portCompleted On every normal region termination the coordinator advanced region executions inline in the portCompleted handler's continuation. That advance terminates the completed region, which sends EndWorker to each of its workers - so EndWorker went out before the reply to the very portCompleted being handled, and both travel the same FIFO control channel to that worker. The worker then processed EndWorker while the reply was still queued behind it, its unprocessed-messages check rejected the termination, and the first attempt failed with 2 ERRORs + 2 WARNs + 2 stack traces before the 200 ms retry succeeded (the LoopIntegrationSpec flake). The advance now goes through the coordinator's own control channel: WorkflowExecutionManager.requestAdvanceRegionExecutions() sends CoordinatorInitiateAdvanceRegionExecutions to the coordinator itself, and its handler runs advanceRegionExecutions. A self-directed control message is transmitted and then received before it is handled, so it lands a whole message behind every reply the requesting round still owed - EndWorker can no longer overtake a reply. Everything else is left alone: portCompleted still replies at the end of its statistics/bookkeeping continuation, so a failure there is still returned to the sending worker as a ControlError, and an advance failure still reaches the client as a FatalError. The worker-side unprocessed-messages check is untouched.
b50c0a5 to
905816c
Compare
Yicong-Huang
left a comment
There was a problem hiding this comment.
🔴 1 must-fix · 1 advisory — clean, well-tested fix for the dominant portCompleted-reply instance of the #6891 race; one completeness question.
Correctness (2)
PortCompletedHandler.scala:86— deferral closes only theportCompleted-reply race; the same worker'sworkerExecutionCompletedack can still raceEndWorker, so #6891 may not be fully closed (must-fix, see inline)PortCompletedHandlerSpec.scala:200— the spec never interleaves aworkerExecutionCompleted, so it doesn't guard the ack race #6891 describes (advisory, see inline)
Verification trace
Traced the send ordering on the coordinator→worker FIFO channel. The deferral robustly orders portCompleted's own reply before EndWorker (both are decided within one round; the self-advance is only handled in a later round) — verified, and the new spec pins it. But region completion (RegionExecution.getState) keys only on port completion, and OutputManager.finalizeOutput emits FinalizePort (→portCompleted) before FinalizeExecutor (→workerExecutionCompleted), so the region-completing worker's workerExecutionCompleted is always still pending when its portCompleted triggers the advance. The self-advance (SELF channel) and that workerExecutionCompleted (worker→coordinator channel) have no cross-channel FIFO order — NetworkInputGateway.tryPickControlChannel scans a mutable.HashMap — so a round that picks the self-advance first emits EndWorker ahead of the workerExecutionCompleted ack, which then queues behind it at the worker and trips EndHandler. The stats-query round-trips only add nondeterministic delay, not an ordering guarantee.
Yicong-Huang
left a comment
There was a problem hiding this comment.
Conditionally approved.
After discussion, this can only partially fix the issue. There are more events could happen after EndWorker. If this can reduce the flakiness of the tests, I agree to merge it first and keep the issue open for further investigation as follow up. Please handle my inline comments as much as possible, though.
Addresses review on apache#6960: deferring the region advance orders portCompleted's own reply before EndWorker, but it cannot order a reply the coordinator has not produced yet. Region completion keys only on port completion, so the deferred advance fires EndWorker as soon as the last portCompleted is booked - while the same worker's workerExecutionCompleted (emitted right after, FinalizePort before FinalizeExecutor) may still be queued at the coordinator. The coordinator picks input channels out of a HashMap (NetworkInputGateway.tryPickControlChannel), so there is no cross-channel order to rely on: it can run the advance first and reply afterwards, leaving that reply queued behind EndWorker at the worker. Ordering alone cannot close this, so EndWorker no longer treats a queued ReturnInvocation as unprocessed work. Processing one only fulfills a promise for a request the worker already issued, and every worker-to-coordinator call discards its future, so nothing is pending on it. Everything else - control invocations, data, embedded control messages, timer controls, actor commands - still fails the request so the coordinator retries and the worker drains it. The Python worker matches, deciding on a work counter maintained in InternalQueue.put/get: the check neither inspects the next message nor puts anything back. Also inlines the advance request at its call site rather than wrapping it in WorkflowExecutionManager, and renames the spec helper to say what it builds.
b0757e5 to
6382dbd
Compare
Yicong-Huang
left a comment
There was a problem hiding this comment.
I found unrelated changes? Please take a look
Drops this PR's Python changes so internal_queue.py is untouched: the ack-tolerance there was decided by a work counter maintained in InternalQueue.put/get, which cannot be provided without changing that class. The Python worker therefore keeps main's behaviour, failing EndWorker whenever anything is queued, and the coordinator's existing retry covers it as before. The Scala fix is unaffected: the deferred advance orders the replies the coordinator has already produced, and EndHandler tolerates a queued ReturnInvocation for the ones it has not.
Backport auto-label reportThis
|
Yicong-Huang
left a comment
There was a problem hiding this comment.
🔴 2 resolved · 0 open · 2 new (2 new = 2 newly introduced · 0 late catches)
The ack race I flagged last round is genuinely closed and well-tested — both new specs fail on main. Two things remain: a doc sweep, and reconsidering Closes.
Correctness (1)
EndHandler.scala:49(+ 3 more) — scaladoc references removed/renamed methods:hasUnprocessedWork→findUnprocessedWork, andWorkflowExecutionManager.requestAdvanceRegionExecutions()(removed) → the RPCcoordinatorInitiateAdvanceRegionExecutionsatWorkflowExecutionManager.scala:101,RegionExecutionManager.scala:144,AdvanceRegionExecutionsHandler.scala:36; the PR description repeats the dead name too (must-fix, see inline)
Conventions (1)
Closes #6891closes only the JVM instance of the race (the observed flake was JVM-side); the Python worker is deliberately left unfixed, so parity is a follow-up. RecommendRelated to #6891and keeping the issue open — matching the plan we agreed last round (advisory)
Verification trace
Traced the EndHandler ack-tolerance fix: the only worker-to-coordinator calls — workerExecutionCompleted, portCompleted, workerStateUpdated, consoleMessageTriggered, all in DataProcessor — are fire-and-forget (future discarded), and no worker-side code awaits any RPC reply, so terminating with a queued ReturnInvocation strands nothing. The deferral orders portCompleted's own reply before EndWorker deterministically (self-addressed advance is handled a round later); the residual workerExecutionCompleted ack cannot be ordered (self-advance vs worker-to-coordinator channel have no cross-channel FIFO — tryPickControlChannel scans a HashMap), which is exactly what the ack-tolerance covers. Both halves close #6891 for JVM workers.
… method The scaladoc still named WorkflowExecutionManager.requestAdvanceRegionExecutions(), which was inlined into PortCompletedHandler last round, and EndHandler's scaladoc named hasUnprocessedWork instead of findUnprocessedWork.
Yicong-Huang
left a comment
There was a problem hiding this comment.
🟡 1 resolved · 1 open · 0 new (0 new = 0 newly introduced · 0 late catches)
The prior doc-sweep must-fix is fully resolved — all four references now name CoordinatorInitiateAdvanceRegionExecutions / findUnprocessedWork, and a repo-wide grep for the dead names is empty. One advisory remains.
Conventions (1)
Closes #6891closes the issue on merge, but the PR is JVM-only and #6891's expected behavior covers the Python side too. RecommendRelated to #6891and keeping the issue open for the Python-parity follow-up (advisory)
|
Backport PR opened: draft #7096 (#7096) to |
…ot inside portCompleted (#6960) ### What changes were proposed in this PR? **Root cause of the `LoopIntegrationSpec` retry storm.** `portCompleted`'s handler advanced region executions inline in its own continuation. That advance terminates a completed region, which sends `EndWorker` to each of its workers — so `EndWorker` went out *before* the reply to the very `portCompleted` being handled, and both travel the same FIFO control channel to that worker: ``` Before — all inside one handler round: advanceRegionExecutions() ──> EndWorker to W (seq N on W's control channel) return EmptyReturn() ──> ReturnInvocation to W (seq N+1, same channel) W processes EndWorker while the reply is still queued behind it: EndHandler: queue not empty => IllegalStateException("worker still has unprocessed messages") => attempt 1 of 150 fails => 2 ERRORs + 2 WARNs + 2 stack traces => 200 ms retry succeeds ``` The check's premise — that `EndWorker` is the last message a worker receives — was being violated by the coordinator itself on every teardown. Whether the worker actually observed the trailing reply was a microsecond race, hence the ~1-in-10 flake. **Fix: advance in a later coordinator round instead of inline.** A new coordinator-to-coordinator RPC carries the advance: ``` After: portCompleted round: bookkeeping; send CoordinatorInitiateAdvanceRegionExecutions to self return EmptyReturn() ──> ReturnInvocation to W later round: advance runs ──> EndWorker to W ``` A message the coordinator addresses to itself is transmitted and then received before it is handled, so it lands a whole message behind every reply the requesting round still owed — `EndWorker` can no longer overtake a reply, and the first termination attempt succeeds. **Second half: the worker tolerates a reply it cannot be ordered against.** Deferring orders the replies the coordinator has *already produced*, but region completion keys only on **port** completion, so the advance fires as soon as the last `portCompleted` is booked — while the same worker's `workerExecutionCompleted` (emitted right after it) may still be queued at the coordinator. Since `NetworkInputGateway.tryPickControlChannel` selects channels out of a `HashMap`, there is no cross-channel order: the advance can run first and that reply be emitted afterwards, landing behind `EndWorker`. Ordering cannot close this, so `EndWorker` no longer counts a queued `ReturnInvocation` as unprocessed work — processing one only fulfills a promise for a request the worker already issued, and all four worker→coordinator calls discard their futures, so nothing is pending on it. Every other queued element still fails the request, so the coordinator retries and the worker drains it first. This PR is JVM-only: the Python worker keeps its current check, which fails `EndWorker` whenever anything is queued, and the coordinator's retry covers it exactly as it does today. Bringing it to parity needs a way to classify what is queued without inspecting the next message, which means changing `InternalQueue` — left for a follow-up rather than bundled here. | | Before | After | |---|---|---| | Coordinator send order | `EndWorker`, then the `portCompleted` reply | reply, then `EndWorker` | | First `EndWorker` attempt | fails on a benign race (storm ~1 run in 10) | succeeds | | Normal teardown logs | 2 ERROR + 2 WARN + 2 stack traces | none | | Worker-side check | any queued message fails | a queued **`ReturnInvocation`** no longer fails; all real work still does | | Bookkeeping failure | `ControlError` to the reporting worker | unchanged | | Advance failure | `FatalError` to the client | unchanged | Changes: - **`coordinatorservice.proto`**: adds `CoordinatorInitiateAdvanceRegionExecutions`, alongside the existing self-directed `CoordinatorInitiateQueryStatistics`. - **`AdvanceRegionExecutionsHandler`** (new): handles that self-addressed request by running the advance, and reports failures to the client exactly as the inline call did. `advanceRegionExecutions`'s scaladoc now tells callers handling a worker-initiated request to go through it. - **`PortCompletedHandler`**: keeps its bookkeeping and its reply position; only the advance moves. - **`RegionExecutionManager`**: corrects the scaladoc that claimed `EndWorker` "will be the last message each worker receives". `StartWorkflowHandler` still advances directly: its caller is the client, it awaits the future to answer with the workflow state, and no `EndWorker` is involved at startup. ### Any related issues, documentation, discussions? #6891. ### How was this PR tested? - **`PortCompletedHandlerSpec` (new)**: drives a real `CoordinatorProcessor` through the real RPC server with a captured output gateway, over a single-region schedule already in flight. Pins that the advance leaves as a separate coordinator-addressed control message; that the *only* thing sent to the reporting worker while handling `portCompleted` is its reply (the direct regression guard — on `main` this list also contains `EndWorker`); that the port is recorded completed before the request goes out, since the advance now reads that state in a later round; that a port belonging to no executing region requests nothing; and that a failed continuation still returns a `ControlError` to the reporting worker. - Baseline check: 3 of those tests fail against `main`'s handler, while the `ControlError` test passes on **both** `main` and this branch — which is what verifies the worker-facing error contract is preserved rather than merely asserted. - Untouched and green, as evidence termination semantics are unchanged: `RegionExecutionManagerSpec`, `WorkflowExecutionManagerSpec`, `EndHandlerSpec`, `CoordinatorSpec`, `TrivialControlSpec`, `AsyncRPCClientSpec`. - Lint: `scalafmtCheck`, `scalafixAll --check` pass. - Loop confirmation is CI-side and statistical: `amber-integration` teardown logs should contain zero `Failed to terminate region ... on attempt 1` lines (runs without this fix show them repeatedly). ### Was this PR authored or co-authored using generative AI tooling? (backported from commit 226614b) Generated-by: Claude Code (Fable 5)
…und, not inside portCompleted (#7096) ### What changes were proposed in this PR? Automated backport of #6960 to `release/v1.2`. Source: 226614b · [automation run](https://github.com/apache/texera/actions/runs/30506845994) ### Any related issues, documentation, discussions? Backport of #6960. ### How was this PR tested? Release-branch CI runs on this branch once the conflicts are resolved and this PR is marked ready for review. ### Was this PR authored or co-authored using generative AI tooling? No. Co-authored-by: Xinyuan Lin <xinyual3@uci.edu>
What changes were proposed in this PR?
Root cause of the
LoopIntegrationSpecretry storm.portCompleted's handler advanced region executions inline in its own continuation. That advance terminates a completed region, which sendsEndWorkerto each of its workers — soEndWorkerwent out before the reply to the veryportCompletedbeing handled, and both travel the same FIFO control channel to that worker:The check's premise — that
EndWorkeris the last message a worker receives — was being violated by the coordinator itself on every teardown. Whether the worker actually observed the trailing reply was a microsecond race, hence the ~1-in-10 flake.Fix: advance in a later coordinator round instead of inline. A new coordinator-to-coordinator RPC carries the advance:
A message the coordinator addresses to itself is transmitted and then received before it is handled, so it lands a whole message behind every reply the requesting round still owed —
EndWorkercan no longer overtake a reply, and the first termination attempt succeeds.Second half: the worker tolerates a reply it cannot be ordered against. Deferring orders the replies the coordinator has already produced, but region completion keys only on port completion, so the advance fires as soon as the last
portCompletedis booked — while the same worker'sworkerExecutionCompleted(emitted right after it) may still be queued at the coordinator. SinceNetworkInputGateway.tryPickControlChannelselects channels out of aHashMap, there is no cross-channel order: the advance can run first and that reply be emitted afterwards, landing behindEndWorker. Ordering cannot close this, soEndWorkerno longer counts a queuedReturnInvocationas unprocessed work — processing one only fulfills a promise for a request the worker already issued, and all four worker→coordinator calls discard their futures, so nothing is pending on it. Every other queued element still fails the request, so the coordinator retries and the worker drains it first. This PR is JVM-only: the Python worker keeps its current check, which failsEndWorkerwhenever anything is queued, and the coordinator's retry covers it exactly as it does today. Bringing it to parity needs a way to classify what is queued without inspecting the next message, which means changingInternalQueue— left for a follow-up rather than bundled here.EndWorker, then theportCompletedreplyEndWorkerEndWorkerattemptReturnInvocationno longer fails; all real work still doesControlErrorto the reporting workerFatalErrorto the clientChanges:
coordinatorservice.proto: addsCoordinatorInitiateAdvanceRegionExecutions, alongside the existing self-directedCoordinatorInitiateQueryStatistics.AdvanceRegionExecutionsHandler(new): handles that self-addressed request by running the advance, and reports failures to the client exactly as the inline call did.advanceRegionExecutions's scaladoc now tells callers handling a worker-initiated request to go through it.PortCompletedHandler: keeps its bookkeeping and its reply position; only the advance moves.RegionExecutionManager: corrects the scaladoc that claimedEndWorker"will be the last message each worker receives".StartWorkflowHandlerstill advances directly: its caller is the client, it awaits the future to answer with the workflow state, and noEndWorkeris involved at startup.Any related issues, documentation, discussions?
#6891.
How was this PR tested?
PortCompletedHandlerSpec(new): drives a realCoordinatorProcessorthrough the real RPC server with a captured output gateway, over a single-region schedule already in flight. Pins that the advance leaves as a separate coordinator-addressed control message; that the only thing sent to the reporting worker while handlingportCompletedis its reply (the direct regression guard — onmainthis list also containsEndWorker); that the port is recorded completed before the request goes out, since the advance now reads that state in a later round; that a port belonging to no executing region requests nothing; and that a failed continuation still returns aControlErrorto the reporting worker.main's handler, while theControlErrortest passes on bothmainand this branch — which is what verifies the worker-facing error contract is preserved rather than merely asserted.RegionExecutionManagerSpec,WorkflowExecutionManagerSpec,EndHandlerSpec,CoordinatorSpec,TrivialControlSpec,AsyncRPCClientSpec.scalafmtCheck,scalafixAll --checkpass.amber-integrationteardown logs should contain zeroFailed to terminate region ... on attempt 1lines (runs without this fix show them repeatedly).Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Fable 5)