Conversation
rgamba
reviewed
Sep 16, 2026
| request.proxyMethod.invoke(request.actionObject) | ||
| } | ||
| } finally { | ||
| interruptedByCancel = registration != null && inFlightActions.exit(registration) |
Member
There was a problem hiding this comment.
I wonder if we need to evaluate wether the action is async and exit the inFlightAction only after the async execution completed rather than synchronously here, otherwise we'll exit immeidately and the action might still be executing async?
| * a thread that has already left its action, so a thread never carries an unreported interrupt out. | ||
| */ | ||
| @Singleton | ||
| class InFlightActions { |
Member
There was a problem hiding this comment.
curious, did you consider doing the cancellation at the workflow level rather than the action level? is this to allow graceful handling of the action cancellation at the workflow layer? otherwise at the workflow level we can reuse the existing workflows in flight registry we currently use for lease renewal
Behind INFLIGHT_CANCELLATION_INTERRUPT (which also requires INFLIGHT_CANCELLATION_CHECKPOINTS), cancelWorkflow interrupts the thread running the cancelled workflow's action in this process. ActionExecutor registers the executing thread per workflow for the duration of a synchronous action; an action that fails from the interrupt ends the workflow as cancelled with no checkpoint, no iteration increment and no retry, while one that completes anyway is checkpointed normally and the boundary check stops the next action. The founding assumed the lease renewer's taskFuture.cancel(true) could be reused; that future is the scheduler thread's, blocked waiting on the execution, so cancelling it never reached the action. Delivery is therefore same-process only: a cancel taken by another worker is observed at the next action boundary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sndre
force-pushed
the
feat/inflight-cancellation-interrupt
branch
from
September 30, 2026 18:58
db94a58 to
9c5fb33
Compare
This branch has not been deployed
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.
Layer 2 of in-flight cancellation: with
INFLIGHT_CANCELLATION_INTERRUPTon (andINFLIGHT_CANCELLATION_CHECKPOINTS, which it requires),cancelWorkflowinterrupts the thread running the cancelled workflow's action in this process, so a blocking action stops instead of running to completion.Mechanism.
ActionExecutorregisters the executing thread per workflow id (InFlightActions) for the duration of a synchronous action.cancelWorkflowinterrupts every registration for that workflow after persisting CANCELLED. An action that fails while an interrupt was delivered ends asWorkflowCancelledException: no checkpoint, no iteration increment, no retry. One that completes anyway is checkpointed normally, counted asinflightCancellationIgnored, and Layer 1's boundary check stops the next action. Per-registration locking keeps an interrupt from landing on a thread that has already left its action, so no pooled thread carries a stray interrupt flag.Two deviations from the founding record, stated here on purpose. The record assumed reusing the lease renewer's
taskFuture.cancel(true); that future belongs to the scheduler thread blocked inhandleTask(...).get(), not to the task-handler thread running the action, so it cannot reach the action and this change interrupts the action thread directly. Consequences: delivery no longer depends onAUTOMATIC_LEASE_RENEWAL, and it is same-process only — a cancel taken by another worker delivers no interrupt and the action is stopped at its next boundary by Layer 1. The opt-in surface is a per-embedder flag, not per-workflow.Not covered: Kotlin suspend actions and future-returning actions are not registered, a cancel landing before an action registers is caught by the boundary check instead, and an action that swallows the interrupt completes (counted as
inflightCancellationIgnored).Tests.
InFlightActionsTest(delivery, two concurrent actions of one workflow, confinement to the cancelled workflow, a 2,000-iteration race between exit and interrupt),ActionExecutorTest(interrupted action ends cancelled with nothing checkpointed; a swallowing action completes with a clear flag),SkipperEngineTest(interrupt with both flags on; none with either off), and an SQLite integ test where a 20 s blocking action is cut short by cancel and the caller seesCancelledWorkflow.Follow-up (pre-existing, not changed here). A signal handler's action that ends in
WorkflowCancelledExceptionstill goes throughsignalWorkflow's success path, which writes RUNNING and reschedules; only the store's version check stops a cancelled workflow from being resurrected. Same hole is reachable from Layer 1's boundary check onmain; needs its own settle guard.Evaluators. Three rounds of Argus / Subtraction / Test-leak; final round clean on all three. Round 1 Argus: exit/interrupt race, one entry per workflow id, untested opt-in — fixed (per-registration lock, list per workflow, flag-off test). Round 2 Argus: confinement to the cancelled workflow and the swallowing-action path untested — fixed with tests. Round 3 non-blocking, handled: registration now skipped unless the flag is on (no per-action cost for non-adopters); the flag's KDoc states the interrupt lands in arbitrary action code and can close an interruptible channel or poison a pooled connection. Kept, per Subtraction: the interrupted-action mapping to
WorkflowCancelledException(it is what prevents a transient checkpoint and a retry of the interrupted action).🤖 Generated with Claude Code