Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/reference/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ same nested shape and input order. Every leaf emits the shared

Use `ctx.saga().run(forward)` for sequential reverse-order compensation.
Register each compensation only after its forward activity completes. The
helper compensates on failure or cooperative cancellation and raises
helper compensates on failure and raises
`SagaCompensationFailed` if compensation itself fails.
Terminal `Client.cancel_workflow` does not resume workflow code to run it.

::: durable_workflow.workflow
19 changes: 11 additions & 8 deletions src/durable_workflow/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,7 @@ async def query(self, query_name: str, args: list[Any] | None = None) -> Any:
return await self._client.query_workflow(self.workflow_id, query_name, args=args)

async def cancel(self, *, reason: str | None = None) -> None:
"""Request graceful cancellation of this workflow. See :meth:`Client.cancel_workflow`."""
"""Close this workflow's current run as cancelled. See :meth:`Client.cancel_workflow`."""
await self._client.cancel_workflow(self.workflow_id, reason=reason)

async def terminate(self, *, reason: str | None = None) -> None:
Expand Down Expand Up @@ -4114,22 +4114,25 @@ async def query_workflow(
)

async def cancel_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
"""Request graceful cancellation of a workflow's current run.
"""Close the current run as cancelled immediately.

Cancellation is cooperative: the server delivers a cancellation signal
that the workflow can observe and handle (e.g. to roll back via a
saga). Compare with :meth:`terminate_workflow`, which is forceful.
Server cancels open tasks and timers; it does not resume workflow code
to run saga or ``finally`` cleanup. :meth:`terminate_workflow` also
closes immediately, with a distinct terminal outcome. Embedded
Laravel's cooperative ``requestCancellation()`` is not yet available
through this service-mode API.
"""
body: dict[str, Any] = {}
if reason is not None:
body["reason"] = reason
await self._request("POST", f"/workflows/{workflow_id}/cancel", json=body, context=workflow_id)

async def terminate_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
"""Forcefully stop a workflow without giving it a chance to clean up.
"""Close the current run as terminated immediately.

Prefer :meth:`cancel_workflow` when the workflow code can implement
graceful shutdown. Termination is an operator escape hatch.
Like :meth:`cancel_workflow`, this does not resume workflow code for
cleanup. Use the distinct terminal outcome when termination is the
appropriate operator action.
"""
body: dict[str, Any] = {}
if reason is not None:
Expand Down
8 changes: 5 additions & 3 deletions src/durable_workflow/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,8 @@ def __init__(
class WorkflowTerminated(DurableWorkflowError):
"""A workflow was terminated by operator action.

Termination is non-gracious and skips normal cleanup, unlike cancellation.
Like terminal cancellation, termination does not resume workflow cleanup.
It records a distinct terminal outcome.
"""

def __init__(self, message: str = "workflow was terminated") -> None:
Expand Down Expand Up @@ -643,8 +644,9 @@ class ActivityCancelled(BaseException):
"""An in-flight activity was cancelled.

Raised inside :meth:`durable_workflow.ActivityContext.heartbeat` when the
server reports that the owning workflow has asked for cancellation, so the
activity can exit cleanly on its next heartbeat.
server reports that the task was revoked or its run was cancelled, so the
activity can exit on its next heartbeat. This permits local activity cleanup,
not durable workflow compensation after the run closes.

Inherits from :class:`BaseException` — not :class:`Exception` — so that a
user ``except Exception:`` block inside the activity function cannot
Expand Down
6 changes: 5 additions & 1 deletion src/durable_workflow/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1698,7 +1698,11 @@ def _accept_message_stream(self, arguments: list[Any]) -> None:

@property
def is_cancellation_requested(self) -> bool:
"""Whether this workflow task requests cooperative cancellation."""
"""Whether this task carries a cooperative cancellation request.

Server's current ``/cancel`` route is terminal and does not set this
flag. Service-mode cooperative cancellation is not yet available.
"""
return self._cancel_requested

def throw_if_cancellation_requested(self) -> None:
Expand Down
Loading