Repository navigation
Add explicit cancellation (preview) - #264
Merged
Merged
Conversation
With explicit cancellation, Restate cancelling an invocation neither fails
the handler's awaits nor cancels the calls it made: the handler finds out
by awaiting cancellation(ctx), and decides what to stop.
@explicit_cancellation
@greeter.handler()
async def greet(ctx, name):
done, _ = await restate.select(work=ctx.service_call(slow, arg=name), cancelled=cancellation(ctx))
if done == "cancelled":
await cancel_previous_calls(ctx) # the calls still in flight
raise TerminalError("cancelled", status_code=409)
In restate._preview, next to explicit_recovery: on a handler, or a whole
service, object or workflow. The VM disables shared-core's implicit
cancellation; the context keeps the attempt's calls that may be in flight
(only with the option on), which cancel_previous_calls cancels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RePHkTUvGceAzHBUPULRvy
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed the explicit cancellation preview end to end: the Python API, the context bookkeeping, the PyVM option wiring into shared core 8.3.0, and the integration tests.
- Preview API:
explicit_cancellation,cancellation(ctx)andcancel_previous_calls(ctx)inrestate._preview. The decorator follows the same service/handler pattern asexplicit_recovery. - VM option:
PyVM(..., explicit_cancellation=True)maps toImplicitCancellationOption::Disabled. Otherwise it keeps the shared-core default, so existing handlers behave exactly as before. - In-flight call tracking:
create_call_future(the only call path, scoped calls included) records calls only when the option is on, and drops completed ones as new calls are made. One-way sends are not tracked, which matches the shared-core defaultcancel_children_one_way_calls: false. - Cancellation future:
cancellation(ctx)is cached on the context, so taking the notification once is safe however many times the future is awaited. With implicit cancellation disabled,do_progressno longer addsCANCEL_NOTIFICATION_HANDLEitself, so nothing else consumes the notification. - Replay determinism:
cancel_previous_callsdecides what to cancel withis_completed(). I checked shared core 8.3.0: notifications are moved fromto_processtoreadyone at a time, in journal order, and stop at the first resolution. So the set of calls it cancels, and thesys_cancelcommands it journals, come out the same on replay. - Tests: these cover the configuration surface, cancelling a call after a cancellation, the case where completed calls leave nothing to cancel, and the implicit-mode behaviour, all against the Docker harness.
ruffandpyrightpass locally on the changed files.
claude-opus-5-5 | 𝕏
Like every other option: @svc.handler(explicit_cancellation=True), a workflow's main()/handler() too, and Service/VirtualObject/Workflow(..., explicit_cancellation=True) for all their handlers. The restate._preview.explicit_cancellation decorator is gone; cancellation() and cancel_previous_calls() stay in restate._preview. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RePHkTUvGceAzHBUPULRvy
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the last review, explicit_cancellation has moved from a restate._preview decorator to a keyword option on the service constructors and handler decorators.
- Removed the
explicit_cancellationdecorator:restate._previewnow exports onlycancellation(ctx)andcancel_previous_calls(ctx). Their docstrings and theTerminalErrormessage now point toexplicit_cancellation=True, and no references to the old decorator remain anywhere in the repo. - Added the
explicit_cancellationkey to the service constructors:Service,VirtualObjectandWorkflowstore it in place of the hardcodedFalse.server.pystill applieshandler.explicit_cancellation or service.explicit_cancellation. - Added the key to the handler decorators:
Service.handler,VirtualObject.handler,Workflow.mainandWorkflow.handlerall pass it tomake_handlerby keyword, as its last parameter. Existing positional call sites keep their argument order, and those three files contain the onlymake_handlercallers. - Updated the tests: the configuration test now covers the constructor key, a virtual object handler, and a workflow's
main()and shared handler. A newon_one_handlerservice tests the handler-level key against the server.ruff,ruff format --checkandpyrightpass, and so do the configuration tests that don't need Docker.
claude-opus-5-5 | 𝕏
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.

Explicit cancellation (preview)
By default, when Restate cancels an invocation, the cancellation fails whatever the handler awaits next, and the calls it made are cancelled with it. With explicit cancellation, neither happens. The handler finds out by awaiting
cancellation(ctx), and decides what to stop and how to finish:This is useful when cancellation must interrupt work that isn't a Restate operation (an HTTP request to a model API, say), or must leave state consistent before the handler ends. It mirrors the TypeScript SDK's
explicitCancellation.Changes
explicit_cancellation=True, a key like the other options:handler(), and a workflow'smain().Service,VirtualObjectandWorkflowconstructors, for all their handlers.restate._preview:cancellation(ctx)returns a future that completes when the invocation is cancelled.cancel_previous_calls(ctx)cancels the attempt's calls that are still in flight.src/lib.rs:PyVM(..., explicit_cancellation=)disables shared-core's implicit cancellation.ServerInvocationContext:The key is marked as preview in its docstring. A follow-up PR gives
explicit_recoverythe same key form, keeping its decorator as a deprecated alias, so the two preview options are set the same way.Tests
tests/explicit_cancellation.py:main()and its shared handlers).cancellation(ctx)is refused with a terminal error.This PR is a prerequisite of #262 (session objects and agents), whose agents use it to stop a turn, including a model request in flight.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RePHkTUvGceAzHBUPULRvy