Skip to content

Commit 9ae32ba

Browse files
committed
Add documentation of signal and update handlers
1 parent e6aadd3 commit 9ae32ba

File tree

1 file changed

+50
-14
lines changed

1 file changed

+50
-14
lines changed

README.md

+50-14
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ informal introduction to the features and their implementation.
6565
- [Asyncio Cancellation](#asyncio-cancellation)
6666
- [Workflow Utilities](#workflow-utilities)
6767
- [Exceptions](#exceptions)
68+
- [Signal and update handlers](#signal-and-update-handlers)
6869
- [External Workflows](#external-workflows)
6970
- [Testing](#testing)
7071
- [Automatic Time Skipping](#automatic-time-skipping)
@@ -581,28 +582,35 @@ Here are the decorators that can be applied:
581582
* The purpose of this decorator is to allow operations involving workflow arguments to be performed in the `__init__`
582583
method, before any signal or update handler has a chance to execute.
583584
* `@workflow.signal` - Defines a method as a signal
584-
* Can be defined on an `async` or non-`async` function at any hierarchy depth, but if decorated method is overridden,
585-
the override must also be decorated
586-
* The method's arguments are the signal's arguments
587-
* Can have a `name` param to customize the signal name, otherwise it defaults to the unqualified method name
585+
* Can be defined on an `async` or non-`async` method at any point in the class hierarchy, but if the decorated method
586+
is overridden, then the override must also be decorated.
587+
* The method's arguments are the signal's arguments.
588+
* Return value is ignored.
589+
* May mutate workflow state, and make calls to other workflow APIs like starting activities, etc.
590+
* Can have a `name` param to customize the signal name, otherwise it defaults to the unqualified method name.
588591
* Can have `dynamic=True` which means all otherwise unhandled signals fall through to this. If present, cannot have
589592
`name` argument, and method parameters must be `self`, a string signal name, and a
590593
`Sequence[temporalio.common.RawValue]`.
591594
* Non-dynamic method can only have positional arguments. Best practice is to only take a single argument that is an
592595
object/dataclass of fields that can be added to as needed.
593-
* Return value is ignored
594-
* `@workflow.query` - Defines a method as a query
595-
* All the same constraints as `@workflow.signal` but should return a value
596-
* Should not be `async`
597-
* Temporal queries should never mutate anything in the workflow or call any calls that would mutate the workflow
596+
* See [Signal and update handlers](#signal-and-update-handlers) below
598597
* `@workflow.update` - Defines a method as an update
599-
* May both accept as input and return a value
598+
* Can be defined on an `async` or non-`async` method at any point in the class hierarchy, but if the decorated method
599+
is overridden, then the override must also be decorated.
600+
* May accept input and return a value
601+
* The method's arguments are the update's arguments.
600602
* May be `async` or non-`async`
601603
* May mutate workflow state, and make calls to other workflow APIs like starting activities, etc.
602-
* Also accepts the `name` and `dynamic` parameters like signals and queries, with the same semantics.
604+
* Also accepts the `name` and `dynamic` parameters like signal, with the same semantics.
603605
* Update handlers may optionally define a validator method by decorating it with `@update_handler_method.validator`.
604606
To reject an update before any events are written to history, throw an exception in a validator. Validators cannot
605607
be `async`, cannot mutate workflow state, and return nothing.
608+
* See [Signal and update handlers](#signal-and-update-handlers) below
609+
* `@workflow.query` - Defines a method as a query
610+
* Should return a value
611+
* Should not be `async`
612+
* Temporal queries should never mutate anything in the workflow or call any calls that would mutate the workflow
613+
* Also accepts the `name` and `dynamic` parameters like signal and update, with the same semantics.
606614

607615
#### Running
608616

@@ -705,9 +713,15 @@ deterministic:
705713

706714
#### Asyncio Cancellation
707715

708-
Cancellation is done the same way as `asyncio`. Specifically, a task can be requested to be cancelled but does not
709-
necessarily have to respect that cancellation immediately. This also means that `asyncio.shield()` can be used to
710-
protect against cancellation. The following tasks, when cancelled, perform a Temporal cancellation:
716+
Cancellation is done using `asyncio` [task cancellation](https://docs.python.org/3/library/asyncio-task.html#task-cancellation).
717+
This means that tasks are requested to be cancelled but can catch the
718+
[`asyncio.CancelledError`](https://docs.python.org/3/library/asyncio-exceptions.html#asyncio.CancelledError), thus
719+
allowing them to perform some cleanup before allowing the cancellation to proceed (i.e. re-raising the error), or to
720+
deny the cancellation entirely. It also means that
721+
[`asyncio.shield()`](https://docs.python.org/3/library/asyncio-task.html#shielding-from-cancellation) can be used to
722+
protect tasks against cancellation.
723+
724+
The following tasks, when cancelled, perform a Temporal cancellation:
711725

712726
* Activities - when the task executing an activity is cancelled, a cancellation request is sent to the activity
713727
* Child workflows - when the task starting or executing a child workflow is cancelled, a cancellation request is sent to
@@ -746,6 +760,8 @@ While running in a workflow, in addition to features documented elsewhere, the f
746760
be marked non-retryable or include details as needed.
747761
* Other exceptions that come from activity execution, child execution, cancellation, etc are already instances of
748762
`FailureError` and will fail the workflow when uncaught.
763+
* Update handlers are special: an instance of `temporalio.exceptions.FailureError` raised in an update handler will fail
764+
the update instead of failing the workflow.
749765
* All other exceptions fail the "workflow task" which means the workflow will continually retry until the workflow is
750766
fixed. This is helpful for bad code or other non-predictable exceptions. To actually fail the workflow, use an
751767
`ApplicationError` as mentioned above.
@@ -757,6 +773,26 @@ cause every exception to fail the workflow instead of the task. Also, as a speci
757773
`temporalio.workflow.NondeterminismError` (or any superclass of it) is set, non-deterministic exceptions will fail the
758774
workflow. WARNING: These settings are experimental.
759775

776+
#### Signal and update handlers
777+
778+
Signal and update handlers are defined using decorated methods as shown in the example [above](#definition). Client code
779+
sends signals and updates using `workflow_handle.signal`, `workflow_handle.execute_update`, or
780+
`workflow_handle.start_update`. When the workflow receives one of these requests, it starts an `asyncio.Task` executing
781+
the corresponding handler method with the argument(s) from the request.
782+
783+
The handler methods may be `async def` and can do all the async operations described above (e.g. invoking activities and
784+
child workflows, and waiting on timers and conditions). Notice that this means that handler tasks will be executing
785+
concurrently with respect to each other and the main workflow task. Use
786+
[asyncio.Lock](https://docs.python.org/3/library/asyncio-sync.html#lock) and
787+
[asyncio.Semaphore](https://docs.python.org/3/library/asyncio-sync.html#semaphore) if necessary.
788+
789+
Your main workflow task may finish as a result of successful completion, cancellation, continue-as-new, or failure. You
790+
should ensure that all in-progress signal and update handler tasks have finished before this happens; if you do not, you
791+
will see a warning (the warning can be disabled via the `workflow.signal`/`workflow.update` decorators). One way to
792+
ensure that handler tasks have finished is to wait on the `workflow.all_handlers_finished` condition:
793+
```python
794+
await workflow.wait_condition(lambda: workflow.all_handlers_finished())
795+
```
760796
#### External Workflows
761797

762798
* `workflow.get_external_workflow_handle()` inside a workflow returns a handle to interact with another workflow

0 commit comments

Comments
 (0)