diff --git a/docs/reference/workflow.md b/docs/reference/workflow.md index e9d02f2..91cf68e 100644 --- a/docs/reference/workflow.md +++ b/docs/reference/workflow.md @@ -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 diff --git a/src/durable_workflow/client.py b/src/durable_workflow/client.py index bf7665d..b04368e 100644 --- a/src/durable_workflow/client.py +++ b/src/durable_workflow/client.py @@ -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: @@ -4114,11 +4114,13 @@ 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: @@ -4126,10 +4128,11 @@ async def cancel_workflow(self, workflow_id: str, *, reason: str | None = None) 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: diff --git a/src/durable_workflow/errors.py b/src/durable_workflow/errors.py index a87e84b..a7f858f 100644 --- a/src/durable_workflow/errors.py +++ b/src/durable_workflow/errors.py @@ -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: @@ -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 diff --git a/src/durable_workflow/workflow.py b/src/durable_workflow/workflow.py index 98345fe..2650fbf 100644 --- a/src/durable_workflow/workflow.py +++ b/src/durable_workflow/workflow.py @@ -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: