Skip to content

Commit efeede8

Browse files
docs: correct cancellation and cleanup contract (#70)
Co-authored-by: Durable Workflow <support@durable-workflow.com>
1 parent 178e966 commit efeede8

4 files changed

Lines changed: 23 additions & 13 deletions

File tree

‎docs/reference/workflow.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ same nested shape and input order. Every leaf emits the shared
1717

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

2324
::: durable_workflow.workflow

‎src/durable_workflow/client.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,7 +1316,7 @@ async def query(self, query_name: str, args: list[Any] | None = None) -> Any:
13161316
return await self._client.query_workflow(self.workflow_id, query_name, args=args)
13171317

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

13221322
async def terminate(self, *, reason: str | None = None) -> None:
@@ -4114,22 +4114,25 @@ async def query_workflow(
41144114
)
41154115

41164116
async def cancel_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
4117-
"""Request graceful cancellation of a workflow's current run.
4117+
"""Close the current run as cancelled immediately.
41184118
4119-
Cancellation is cooperative: the server delivers a cancellation signal
4120-
that the workflow can observe and handle (e.g. to roll back via a
4121-
saga). Compare with :meth:`terminate_workflow`, which is forceful.
4119+
Server cancels open tasks and timers; it does not resume workflow code
4120+
to run saga or ``finally`` cleanup. :meth:`terminate_workflow` also
4121+
closes immediately, with a distinct terminal outcome. Embedded
4122+
Laravel's cooperative ``requestCancellation()`` is not yet available
4123+
through this service-mode API.
41224124
"""
41234125
body: dict[str, Any] = {}
41244126
if reason is not None:
41254127
body["reason"] = reason
41264128
await self._request("POST", f"/workflows/{workflow_id}/cancel", json=body, context=workflow_id)
41274129

41284130
async def terminate_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
4129-
"""Forcefully stop a workflow without giving it a chance to clean up.
4131+
"""Close the current run as terminated immediately.
41304132
4131-
Prefer :meth:`cancel_workflow` when the workflow code can implement
4132-
graceful shutdown. Termination is an operator escape hatch.
4133+
Like :meth:`cancel_workflow`, this does not resume workflow code for
4134+
cleanup. Use the distinct terminal outcome when termination is the
4135+
appropriate operator action.
41334136
"""
41344137
body: dict[str, Any] = {}
41354138
if reason is not None:

‎src/durable_workflow/errors.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,8 @@ def __init__(
581581
class WorkflowTerminated(DurableWorkflowError):
582582
"""A workflow was terminated by operator action.
583583
584-
Termination is non-gracious and skips normal cleanup, unlike cancellation.
584+
Like terminal cancellation, termination does not resume workflow cleanup.
585+
It records a distinct terminal outcome.
585586
"""
586587

587588
def __init__(self, message: str = "workflow was terminated") -> None:
@@ -643,8 +644,9 @@ class ActivityCancelled(BaseException):
643644
"""An in-flight activity was cancelled.
644645
645646
Raised inside :meth:`durable_workflow.ActivityContext.heartbeat` when the
646-
server reports that the owning workflow has asked for cancellation, so the
647-
activity can exit cleanly on its next heartbeat.
647+
server reports that the task was revoked or its run was cancelled, so the
648+
activity can exit on its next heartbeat. This permits local activity cleanup,
649+
not durable workflow compensation after the run closes.
648650
649651
Inherits from :class:`BaseException` — not :class:`Exception` — so that a
650652
user ``except Exception:`` block inside the activity function cannot

‎src/durable_workflow/workflow.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1698,7 +1698,11 @@ def _accept_message_stream(self, arguments: list[Any]) -> None:
16981698

16991699
@property
17001700
def is_cancellation_requested(self) -> bool:
1701-
"""Whether this workflow task requests cooperative cancellation."""
1701+
"""Whether this task carries a cooperative cancellation request.
1702+
1703+
Server's current ``/cancel`` route is terminal and does not set this
1704+
flag. Service-mode cooperative cancellation is not yet available.
1705+
"""
17021706
return self._cancel_requested
17031707

17041708
def throw_if_cancellation_requested(self) -> None:

0 commit comments

Comments
 (0)