feat(environments): add defer_ack to work poller to fix dispatch-mode strand (#1746) - #1751
Open
PranavMishra28 wants to merge 1 commit into
Open
Conversation
… strand In the drain-and-dispatch shape (auto_stop=False), iter_work / aiter_work ack a claimed item (queued -> starting) *before* yielding it to the handoff owner. If that owner dies before its first heartbeat, the item is left in `starting` with a null heartbeat, and poll(reclaim_older_than_ms=...) only reclaims *unacknowledged* work — so the item is permanently stranded, unreachable by both reclaim and lease-TTL requeue. There is no server-side nack endpoint (only poll/ack/stop/heartbeat), so a pure nack can't be done client-side. Instead add an opt-in defer_ack flag: when True, the poller yields the item still unacknowledged and the consumer / handoff owner performs the ack (and stop) when it commits to processing. A failed handoff then leaves the item recoverable via reclaim_older_than_ms / lease TTL. defer_ack defaults to False (exact current ack-before-yield behavior, fully backward-compatible) and requires auto_stop=False — the dispatch pattern where the consumer owns the lifecycle; pairing it with auto_stop=True raises ValueError, since the poller can't stop an item it deferred acking. Wired through both iter_work/aiter_work and the public poller() entry point. Tested with the existing FakeWork / FakeAsyncWork harness (no live API): the new flag yields unacknowledged, never acks on a body raise (the recoverable strand path), and rejects auto_stop=True — sync and async. Fixes anthropics#1746 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PranavMishra28
marked this pull request as ready for review
July 11, 2026 02:37
Author
|
@arcaputo3 shd fix your issue now |
Author
|
@TomerAberbach you wrote the poller so sending this your way — small opt-in fix (defer_ack) for the drain-mode strand where a handoff that dies before its first heartbeat leaves the work item stuck in starting instead of reclaimable. CI's paused awaiting approval since it's my first PR here; an approve-and-run would surface the checks. whenever you've got a sec, thanks! |
encisoale85-ship-it
approved these changes
Jul 31, 2026
Author
|
Hi @TomerAberbach or @anthropics/sdk team - gently following up on this PR when you have a moment! Could a maintainer please approve the 2 pending workflows and provide a code owner review? Thanks! |
encisoale85-ship-it
approved these changes
Jul 31, 2026
encisoale85-ship-it
approved these changes
Jul 31, 2026
Author
|
I don't think you have the write access, and therefore I am unable to merge the PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
In the poller's drain-and-dispatch shape (
auto_stop=False— the webhook/handoff pattern the docstring describes),iter_work/aiter_workacka claimed item (transitioning itqueued → starting) before yielding it to the process that will actually run it. If that handoff process dies before its first heartbeat, the item is left instartingwith a null heartbeat — andpoll(reclaim_older_than_ms=…)only reclaims unacknowledged work, so the item is reachable by neitherreclaimnor lease-TTL requeue. It's permanently stranded. (This is issue #1746, reported with 29 real items stuck instarting.)Why not a nack
The issue proposed either a deferred-ack mode or a nack/fail API. The
Workresource exposes onlypoll/ack/stop/heartbeat— there is nonack/failendpoint — so a true nack can't be implemented client-side. That leaves deferred ack.The fix
Adds an opt-in
defer_ack: bool = Falsetoiter_work/aiter_workand the publicpoller()entry point:False) — exact current behavior (ack before yield). Fully backward-compatible; no existing caller changes.True— the poller yields the item still unacknowledged (queued); the consumer / handoff owner performs theack(andstop) when it commits to processing. A handoff that dies before acking now leaves the item recoverable viareclaim_older_than_ms/ lease TTL instead of stranded.defer_ack=Truerequiresauto_stop=False(the dispatch pattern where the consumer owns the lifecycle); pairing it withauto_stop=TrueraisesValueError, since the poller can'tstopan item whoseackit deferred. Validated before the first poll, sync and async.How it's tested
Uses the existing
FakeWork/FakeAsyncWorkharness intests/lib/environments/test_poller_method.py— no live API. New tests (sync + async):..._defer_ack_yields_unacked— items are handed off with noack/stopcall by the poller...._defer_ack_does_not_ack_on_body_raise— the strand path: a handoff that fails after the yield leaves the item unacknowledged (i.e. reclaimable), assertingack_calls == []...._defer_ack_requires_auto_stop_false— theValueErrorguard fires before any poll.Full local run for this PR:
uv run pytest tests/lib/(752 passed; the 2 failures intests/lib/test_credentials.pyreproduce identically on cleanmainand are unrelated to this change),ruff check/ruff format --checkclean,mypy .clean (1052 files),pyright0/0/0.Scope / boundary note
The behavior change is entirely in the hand-written
src/anthropic/lib/environments/_poller.py. The 8-line change topoller()inresources/beta/environments/work.pyis a passthrough of the new kwarg;poller()/worker()are maintained custom methods there (they import fromlib/and have this dedicated test file), but if the codegen config needs a matching entry for the new parameter, happy to adjust.What this deliberately does not change
auto_stop=Truerunner path.Fixes #1746