Skip to content

fix(pyamber, v1.2): stop EndWorker from consuming straggler messages - #6995

Open
Yicong-Huang wants to merge 1 commit into
release/v1.2from
backport/6522-stop-endworker-from-consuming-straggler-v1.2
Open

fix(pyamber, v1.2): stop EndWorker from consuming straggler messages #6995
Yicong-Huang wants to merge 1 commit into
release/v1.2from
backport/6522-stop-endworker-from-consuming-straggler-v1.2

Conversation

@Yicong-Huang

@Yicong-Huang Yicong-Huang commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

Backport of #6522 to release/v1.2, cherry-picked from 7e4a9b4. The cherry-pick applied cleanly.

Follows the Direct Backport Push convention; opened as a PR (rather than a direct push) per a backport-coverage audit.

Any related issues, documentation, discussions?

Backport of #6522. Originally linked #6521.

How was this PR tested?

Release-branch CI runs on this PR. Cherry-pick applied cleanly onto release/v1.2; no manual conflict resolution was needed.

Was this PR authored or co-authored using generative AI tooling?

Yes — backport prepared with Claude Code (mechanical cherry-pick; the change itself is #6522 by its original author).

🤖 Generated with Claude Code

### What changes were proposed in this PR?

**Root cause.** The Python worker's `EndWorkerHandler` guard logs its
"unprocessed messages" warning through `input_queue.get()` — a
destructive, blocking read that removes a pending message — and then
`assert input_queue.is_empty()`. With exactly one straggler message the
straggler is silently dropped and `EndWorker` is acknowledged as
success; with two or more, one message is still destroyed and the RPC
fails with a bare `AssertionError` — and because the coordinator retries
`EndWorker`, every retry that hits the guard eats another queued message
until the last one is dropped under a success ack.

| queue state at `EndWorker` | before | after |
|---|---|---|
| empty | ack | ack (unchanged) |
| 1 straggler | straggler **dropped**, then ack | RPC fails, straggler
kept |
| ≥ 2 stragglers | 1 dropped, then `AssertionError` | RPC fails, all
kept |

**Fix.** `end_worker_handler.py` reads the queued count once via
`input_queue.size()` and branches on it, so nothing is consumed. When
the queue is non-empty it logs the pending count and raises
`RuntimeError("worker still has unprocessed messages")` instead of
consuming a message and asserting. The raise rides the existing failure
path — `AsyncRPCServer.receive` converts a handler exception into a
`ControlError` reply, `AsyncRPCClient.fulfillPromise` on the coordinator
turns it into a failed future, and
`RegionExecutionManager.terminateWorkersWithRetry` re-sends `EndWorker`
on a fixed `killRetryDelay` (bounded by `maxTerminationAttempts`),
succeeding once the queue has drained — the exact Python analogue of the
Scala `EndHandler`'s `Future.exception`:

```
coordinator                        worker input queue at EndWorker arrival
    |                              [ReturnInvocation, ..., EndWorker]
    |-- EndWorker ---------------->|
    |<- ControlError --------------|   size() for the log; nothing consumed
    |   (retry after delay)
    |-- EndWorker ---------------->|   queue drained by the main loop
    |<- EmptyReturn ---------------|   safe to gracefulStop
```

The local is annotated as `InternalQueue` because `size()` lives on
`InternalQueue`, not the base `IQueue` interface. No new inspection API
(e.g. `peek()`) is added — `InternalQueue` stays non-peekable, like
`queue.Queue` — so the change is confined to the handler plus its new
test.

### Any related issues, documentation, discussions?

Closes #6521

### How was this PR tested?

TDD — the tests were written first and fail against the unfixed handler
(with 1 straggler: no exception is raised and the message vanishes; with
≥ 2: `AssertionError` instead of a clean RPC failure):

- New
`src/test/python/core/architecture/handlers/control/test_end_worker_handler.py`:
acks on an empty queue; fails the RPC with one straggler; does **not**
consume the straggler; keeps all messages with two stragglers; acks
again once the queue drains (the retry protocol end-to-end). This is the
Python analogue of the Scala `EndHandlerSpec`.

Ran locally: `cd amber && pytest -m "not integration"` on the touched
test file plus the full unit suite, and `ruff check src/main/python
src/test/python && ruff format --check src/main/python src/test/python`.

### Was this PR authored or co-authored using generative AI tooling?

(backported from commit 7e4a9b4)

Generated-by: Claude Code (Fable 5)
@Yicong-Huang Yicong-Huang added the release/v1.2 back porting to release/v1.2 label Jul 29, 2026
@Yicong-Huang
Yicong-Huang requested a review from xuang7 July 29, 2026 04:11
@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • No candidates found from git blame history.

@Yicong-Huang Yicong-Huang removed the release/v1.2 back porting to release/v1.2 label Jul 29, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.96%. Comparing base (fd5f487) to head (6711566).
⚠️ Report is 2 commits behind head on release/v1.2.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                Coverage Diff                 @@
##             release/v1.2    #6995      +/-   ##
==================================================
+ Coverage           53.95%   53.96%   +0.01%     
  Complexity           1441     1441              
==================================================
  Files                 809      809              
  Lines               34144    34145       +1     
  Branches             3448     3448              
==================================================
+ Hits                18421    18428       +7     
+ Misses              14815    14809       -6     
  Partials              908      908              
Flag Coverage Δ *Carryforward flag
access-control-service 64.61% <ø> (ø) Carriedforward from fd5f487
agent-service 34.36% <ø> (ø) Carriedforward from fd5f487
amber 59.61% <ø> (ø) Carriedforward from fd5f487
computing-unit-managing-service 1.65% <ø> (ø) Carriedforward from fd5f487
config-service 56.06% <ø> (ø) Carriedforward from fd5f487
file-service 58.59% <ø> (ø) Carriedforward from fd5f487
frontend 47.19% <ø> (ø) Carriedforward from fd5f487
pyamber 91.02% <100.00%> (+0.14%) ⬆️
python 90.80% <ø> (+0.06%) ⬆️ Carriedforward from fd5f487
workflow-compiling-service 58.69% <ø> (ø) Carriedforward from fd5f487

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants