|
1 | 1 | import asyncio |
2 | 2 | from datetime import timedelta |
3 | | -from typing import cast |
4 | 3 |
|
5 | 4 | from temporalio import exceptions, workflow |
6 | 5 |
|
7 | | -from message_passing.waiting_for_handlers_and_compensation import ( |
| 6 | +from message_passing.waiting_for_handlers import ( |
8 | 7 | WorkflowExitType, |
9 | 8 | WorkflowInput, |
| 9 | + WorkflowResult, |
10 | 10 | ) |
11 | | -from message_passing.waiting_for_handlers_and_compensation.activities import ( |
| 11 | +from message_passing.waiting_for_handlers.activities import ( |
12 | 12 | activity_executed_by_update_handler, |
13 | | - activity_executed_by_update_handler_to_perform_compensation, |
14 | | - activity_executed_to_perform_workflow_compensation, |
15 | 13 | ) |
16 | 14 |
|
17 | 15 |
|
18 | | -@workflow.defn |
19 | | -class WaitingForHandlersAndCompensationWorkflow: |
| 16 | +def is_workflow_exit_exception(e: BaseException) -> bool: |
20 | 17 | """ |
21 | | - This Workflow demonstrates how to wait for message handlers to finish and |
22 | | - perform compensation/cleanup: |
| 18 | + True if the exception is of a type that will cause the workflow to exit. |
23 | 19 |
|
24 | | - 1. It ensures that all signal and update handlers have finished before a |
25 | | - successful return, and on failure and cancellation. |
26 | | - 2. The update handler performs any necessary compensation/cleanup when the |
27 | | - workflow is cancelled or fails. |
| 20 | + This is as opposed to exceptions that cause a workflow task failure, which |
| 21 | + are retried automatically by Temporal. |
28 | 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)) |
29 | 26 |
|
30 | | - def __init__(self) -> None: |
31 | | - # 👉 If the workflow exits prematurely, this future will be completed |
32 | | - # with the associated exception as its value. Message handlers can then |
33 | | - # "race" this future against a task performing the message handler's own |
34 | | - # application logic; if this future completes before the message handler |
35 | | - # task then the handler should abort and perform compensation. |
36 | | - self.workflow_exit: asyncio.Future[None] = asyncio.Future() |
37 | | - |
38 | | - # The following two attributes are implementation detail of this sample |
39 | | - # and can be ignored |
40 | | - self._update_started = False |
41 | | - self._update_compensation_done = False |
42 | | - self._workflow_compensation_done = False |
43 | 27 |
|
| 28 | +@workflow.defn |
| 29 | +class WaitingForHandlersWorkflow: |
44 | 30 | @workflow.run |
45 | | - async def run(self, input: WorkflowInput) -> str: |
| 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 | + """ |
46 | 47 | try: |
47 | 48 | # 👉 Use this `try...except` style, instead of waiting for message |
48 | 49 | # handlers to finish in a `finally` block. The reason is that some |
49 | 50 | # exception types cause a workflow task failure as opposed to |
50 | 51 | # workflow exit, in which case we do *not* want to wait for message |
51 | 52 | # handlers to finish. |
52 | | - |
53 | | - # 👉 self._run contains your actual application logic. This is |
54 | | - # implemented in a separate method in order to separate |
55 | | - # "platform-level" concerns (waiting for handlers to finish and |
56 | | - # ensuring that compensation is performed when appropriate) from |
57 | | - # application logic. In this sample, its actual implementation is |
58 | | - # below but contains nothing relevant. |
59 | | - result = await self._run(input) |
60 | | - self.workflow_exit.set_result(None) |
| 53 | + result = await self._my_workflow_application_logic(input) |
61 | 54 | await workflow.wait_condition(workflow.all_handlers_finished) |
62 | 55 | return result |
63 | 56 | # 👉 Catch BaseException since asyncio.CancelledError does not inherit |
64 | 57 | # from Exception. |
65 | 58 | except BaseException as e: |
66 | 59 | if is_workflow_exit_exception(e): |
67 | | - self.workflow_exit.set_exception(e) |
68 | 60 | await workflow.wait_condition(workflow.all_handlers_finished) |
69 | | - await self.workflow_compensation() |
70 | | - self._workflow_compensation_done = True |
71 | 61 | raise |
72 | 62 |
|
73 | | - async def workflow_compensation(self): |
74 | | - await workflow.execute_activity( |
75 | | - activity_executed_to_perform_workflow_compensation, |
76 | | - start_to_close_timeout=timedelta(seconds=10), |
77 | | - ) |
78 | | - self._update_compensation_done = True |
| 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 |
79 | 68 |
|
80 | 69 | @workflow.update |
81 | 70 | async def my_update(self) -> str: |
82 | | - """ |
83 | | - An update handler that handles exceptions raised in its own execution |
84 | | - and in that of the main workflow method. |
85 | | -
|
86 | | - It ensures that: |
87 | | - - Compensation/cleanup is always performed when appropriate |
88 | | - - The update caller gets the update result, or WorkflowUpdateFailedError |
89 | | - """ |
90 | | - # 👉 As with the main workflow method, the update application logic is |
91 | | - # implemented in a separate method in order to separate "platform-level" |
92 | | - # error-handling and compensation concerns from application logic. Note |
93 | | - # that coroutines must be wrapped in tasks in order to use |
94 | | - # workflow.wait. |
95 | | - update_task = asyncio.create_task(self._my_update()) |
96 | | - |
97 | | - # 👉 "Race" the workflow_exit future against the handler's own application |
98 | | - # logic. Always use `workflow.wait` instead of `asyncio.wait` in |
99 | | - # Workflow code: asyncio's version is non-deterministic. |
100 | | - await workflow.wait( # type: ignore |
101 | | - [update_task, self.workflow_exit], return_when=asyncio.FIRST_EXCEPTION |
102 | | - ) |
103 | | - try: |
104 | | - if update_task.done(): |
105 | | - # 👉 The update has finished (whether successfully or not). |
106 | | - # Regardless of whether the main workflow method is about to |
107 | | - # exit or not, the update caller should receive a response |
108 | | - # informing them of the outcome of the update. So return the |
109 | | - # result, or raise the exception that caused the update handler |
110 | | - # to exit. |
111 | | - return await update_task |
112 | | - else: |
113 | | - # 👉 The main workflow method exited prematurely due to an |
114 | | - # error, and this happened before the update finished. Fail the |
115 | | - # update with the workflow exception as cause. |
116 | | - raise exceptions.ApplicationError( |
117 | | - "The update failed because the workflow run exited" |
118 | | - ) from cast(BaseException, self.workflow_exit.exception()) |
119 | | - # 👉 Catch BaseException since asyncio.CancelledError does not inherit |
120 | | - # from Exception. |
121 | | - except BaseException as e: |
122 | | - if is_workflow_exit_exception(e): |
123 | | - try: |
124 | | - await self.my_update_compensation() |
125 | | - except BaseException as e: |
126 | | - raise exceptions.ApplicationError( |
127 | | - "Update compensation failed" |
128 | | - ) from e |
129 | | - raise |
130 | | - |
131 | | - async def my_update_compensation(self): |
132 | | - await workflow.execute_activity( |
133 | | - activity_executed_by_update_handler_to_perform_compensation, |
134 | | - start_to_close_timeout=timedelta(seconds=10), |
135 | | - ) |
136 | | - self._update_compensation_done = True |
137 | | - |
138 | | - @workflow.query |
139 | | - def workflow_compensation_done(self) -> bool: |
140 | | - return self._workflow_compensation_done |
141 | | - |
142 | | - @workflow.query |
143 | | - def update_compensation_done(self) -> bool: |
144 | | - return self._update_compensation_done |
145 | | - |
146 | | - # The following methods are placeholders for the actual application logic |
147 | | - # that you would perform in your main workflow method or update handler. |
148 | | - # Their implementation can be ignored. |
149 | | - |
150 | | - async def _my_update(self) -> str: |
151 | | - # Ignore this method unless you are interested in the implementation |
152 | | - # details of this sample. |
153 | 71 | self._update_started = True |
154 | 72 | await workflow.execute_activity( |
155 | 73 | activity_executed_by_update_handler, |
156 | 74 | start_to_close_timeout=timedelta(seconds=10), |
157 | 75 | ) |
158 | 76 | return "update-result" |
159 | 77 |
|
160 | | - async def _run(self, input: WorkflowInput) -> str: |
161 | | - # Ignore this method unless you are interested in the implementation |
162 | | - # details of this sample. |
| 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. |
163 | 84 |
|
164 | 85 | # Wait until handlers have started, so that we are demonstrating that we |
165 | 86 | # wait for them to finish. |
166 | 87 | await workflow.wait_condition(lambda: self._update_started) |
167 | 88 | if input.exit_type == WorkflowExitType.SUCCESS: |
168 | | - return "workflow-result" |
| 89 | + return WorkflowResult(data="workflow-result") |
169 | 90 | elif input.exit_type == WorkflowExitType.FAILURE: |
170 | 91 | raise exceptions.ApplicationError("deliberately failing workflow") |
171 | 92 | elif input.exit_type == WorkflowExitType.CANCELLATION: |
172 | 93 | # Block forever; the starter will send a workflow cancellation request. |
173 | 94 | await asyncio.Future() |
174 | 95 | raise AssertionError("unreachable") |
175 | | - |
176 | | - |
177 | | -def is_workflow_exit_exception(e: BaseException) -> bool: |
178 | | - # 👉 If you have set additional failure_exception_types you should also |
179 | | - # check for these here. |
180 | | - return isinstance(e, (asyncio.CancelledError, exceptions.FailureError)) |
0 commit comments