harden(listen): release held finalization leases on shutdown cancel (follow-up to #10066)#10044
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Verified the fix end-to-end against the close-after-104 regression from #9995. Strong work — the ownership model here is the right shape. What I confirmed locally (cloned head
Static review notes (all clean):
The failure-class definition ( Flagging for a human maintainer sign-off before merge rather than approving myself: this PR changes a durable-state ownership boundary (claimed Firestore job ledger ↔ pusher session lifetime) on a production finalization path, and the lease-release-on-shutdown behavior is the kind of correctness call that benefits from a maintainer's eyes even though the tests are persuasive. Not a request for changes — the implementation and validation look sound. One minor (non-blocking) observation for whoever lands it: the by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
…n-session close
Inline/BYOK finalization handed off over the listener->pusher WebSocket
(opcode 104) ran as a connection-scoped task: pusher session cleanup
cancelled it, and CancelledError bypassed the retry/dead-letter handlers,
leaving the claimed job leased until a later session or lease expiry.
Claimed finalization jobs are durable work owned by the Firestore job
ledger, so they now run detached from the session's cancellable task set:
- opcode-104 dispatch spawns into a module-level detached registry that
session cleanup never cancels; the connection-scoped spawn()/bg_tasks
plumbing had no other user and is removed.
- send_result is best-effort: results are dropped once the origin socket
is gone; the ledger is the durable outcome and a later re-request
observes it via claim status.
- residual cancellation (process shutdown) now requeues a held lease via
mark_finalization_retryable('worker_shutdown') instead of stranding the
job leased; the lease-epoch guard makes it a no-op after completion.
Regression tests drive the real _websocket_util_trigger with a
close-after-104 session and prove the claimed job converges to completed
without a later live session, and that cancellation mid-lease requeues
the job.
Fixes BasedHardware#9995.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Names the contract violated by BasedHardware#9995 and by the earlier retail fixes (BasedHardware#9648, BasedHardware#9704): durable ledger-owned work, once claimed, must not inherit the lifetime of the connection or session that dispatched it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up on BasedHardware#10044: the detached registry is bounded in practice by Firestore job claims, but expose its size as pusher_detached_finalization_tasks so any future inline fan-out is observable before it needs a cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3a6870d to
481c466
Compare
|
Thanks for the thorough verification @Git-on-my-level. Addressed the one observation: pushed Branch is also rebased onto current |
…nline-finalization # Conflicts: # backend/routers/pusher.py
|
@undivisible Updated as requested:
Ready for re-review. |
Summary
Follow-up to #10066, which landed the core #9995 fix (process-scope ownership of claimed finalization via
start_background_task, best-effort result callback). This PR ships the residual hardening on top of that mechanism:except asyncio.CancelledErrorin_process_conversation_taskconverts the only cancellation that can still reach a claimed job — the process-shutdown drain (drain_background_taskscancels tracked tasks) — into an explicitmark_finalization_retryable(..., 'worker_shutdown')underasyncio.shield, so a restart requeues the held lease instead of waiting out lease expiry. Lease-epoch fencing keeps the release a no-op after completion.PUSHER_DETACHED_FINALIZATION_TASKSgauge (inc/dec around the task body) — the observability Git-on-my-level's earlier review asked for, surviving fix(listen): preserve finalization after source close #10066's mechanism change.send_resultreturns early when the socket is not CONNECTED, complementing fix(listen): preserve finalization after source close #10066's narrowed exception handling; the regression test asserts the frame is dropped, not attempted._websocket_util_triggerproving the claimed job converges tocompletedwith no later live session, plus a mid-lease cancel proving theworker_shutdownrequeue.Relates to #9995 (core fix landed in #10066; this completes its hardening tail).
Failure class
Failure-Class: new
FC-session-scoped-durable-work: durable ledger-owned work, once claimed, must not inherit the lifetime of the connection or session that dispatched it. #9648 (durable job ledger), #9704 (inline terminal state), and #10066 (session-close cancellation) were all retail fixes at this boundary; the definition names the class so recurrence is declared, not rediscovered.Durable guard (as of current head)
start_background_task(process scope, tracked, drained at shutdown) — this PR does not carry a private detached registry; the earlier detach implementation was superseded by fix(listen): preserve finalization after source close #10066 and dropped in the merge.CancelledErrorbypasses the retry/dead-letter handlers (it is aBaseException) and strands the claimed job leased until expiry. The new handler is the missing half of fix(listen): preserve finalization after source close #10066's ownership move.Product invariants affected
none (verified with
scripts/pr-preflight --suggest)Validation
pusher.py/metrics.pyreplaced with origin/main's, tests kept):pytest tests/unit/test_listen_finalization_cloud_tasks.py -k "survives or requeues_the_held_lease"→ 4 failed (lease never released on cancel; gauge/no-send assertions unmet). Restored branch code → same selection passes.tests/unit/test_listen_finalization_cloud_tasks.py→ 72 passed (includes fix(listen): preserve finalization after source close #10066's own tests —test_pusher_keeps_a_completed_job_terminal_when_source_result_delivery_failsstill green with the pre-check in place).origin/main(95ca0fa); post-merge diff vs main is exactly the residual surface:backend/routers/pusher.py+31/−1, gauge inutils/metrics.py, FC definition, +127 test lines.CI checks on this head await maintainer workflow-run approval (fork PR) — nothing reports until then.
Not exercised live: a real listen→pusher deployment restarting mid-finalization (no live services here); the mid-lease cancel test encodes that timing hermetically.
🤖 Generated with Claude Code