@@ -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,17 +760,39 @@ 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.
752768
753769This default can be changed by providing a list of exception types to ` workflow_failure_exception_types ` when creating a
754770` Worker ` or ` failure_exception_types ` on the ` @workflow.defn ` decorator. If a workflow-thrown exception is an instance
755- of any type in either list, it will fail the workflow instead of the task. This means a value of ` [Exception] ` will
756- cause every exception to fail the workflow instead of the task. Also, as a special case, if
771+ of any type in either list, it will fail the workflow (or update) instead of the workflow task. This means a value of
772+ ` [Exception] ` will cause every exception to fail the workflow instead of the workflow task. Also, as a special case, if
757773` temporalio.workflow.NondeterminismError ` (or any superclass of it) is set, non-deterministic exceptions will fail the
758774workflow. 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