|
1 | 1 | """Typed exceptions raised by the Durable Workflow client and worker. |
2 | 2 |
|
3 | | -Every exception inherits from :class:`DurableWorkflowError`, so callers that |
4 | | -only want to distinguish SDK errors from unrelated failures can catch that |
5 | | -base. More specific subclasses let callers react to particular outcomes — |
6 | | -workflow-not-found, update-rejected, schedule-already-exists — without |
7 | | -parsing server response bodies. |
| 3 | +Every *error* exception inherits from :class:`DurableWorkflowError`, so |
| 4 | +callers that only want to distinguish SDK errors from unrelated failures can |
| 5 | +catch that base. More specific subclasses let callers react to particular |
| 6 | +outcomes — workflow-not-found, update-rejected, schedule-already-exists — |
| 7 | +without parsing server response bodies. |
| 8 | +
|
| 9 | +Cancellation is intentionally *not* in that hierarchy. :class:`WorkflowCancelled` |
| 10 | +and :class:`ActivityCancelled` inherit from :class:`BaseException` directly, |
| 11 | +so a generic ``except Exception:`` block cannot accidentally swallow a |
| 12 | +cancellation signal. Callers that want to handle cancellation must name the |
| 13 | +class explicitly (``except (ActivityCancelled, ...):``). This mirrors the way |
| 14 | +:class:`asyncio.CancelledError` and :class:`KeyboardInterrupt` behave in the |
| 15 | +standard library and avoids the historical mistake called out in |
| 16 | +https://github.com/temporalio/sdk-python/issues/1292. |
8 | 17 | """ |
9 | 18 |
|
10 | 19 | from __future__ import annotations |
@@ -140,19 +149,39 @@ def __init__(self, message: str = "workflow was terminated") -> None: |
140 | 149 | super().__init__(message) |
141 | 150 |
|
142 | 151 |
|
143 | | -class WorkflowCancelled(DurableWorkflowError): |
144 | | - """A workflow was cancelled and finished in the ``cancelled`` state.""" |
| 152 | +class WorkflowCancelled(BaseException): |
| 153 | + """A workflow was cancelled and finished in the ``cancelled`` state. |
| 154 | +
|
| 155 | + Inherits from :class:`BaseException` — not :class:`Exception` — so that a |
| 156 | + generic ``except Exception:`` block cannot accidentally swallow the |
| 157 | + cancellation outcome. Callers that want to treat a cancelled workflow |
| 158 | + differently from a failed one (e.g. to skip alerting) must catch this |
| 159 | + class by name. |
| 160 | + """ |
145 | 161 |
|
146 | 162 | def __init__(self, message: str = "workflow was cancelled") -> None: |
147 | 163 | super().__init__(message) |
148 | 164 |
|
149 | 165 |
|
150 | | -class ActivityCancelled(DurableWorkflowError): |
| 166 | +class ActivityCancelled(BaseException): |
151 | 167 | """An in-flight activity was cancelled. |
152 | 168 |
|
153 | 169 | Raised inside :meth:`durable_workflow.ActivityContext.heartbeat` when the |
154 | 170 | server reports that the owning workflow has asked for cancellation, so the |
155 | 171 | activity can exit cleanly on its next heartbeat. |
| 172 | +
|
| 173 | + Inherits from :class:`BaseException` — not :class:`Exception` — so that a |
| 174 | + user ``except Exception:`` block inside the activity function cannot |
| 175 | + accidentally swallow the cancellation signal. Activities that need to run |
| 176 | + cleanup on cancellation should catch this class by name and re-raise: |
| 177 | +
|
| 178 | + .. code-block:: python |
| 179 | +
|
| 180 | + try: |
| 181 | + await activity.context().heartbeat() |
| 182 | + except ActivityCancelled: |
| 183 | + cleanup() |
| 184 | + raise |
156 | 185 | """ |
157 | 186 |
|
158 | 187 | def __init__(self, message: str = "activity was cancelled") -> None: |
|
0 commit comments