|
| 1 | +import asyncio |
| 2 | +from datetime import timedelta |
| 3 | + |
| 4 | +from temporalio import exceptions, workflow |
| 5 | + |
| 6 | +from message_passing.waiting_for_handlers import ( |
| 7 | + WorkflowExitType, |
| 8 | + WorkflowInput, |
| 9 | + WorkflowResult, |
| 10 | +) |
| 11 | +from message_passing.waiting_for_handlers.activities import ( |
| 12 | + activity_executed_by_update_handler, |
| 13 | +) |
| 14 | + |
| 15 | + |
| 16 | +def is_workflow_exit_exception(e: BaseException) -> bool: |
| 17 | + """ |
| 18 | + True if the exception is of a type that will cause the workflow to exit. |
| 19 | +
|
| 20 | + This is as opposed to exceptions that cause a workflow task failure, which |
| 21 | + are retried automatically by Temporal. |
| 22 | + """ |
| 23 | + # 👉 If you have set additional failure_exception_types you should also |
| 24 | + # check for these here. |
| 25 | + return isinstance(e, (asyncio.CancelledError, exceptions.FailureError)) |
| 26 | + |
| 27 | + |
| 28 | +@workflow.defn |
| 29 | +class WaitingForHandlersWorkflow: |
| 30 | + @workflow.run |
| 31 | + async def run(self, input: WorkflowInput) -> WorkflowResult: |
| 32 | + """ |
| 33 | + This workflow.run method demonstrates a pattern that can be used to wait for signal and |
| 34 | + update handlers to finish in the following circumstances: |
| 35 | +
|
| 36 | + - On successful workflow return |
| 37 | + - On workflow cancellation |
| 38 | + - On workflow failure |
| 39 | +
|
| 40 | + Your workflow can also exit via Continue-As-New. In that case you would usually wait for |
| 41 | + the handlers to finish immediately before the call to continue_as_new(); that's not |
| 42 | + illustrated in this sample. |
| 43 | +
|
| 44 | + If you additionally need to perform cleanup or compensation on workflow failure or |
| 45 | + cancellation, see the message_passing/waiting_for_handlers_and_compensation sample. |
| 46 | + """ |
| 47 | + try: |
| 48 | + # 👉 Use this `try...except` style, instead of waiting for message |
| 49 | + # handlers to finish in a `finally` block. The reason is that some |
| 50 | + # exception types cause a workflow task failure as opposed to |
| 51 | + # workflow exit, in which case we do *not* want to wait for message |
| 52 | + # handlers to finish. |
| 53 | + result = await self._my_workflow_application_logic(input) |
| 54 | + await workflow.wait_condition(workflow.all_handlers_finished) |
| 55 | + return result |
| 56 | + # 👉 Catch BaseException since asyncio.CancelledError does not inherit |
| 57 | + # from Exception. |
| 58 | + except BaseException as e: |
| 59 | + if is_workflow_exit_exception(e): |
| 60 | + await workflow.wait_condition(workflow.all_handlers_finished) |
| 61 | + raise |
| 62 | + |
| 63 | + # Methods below this point can be ignored unless you are interested in |
| 64 | + # the implementation details of this sample. |
| 65 | + |
| 66 | + def __init__(self) -> None: |
| 67 | + self._update_started = False |
| 68 | + |
| 69 | + @workflow.update |
| 70 | + async def my_update(self) -> str: |
| 71 | + self._update_started = True |
| 72 | + await workflow.execute_activity( |
| 73 | + activity_executed_by_update_handler, |
| 74 | + start_to_close_timeout=timedelta(seconds=10), |
| 75 | + ) |
| 76 | + return "update-result" |
| 77 | + |
| 78 | + async def _my_workflow_application_logic( |
| 79 | + self, input: WorkflowInput |
| 80 | + ) -> WorkflowResult: |
| 81 | + # The main workflow logic is implemented in a separate method in order |
| 82 | + # to separate "platform-level" concerns (waiting for handlers to finish |
| 83 | + # and error handling) from application logic. |
| 84 | + |
| 85 | + # Wait until handlers have started, so that we are demonstrating that we |
| 86 | + # wait for them to finish. |
| 87 | + await workflow.wait_condition(lambda: self._update_started) |
| 88 | + if input.exit_type == WorkflowExitType.SUCCESS: |
| 89 | + return WorkflowResult(data="workflow-result") |
| 90 | + elif input.exit_type == WorkflowExitType.FAILURE: |
| 91 | + raise exceptions.ApplicationError("deliberately failing workflow") |
| 92 | + elif input.exit_type == WorkflowExitType.CANCELLATION: |
| 93 | + # Block forever; the starter will send a workflow cancellation request. |
| 94 | + await asyncio.Future() |
| 95 | + raise AssertionError("unreachable") |
0 commit comments