azure-functions-durable 1.2.9__py3-none-any.whl → 1.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure/durable_functions/__init__.py +81 -81
- azure/durable_functions/constants.py +9 -9
- azure/durable_functions/decorators/__init__.py +3 -3
- azure/durable_functions/decorators/durable_app.py +249 -249
- azure/durable_functions/decorators/metadata.py +109 -109
- azure/durable_functions/entity.py +125 -125
- azure/durable_functions/models/DurableEntityContext.py +201 -201
- azure/durable_functions/models/DurableHttpRequest.py +58 -58
- azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
- azure/durable_functions/models/DurableOrchestrationClient.py +781 -781
- azure/durable_functions/models/DurableOrchestrationContext.py +722 -707
- azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
- azure/durable_functions/models/EntityStateResponse.py +23 -23
- azure/durable_functions/models/FunctionContext.py +7 -7
- azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
- azure/durable_functions/models/OrchestratorState.py +117 -116
- azure/durable_functions/models/PurgeHistoryResult.py +33 -33
- azure/durable_functions/models/ReplaySchema.py +8 -8
- azure/durable_functions/models/RetryOptions.py +69 -69
- azure/durable_functions/models/RpcManagementOptions.py +86 -86
- azure/durable_functions/models/Task.py +426 -426
- azure/durable_functions/models/TaskOrchestrationExecutor.py +346 -336
- azure/durable_functions/models/TokenSource.py +56 -56
- azure/durable_functions/models/__init__.py +24 -24
- azure/durable_functions/models/actions/Action.py +23 -23
- azure/durable_functions/models/actions/ActionType.py +18 -18
- azure/durable_functions/models/actions/CallActivityAction.py +41 -41
- azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
- azure/durable_functions/models/actions/CallEntityAction.py +46 -46
- azure/durable_functions/models/actions/CallHttpAction.py +35 -35
- azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
- azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
- azure/durable_functions/models/actions/CompoundAction.py +35 -35
- azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
- azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
- azure/durable_functions/models/actions/NoOpAction.py +35 -35
- azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
- azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
- azure/durable_functions/models/actions/WhenAllAction.py +14 -14
- azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
- azure/durable_functions/models/actions/__init__.py +24 -24
- azure/durable_functions/models/entities/EntityState.py +74 -74
- azure/durable_functions/models/entities/OperationResult.py +76 -76
- azure/durable_functions/models/entities/RequestMessage.py +53 -53
- azure/durable_functions/models/entities/ResponseMessage.py +48 -48
- azure/durable_functions/models/entities/Signal.py +62 -62
- azure/durable_functions/models/entities/__init__.py +17 -17
- azure/durable_functions/models/history/HistoryEvent.py +92 -92
- azure/durable_functions/models/history/HistoryEventType.py +27 -27
- azure/durable_functions/models/history/__init__.py +8 -8
- azure/durable_functions/models/utils/__init__.py +7 -7
- azure/durable_functions/models/utils/entity_utils.py +103 -91
- azure/durable_functions/models/utils/http_utils.py +69 -69
- azure/durable_functions/models/utils/json_utils.py +56 -56
- azure/durable_functions/orchestrator.py +71 -71
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/LICENSE +21 -21
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/METADATA +58 -58
- azure_functions_durable-1.2.10.dist-info/RECORD +100 -0
- tests/models/test_DecoratorMetadata.py +135 -135
- tests/models/test_Decorators.py +107 -107
- tests/models/test_DurableOrchestrationBindings.py +68 -68
- tests/models/test_DurableOrchestrationClient.py +730 -730
- tests/models/test_DurableOrchestrationContext.py +102 -102
- tests/models/test_DurableOrchestrationStatus.py +59 -59
- tests/models/test_OrchestrationState.py +28 -28
- tests/models/test_RpcManagementOptions.py +79 -79
- tests/models/test_TokenSource.py +10 -10
- tests/orchestrator/models/OrchestrationInstance.py +18 -18
- tests/orchestrator/orchestrator_test_utils.py +130 -130
- tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
- tests/orchestrator/test_call_http.py +235 -176
- tests/orchestrator/test_continue_as_new.py +67 -67
- tests/orchestrator/test_create_timer.py +126 -126
- tests/orchestrator/test_entity.py +395 -395
- tests/orchestrator/test_external_event.py +53 -53
- tests/orchestrator/test_fan_out_fan_in.py +175 -175
- tests/orchestrator/test_is_replaying_flag.py +101 -101
- tests/orchestrator/test_retries.py +308 -308
- tests/orchestrator/test_sequential_orchestrator.py +841 -841
- tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
- tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
- tests/orchestrator/test_serialization.py +30 -30
- tests/orchestrator/test_sub_orchestrator.py +95 -95
- tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
- tests/orchestrator/test_task_any.py +60 -60
- tests/tasks/tasks_test_utils.py +17 -17
- tests/tasks/test_new_uuid.py +34 -34
- tests/test_utils/ContextBuilder.py +174 -174
- tests/test_utils/EntityContextBuilder.py +56 -56
- tests/test_utils/constants.py +1 -1
- tests/test_utils/json_utils.py +30 -30
- tests/test_utils/testClasses.py +56 -56
- tests/utils/__init__.py +1 -0
- tests/utils/test_entity_utils.py +24 -0
- azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
- azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
- azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/WHEEL +0 -0
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/top_level.txt +0 -0
|
@@ -1,426 +1,426 @@
|
|
|
1
|
-
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
|
|
2
|
-
from azure.durable_functions.models.actions.CompoundAction import CompoundAction
|
|
3
|
-
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
4
|
-
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
5
|
-
from azure.durable_functions.models.actions.Action import Action
|
|
6
|
-
from azure.durable_functions.models.actions.WhenAnyAction import WhenAnyAction
|
|
7
|
-
from azure.durable_functions.models.actions.WhenAllAction import WhenAllAction
|
|
8
|
-
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
9
|
-
|
|
10
|
-
import enum
|
|
11
|
-
from typing import Any, List, Optional, Set, Type, Union
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TaskState(enum.Enum):
|
|
15
|
-
"""The possible states that a Task can be in."""
|
|
16
|
-
|
|
17
|
-
RUNNING = 0
|
|
18
|
-
SUCCEEDED = 1
|
|
19
|
-
FAILED = 2
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class TaskBase:
|
|
23
|
-
"""The base class of all Tasks.
|
|
24
|
-
|
|
25
|
-
Contains shared logic that drives all of its sub-classes. Should never be
|
|
26
|
-
instantiated on its own.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self, id_: Union[int, str], actions: Union[List[Action], Action]):
|
|
30
|
-
"""Initialize the TaskBase.
|
|
31
|
-
|
|
32
|
-
Parameters
|
|
33
|
-
----------
|
|
34
|
-
id_ : int
|
|
35
|
-
An ID for the task
|
|
36
|
-
actions : List[Any]
|
|
37
|
-
The list of DF actions representing this Task.
|
|
38
|
-
Needed for reconstruction in the extension.
|
|
39
|
-
"""
|
|
40
|
-
self.id: Union[int, str] = id_
|
|
41
|
-
self.state = TaskState.RUNNING
|
|
42
|
-
self.parent: Optional[CompoundTask] = None
|
|
43
|
-
self._api_name: str
|
|
44
|
-
|
|
45
|
-
api_action: Union[Action, Type[CompoundAction]]
|
|
46
|
-
if isinstance(actions, list):
|
|
47
|
-
if len(actions) == 1:
|
|
48
|
-
api_action = actions[0]
|
|
49
|
-
else:
|
|
50
|
-
api_action = CompoundAction
|
|
51
|
-
else:
|
|
52
|
-
api_action = actions
|
|
53
|
-
|
|
54
|
-
self._api_name = api_action.__class__.__name__
|
|
55
|
-
|
|
56
|
-
self.result: Any = None
|
|
57
|
-
self.action_repr: Union[List[Action], Action] = actions
|
|
58
|
-
self.is_played = False
|
|
59
|
-
self._is_scheduled_flag = False
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def _is_scheduled(self) -> bool:
|
|
63
|
-
return self._is_scheduled_flag
|
|
64
|
-
|
|
65
|
-
def _set_is_scheduled(self, is_scheduled: bool):
|
|
66
|
-
self._is_scheduled_flag = is_scheduled
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def is_completed(self) -> bool:
|
|
70
|
-
"""Get indicator of whether the task completed.
|
|
71
|
-
|
|
72
|
-
Note that completion is not equivalent to success.
|
|
73
|
-
"""
|
|
74
|
-
return not (self.state is TaskState.RUNNING)
|
|
75
|
-
|
|
76
|
-
def set_is_played(self, is_played: bool):
|
|
77
|
-
"""Set the is_played flag for the Task.
|
|
78
|
-
|
|
79
|
-
Needed for updating the orchestrator's is_replaying flag.
|
|
80
|
-
|
|
81
|
-
Parameters
|
|
82
|
-
----------
|
|
83
|
-
is_played : bool
|
|
84
|
-
Whether the latest event for this Task has been played before.
|
|
85
|
-
"""
|
|
86
|
-
self.is_played = is_played
|
|
87
|
-
|
|
88
|
-
def change_state(self, state: TaskState):
|
|
89
|
-
"""Transition a running Task to a terminal state: success or failure.
|
|
90
|
-
|
|
91
|
-
Parameters
|
|
92
|
-
----------
|
|
93
|
-
state : TaskState
|
|
94
|
-
The terminal state to assign to this Task
|
|
95
|
-
|
|
96
|
-
Raises
|
|
97
|
-
------
|
|
98
|
-
Exception
|
|
99
|
-
When the input state is RUNNING
|
|
100
|
-
"""
|
|
101
|
-
if state is TaskState.RUNNING:
|
|
102
|
-
raise Exception("Cannot change Task to the RUNNING state.")
|
|
103
|
-
self.state = state
|
|
104
|
-
|
|
105
|
-
def set_value(self, is_error: bool, value: Any):
|
|
106
|
-
"""Set the value of this Task: either an exception of a result.
|
|
107
|
-
|
|
108
|
-
Parameters
|
|
109
|
-
----------
|
|
110
|
-
is_error : bool
|
|
111
|
-
Whether the value represents an exception of a result.
|
|
112
|
-
value : Any
|
|
113
|
-
The value of this Task
|
|
114
|
-
|
|
115
|
-
Raises
|
|
116
|
-
------
|
|
117
|
-
Exception
|
|
118
|
-
When the Task failed but its value was not an Exception
|
|
119
|
-
"""
|
|
120
|
-
new_state = self.state
|
|
121
|
-
if is_error:
|
|
122
|
-
if not isinstance(value, Exception):
|
|
123
|
-
if not (isinstance(value, TaskBase) and isinstance(value.result, Exception)):
|
|
124
|
-
err_message = f"Task ID {self.id} failed but it's value was not an Exception"
|
|
125
|
-
raise Exception(err_message)
|
|
126
|
-
new_state = TaskState.FAILED
|
|
127
|
-
else:
|
|
128
|
-
new_state = TaskState.SUCCEEDED
|
|
129
|
-
self.change_state(new_state)
|
|
130
|
-
self.result = value
|
|
131
|
-
self.propagate()
|
|
132
|
-
|
|
133
|
-
def propagate(self):
|
|
134
|
-
"""Notify parent Task of this Task's state change."""
|
|
135
|
-
has_completed = not (self.state is TaskState.RUNNING)
|
|
136
|
-
has_parent = not (self.parent is None)
|
|
137
|
-
if has_completed and has_parent:
|
|
138
|
-
self.parent.handle_completion(self)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class CompoundTask(TaskBase):
|
|
142
|
-
"""A Task of Tasks.
|
|
143
|
-
|
|
144
|
-
Contains shared logic that drives all of its sub-classes.
|
|
145
|
-
Should never be instantiated on its own.
|
|
146
|
-
"""
|
|
147
|
-
|
|
148
|
-
def __init__(self, tasks: List[TaskBase], compound_action_constructor=None):
|
|
149
|
-
"""Instantiate CompoundTask attributes.
|
|
150
|
-
|
|
151
|
-
Parameters
|
|
152
|
-
----------
|
|
153
|
-
tasks : List[Task]
|
|
154
|
-
The children/sub-tasks of this Task
|
|
155
|
-
compound_action_constructor : Union[WhenAllAction, WhenAnyAction, None]
|
|
156
|
-
Either None or, a WhenAllAction or WhenAnyAction constructor.
|
|
157
|
-
It is None when using the V1 replay protocol, where no Compound Action
|
|
158
|
-
objects size and compound actions are represented as arrays of actions.
|
|
159
|
-
It is not None when using the V2 replay protocol.
|
|
160
|
-
"""
|
|
161
|
-
super().__init__(-1, [])
|
|
162
|
-
child_actions = []
|
|
163
|
-
for task in tasks:
|
|
164
|
-
task.parent = self
|
|
165
|
-
action_repr = task.action_repr
|
|
166
|
-
if isinstance(action_repr, list):
|
|
167
|
-
child_actions.extend(action_repr)
|
|
168
|
-
else:
|
|
169
|
-
if not task._is_scheduled:
|
|
170
|
-
child_actions.append(action_repr)
|
|
171
|
-
if compound_action_constructor is None:
|
|
172
|
-
self.action_repr = child_actions
|
|
173
|
-
else: # replay_schema is ReplaySchema.V2
|
|
174
|
-
self.action_repr = compound_action_constructor(child_actions)
|
|
175
|
-
self._first_error: Optional[Exception] = None
|
|
176
|
-
self.pending_tasks: Set[TaskBase] = set(tasks)
|
|
177
|
-
self.completed_tasks: List[TaskBase] = []
|
|
178
|
-
self.children = tasks
|
|
179
|
-
|
|
180
|
-
if len(self.children) == 0:
|
|
181
|
-
self.state = TaskState.SUCCEEDED
|
|
182
|
-
|
|
183
|
-
# Sub-tasks may have already completed, so we process them
|
|
184
|
-
for child in self.children:
|
|
185
|
-
if not (child.state is TaskState.RUNNING):
|
|
186
|
-
self.handle_completion(child)
|
|
187
|
-
|
|
188
|
-
@property
|
|
189
|
-
def _is_scheduled(self) -> bool:
|
|
190
|
-
return all([child._is_scheduled for child in self.children])
|
|
191
|
-
|
|
192
|
-
def handle_completion(self, child: TaskBase):
|
|
193
|
-
"""Manage sub-task completion events.
|
|
194
|
-
|
|
195
|
-
Parameters
|
|
196
|
-
----------
|
|
197
|
-
child : TaskBase
|
|
198
|
-
The sub-task that completed
|
|
199
|
-
|
|
200
|
-
Raises
|
|
201
|
-
------
|
|
202
|
-
Exception
|
|
203
|
-
When the calling sub-task was not registered
|
|
204
|
-
with this Task's pending sub-tasks.
|
|
205
|
-
"""
|
|
206
|
-
try:
|
|
207
|
-
self.pending_tasks.remove(child)
|
|
208
|
-
except KeyError:
|
|
209
|
-
raise Exception(
|
|
210
|
-
f"Parent Task {self.id} does not have pending sub-task with ID {child.id}."
|
|
211
|
-
f"This most likely means that Task {child.id} completed twice.")
|
|
212
|
-
|
|
213
|
-
self.completed_tasks.append(child)
|
|
214
|
-
self.set_is_played(child.is_played)
|
|
215
|
-
self.try_set_value(child)
|
|
216
|
-
|
|
217
|
-
def try_set_value(self, child: TaskBase):
|
|
218
|
-
"""Transition a CompoundTask to a terminal state and set its value.
|
|
219
|
-
|
|
220
|
-
Should be implemented by sub-classes.
|
|
221
|
-
|
|
222
|
-
Parameters
|
|
223
|
-
----------
|
|
224
|
-
child : TaskBase
|
|
225
|
-
A sub-task that just completed
|
|
226
|
-
|
|
227
|
-
Raises
|
|
228
|
-
------
|
|
229
|
-
NotImplementedError
|
|
230
|
-
This method needs to be implemented by each subclass.
|
|
231
|
-
"""
|
|
232
|
-
raise NotImplementedError
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
class AtomicTask(TaskBase):
|
|
236
|
-
"""A Task with no subtasks."""
|
|
237
|
-
|
|
238
|
-
def _get_action(self) -> Action:
|
|
239
|
-
action: Action
|
|
240
|
-
if isinstance(self.action_repr, list):
|
|
241
|
-
action = self.action_repr[0]
|
|
242
|
-
else:
|
|
243
|
-
action = self.action_repr
|
|
244
|
-
return action
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class TimerTask(AtomicTask):
|
|
248
|
-
"""A Timer Task."""
|
|
249
|
-
|
|
250
|
-
def __init__(self, id_: Union[int, str], action: CreateTimerAction):
|
|
251
|
-
super().__init__(id_, action)
|
|
252
|
-
self.action_repr: Union[List[CreateTimerAction], CreateTimerAction]
|
|
253
|
-
|
|
254
|
-
@property
|
|
255
|
-
def is_cancelled(self) -> bool:
|
|
256
|
-
"""Check if the Timer is cancelled.
|
|
257
|
-
|
|
258
|
-
Returns
|
|
259
|
-
-------
|
|
260
|
-
bool
|
|
261
|
-
Returns whether a timer has been cancelled or not
|
|
262
|
-
"""
|
|
263
|
-
action: CreateTimerAction = self._get_action()
|
|
264
|
-
return action.is_cancelled
|
|
265
|
-
|
|
266
|
-
def cancel(self):
|
|
267
|
-
"""Cancel a timer.
|
|
268
|
-
|
|
269
|
-
Raises
|
|
270
|
-
------
|
|
271
|
-
ValueError
|
|
272
|
-
Raises an error if the task is already completed and an attempt is made to cancel it
|
|
273
|
-
"""
|
|
274
|
-
if not self.is_completed:
|
|
275
|
-
action: CreateTimerAction = self._get_action()
|
|
276
|
-
action.is_cancelled = True
|
|
277
|
-
else:
|
|
278
|
-
raise ValueError("Cannot cancel a completed task.")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
class WhenAllTask(CompoundTask):
|
|
282
|
-
"""A Task representing `when_all` scenarios."""
|
|
283
|
-
|
|
284
|
-
def __init__(self, task: List[TaskBase], replay_schema: ReplaySchema):
|
|
285
|
-
"""Initialize a WhenAllTask.
|
|
286
|
-
|
|
287
|
-
Parameters
|
|
288
|
-
----------
|
|
289
|
-
task : List[Task]
|
|
290
|
-
The list of child tasks
|
|
291
|
-
replay_schema : ReplaySchema
|
|
292
|
-
The ReplaySchema, which determines the inner action payload representation
|
|
293
|
-
"""
|
|
294
|
-
compound_action_constructor = None
|
|
295
|
-
if replay_schema is ReplaySchema.V2:
|
|
296
|
-
compound_action_constructor = WhenAllAction
|
|
297
|
-
super().__init__(task, compound_action_constructor)
|
|
298
|
-
|
|
299
|
-
def try_set_value(self, child: TaskBase):
|
|
300
|
-
"""Transition a WhenAll Task to a terminal state and set its value.
|
|
301
|
-
|
|
302
|
-
Parameters
|
|
303
|
-
----------
|
|
304
|
-
child : TaskBase
|
|
305
|
-
A sub-task that just completed
|
|
306
|
-
"""
|
|
307
|
-
if child.state is TaskState.SUCCEEDED:
|
|
308
|
-
# A WhenAll Task only completes when it has no pending tasks
|
|
309
|
-
# i.e _when all_ of its children have completed
|
|
310
|
-
if len(self.pending_tasks) == 0:
|
|
311
|
-
results = list(map(lambda x: x.result, self.children))
|
|
312
|
-
self.set_value(is_error=False, value=results)
|
|
313
|
-
else: # child.state is TaskState.FAILED:
|
|
314
|
-
# a single error is sufficient to fail this task
|
|
315
|
-
if self._first_error is None:
|
|
316
|
-
self._first_error = child.result
|
|
317
|
-
self.set_value(is_error=True, value=self._first_error)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
class WhenAnyTask(CompoundTask):
|
|
321
|
-
"""A Task representing `when_any` scenarios."""
|
|
322
|
-
|
|
323
|
-
def __init__(self, task: List[TaskBase], replay_schema: ReplaySchema):
|
|
324
|
-
"""Initialize a WhenAnyTask.
|
|
325
|
-
|
|
326
|
-
Parameters
|
|
327
|
-
----------
|
|
328
|
-
task : List[Task]
|
|
329
|
-
The list of child tasks
|
|
330
|
-
replay_schema : ReplaySchema
|
|
331
|
-
The ReplaySchema, which determines the inner action payload representation
|
|
332
|
-
"""
|
|
333
|
-
compound_action_constructor = None
|
|
334
|
-
if replay_schema is ReplaySchema.V2:
|
|
335
|
-
compound_action_constructor = WhenAnyAction
|
|
336
|
-
super().__init__(task, compound_action_constructor)
|
|
337
|
-
|
|
338
|
-
def try_set_value(self, child: TaskBase):
|
|
339
|
-
"""Transition a WhenAny Task to a terminal state and set its value.
|
|
340
|
-
|
|
341
|
-
Parameters
|
|
342
|
-
----------
|
|
343
|
-
child : TaskBase
|
|
344
|
-
A sub-task that just completed
|
|
345
|
-
"""
|
|
346
|
-
if self.state is TaskState.RUNNING:
|
|
347
|
-
self.set_value(is_error=False, value=child)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
class RetryAbleTask(WhenAllTask):
|
|
351
|
-
"""A Task representing `with_retry` scenarios.
|
|
352
|
-
|
|
353
|
-
It inherits from WhenAllTask because retryable scenarios are Tasks
|
|
354
|
-
with equivalent to WhenAll Tasks with dynamically increasing lists
|
|
355
|
-
of children. At every failure, we add a Timer child and a Task child
|
|
356
|
-
to the list of pending tasks.
|
|
357
|
-
"""
|
|
358
|
-
|
|
359
|
-
def __init__(self, child: TaskBase, retry_options: RetryOptions, context):
|
|
360
|
-
tasks = [child]
|
|
361
|
-
super().__init__(tasks, context._replay_schema)
|
|
362
|
-
|
|
363
|
-
self.retry_options = retry_options
|
|
364
|
-
self.num_attempts = 1
|
|
365
|
-
self.context = context
|
|
366
|
-
self.actions = child.action_repr
|
|
367
|
-
self.is_waiting_on_timer = False
|
|
368
|
-
self.error = None
|
|
369
|
-
|
|
370
|
-
@property
|
|
371
|
-
def id_(self):
|
|
372
|
-
"""Obtain the task's ID.
|
|
373
|
-
|
|
374
|
-
Since this is an internal-only abstraction, the task ID is represented
|
|
375
|
-
by the ID of its inner/wrapped task _plus_ a suffix: "_retryable_proxy"
|
|
376
|
-
|
|
377
|
-
Returns
|
|
378
|
-
-------
|
|
379
|
-
[type]
|
|
380
|
-
[description]
|
|
381
|
-
"""
|
|
382
|
-
return str(list(map(lambda x: x.id, self.children))) + "_retryable_proxy"
|
|
383
|
-
|
|
384
|
-
def try_set_value(self, child: TaskBase):
|
|
385
|
-
"""Transition a Retryable Task to a terminal state and set its value.
|
|
386
|
-
|
|
387
|
-
Parameters
|
|
388
|
-
----------
|
|
389
|
-
child : TaskBase
|
|
390
|
-
A sub-task that just completed
|
|
391
|
-
"""
|
|
392
|
-
if self.is_waiting_on_timer:
|
|
393
|
-
# timer fired, re-scheduling original task
|
|
394
|
-
self.is_waiting_on_timer = False
|
|
395
|
-
# As per DTFx semantics: we need to check the number of retires only after the final
|
|
396
|
-
# timer has fired. This means we essentially have to wait for one "extra" timer after
|
|
397
|
-
# the maximum number of attempts has been reached. Removing this extra timer will cause
|
|
398
|
-
# stuck orchestrators as we need to be "in sync" with the replay logic of DTFx.
|
|
399
|
-
if self.num_attempts >= self.retry_options.max_number_of_attempts:
|
|
400
|
-
self.is_waiting_on_timer = True
|
|
401
|
-
# we have reached the maximum number of attempts, set error
|
|
402
|
-
self.set_value(is_error=True, value=self.error)
|
|
403
|
-
else:
|
|
404
|
-
rescheduled_task = self.context._generate_task(
|
|
405
|
-
action=NoOpAction("rescheduled task"), parent=self)
|
|
406
|
-
self.pending_tasks.add(rescheduled_task)
|
|
407
|
-
self.context._add_to_open_tasks(rescheduled_task)
|
|
408
|
-
self.num_attempts += 1
|
|
409
|
-
|
|
410
|
-
return
|
|
411
|
-
if child.state is TaskState.SUCCEEDED:
|
|
412
|
-
if len(self.pending_tasks) == 0:
|
|
413
|
-
# if all pending tasks have completed,
|
|
414
|
-
# and we have a successful child, then
|
|
415
|
-
# we can set the Task's event
|
|
416
|
-
self.set_value(is_error=False, value=child.result)
|
|
417
|
-
|
|
418
|
-
else: # child.state is TaskState.FAILED:
|
|
419
|
-
# increase size of pending tasks by adding a timer task
|
|
420
|
-
# when it completes, we'll retry the original task
|
|
421
|
-
timer_task = self.context._generate_task(
|
|
422
|
-
action=NoOpAction("-WithRetry timer"), parent=self)
|
|
423
|
-
self.pending_tasks.add(timer_task)
|
|
424
|
-
self.context._add_to_open_tasks(timer_task)
|
|
425
|
-
self.is_waiting_on_timer = True
|
|
426
|
-
self.error = child.result
|
|
1
|
+
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
|
|
2
|
+
from azure.durable_functions.models.actions.CompoundAction import CompoundAction
|
|
3
|
+
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
4
|
+
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
5
|
+
from azure.durable_functions.models.actions.Action import Action
|
|
6
|
+
from azure.durable_functions.models.actions.WhenAnyAction import WhenAnyAction
|
|
7
|
+
from azure.durable_functions.models.actions.WhenAllAction import WhenAllAction
|
|
8
|
+
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
9
|
+
|
|
10
|
+
import enum
|
|
11
|
+
from typing import Any, List, Optional, Set, Type, Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskState(enum.Enum):
|
|
15
|
+
"""The possible states that a Task can be in."""
|
|
16
|
+
|
|
17
|
+
RUNNING = 0
|
|
18
|
+
SUCCEEDED = 1
|
|
19
|
+
FAILED = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskBase:
|
|
23
|
+
"""The base class of all Tasks.
|
|
24
|
+
|
|
25
|
+
Contains shared logic that drives all of its sub-classes. Should never be
|
|
26
|
+
instantiated on its own.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, id_: Union[int, str], actions: Union[List[Action], Action]):
|
|
30
|
+
"""Initialize the TaskBase.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
id_ : int
|
|
35
|
+
An ID for the task
|
|
36
|
+
actions : List[Any]
|
|
37
|
+
The list of DF actions representing this Task.
|
|
38
|
+
Needed for reconstruction in the extension.
|
|
39
|
+
"""
|
|
40
|
+
self.id: Union[int, str] = id_
|
|
41
|
+
self.state = TaskState.RUNNING
|
|
42
|
+
self.parent: Optional[CompoundTask] = None
|
|
43
|
+
self._api_name: str
|
|
44
|
+
|
|
45
|
+
api_action: Union[Action, Type[CompoundAction]]
|
|
46
|
+
if isinstance(actions, list):
|
|
47
|
+
if len(actions) == 1:
|
|
48
|
+
api_action = actions[0]
|
|
49
|
+
else:
|
|
50
|
+
api_action = CompoundAction
|
|
51
|
+
else:
|
|
52
|
+
api_action = actions
|
|
53
|
+
|
|
54
|
+
self._api_name = api_action.__class__.__name__
|
|
55
|
+
|
|
56
|
+
self.result: Any = None
|
|
57
|
+
self.action_repr: Union[List[Action], Action] = actions
|
|
58
|
+
self.is_played = False
|
|
59
|
+
self._is_scheduled_flag = False
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def _is_scheduled(self) -> bool:
|
|
63
|
+
return self._is_scheduled_flag
|
|
64
|
+
|
|
65
|
+
def _set_is_scheduled(self, is_scheduled: bool):
|
|
66
|
+
self._is_scheduled_flag = is_scheduled
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_completed(self) -> bool:
|
|
70
|
+
"""Get indicator of whether the task completed.
|
|
71
|
+
|
|
72
|
+
Note that completion is not equivalent to success.
|
|
73
|
+
"""
|
|
74
|
+
return not (self.state is TaskState.RUNNING)
|
|
75
|
+
|
|
76
|
+
def set_is_played(self, is_played: bool):
|
|
77
|
+
"""Set the is_played flag for the Task.
|
|
78
|
+
|
|
79
|
+
Needed for updating the orchestrator's is_replaying flag.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
is_played : bool
|
|
84
|
+
Whether the latest event for this Task has been played before.
|
|
85
|
+
"""
|
|
86
|
+
self.is_played = is_played
|
|
87
|
+
|
|
88
|
+
def change_state(self, state: TaskState):
|
|
89
|
+
"""Transition a running Task to a terminal state: success or failure.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
state : TaskState
|
|
94
|
+
The terminal state to assign to this Task
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
Exception
|
|
99
|
+
When the input state is RUNNING
|
|
100
|
+
"""
|
|
101
|
+
if state is TaskState.RUNNING:
|
|
102
|
+
raise Exception("Cannot change Task to the RUNNING state.")
|
|
103
|
+
self.state = state
|
|
104
|
+
|
|
105
|
+
def set_value(self, is_error: bool, value: Any):
|
|
106
|
+
"""Set the value of this Task: either an exception of a result.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
is_error : bool
|
|
111
|
+
Whether the value represents an exception of a result.
|
|
112
|
+
value : Any
|
|
113
|
+
The value of this Task
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
Exception
|
|
118
|
+
When the Task failed but its value was not an Exception
|
|
119
|
+
"""
|
|
120
|
+
new_state = self.state
|
|
121
|
+
if is_error:
|
|
122
|
+
if not isinstance(value, Exception):
|
|
123
|
+
if not (isinstance(value, TaskBase) and isinstance(value.result, Exception)):
|
|
124
|
+
err_message = f"Task ID {self.id} failed but it's value was not an Exception"
|
|
125
|
+
raise Exception(err_message)
|
|
126
|
+
new_state = TaskState.FAILED
|
|
127
|
+
else:
|
|
128
|
+
new_state = TaskState.SUCCEEDED
|
|
129
|
+
self.change_state(new_state)
|
|
130
|
+
self.result = value
|
|
131
|
+
self.propagate()
|
|
132
|
+
|
|
133
|
+
def propagate(self):
|
|
134
|
+
"""Notify parent Task of this Task's state change."""
|
|
135
|
+
has_completed = not (self.state is TaskState.RUNNING)
|
|
136
|
+
has_parent = not (self.parent is None)
|
|
137
|
+
if has_completed and has_parent:
|
|
138
|
+
self.parent.handle_completion(self)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CompoundTask(TaskBase):
|
|
142
|
+
"""A Task of Tasks.
|
|
143
|
+
|
|
144
|
+
Contains shared logic that drives all of its sub-classes.
|
|
145
|
+
Should never be instantiated on its own.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, tasks: List[TaskBase], compound_action_constructor=None):
|
|
149
|
+
"""Instantiate CompoundTask attributes.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
tasks : List[Task]
|
|
154
|
+
The children/sub-tasks of this Task
|
|
155
|
+
compound_action_constructor : Union[WhenAllAction, WhenAnyAction, None]
|
|
156
|
+
Either None or, a WhenAllAction or WhenAnyAction constructor.
|
|
157
|
+
It is None when using the V1 replay protocol, where no Compound Action
|
|
158
|
+
objects size and compound actions are represented as arrays of actions.
|
|
159
|
+
It is not None when using the V2 replay protocol.
|
|
160
|
+
"""
|
|
161
|
+
super().__init__(-1, [])
|
|
162
|
+
child_actions = []
|
|
163
|
+
for task in tasks:
|
|
164
|
+
task.parent = self
|
|
165
|
+
action_repr = task.action_repr
|
|
166
|
+
if isinstance(action_repr, list):
|
|
167
|
+
child_actions.extend(action_repr)
|
|
168
|
+
else:
|
|
169
|
+
if not task._is_scheduled:
|
|
170
|
+
child_actions.append(action_repr)
|
|
171
|
+
if compound_action_constructor is None:
|
|
172
|
+
self.action_repr = child_actions
|
|
173
|
+
else: # replay_schema is ReplaySchema.V2
|
|
174
|
+
self.action_repr = compound_action_constructor(child_actions)
|
|
175
|
+
self._first_error: Optional[Exception] = None
|
|
176
|
+
self.pending_tasks: Set[TaskBase] = set(tasks)
|
|
177
|
+
self.completed_tasks: List[TaskBase] = []
|
|
178
|
+
self.children = tasks
|
|
179
|
+
|
|
180
|
+
if len(self.children) == 0:
|
|
181
|
+
self.state = TaskState.SUCCEEDED
|
|
182
|
+
|
|
183
|
+
# Sub-tasks may have already completed, so we process them
|
|
184
|
+
for child in self.children:
|
|
185
|
+
if not (child.state is TaskState.RUNNING):
|
|
186
|
+
self.handle_completion(child)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def _is_scheduled(self) -> bool:
|
|
190
|
+
return all([child._is_scheduled for child in self.children])
|
|
191
|
+
|
|
192
|
+
def handle_completion(self, child: TaskBase):
|
|
193
|
+
"""Manage sub-task completion events.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
child : TaskBase
|
|
198
|
+
The sub-task that completed
|
|
199
|
+
|
|
200
|
+
Raises
|
|
201
|
+
------
|
|
202
|
+
Exception
|
|
203
|
+
When the calling sub-task was not registered
|
|
204
|
+
with this Task's pending sub-tasks.
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
self.pending_tasks.remove(child)
|
|
208
|
+
except KeyError:
|
|
209
|
+
raise Exception(
|
|
210
|
+
f"Parent Task {self.id} does not have pending sub-task with ID {child.id}."
|
|
211
|
+
f"This most likely means that Task {child.id} completed twice.")
|
|
212
|
+
|
|
213
|
+
self.completed_tasks.append(child)
|
|
214
|
+
self.set_is_played(child.is_played)
|
|
215
|
+
self.try_set_value(child)
|
|
216
|
+
|
|
217
|
+
def try_set_value(self, child: TaskBase):
|
|
218
|
+
"""Transition a CompoundTask to a terminal state and set its value.
|
|
219
|
+
|
|
220
|
+
Should be implemented by sub-classes.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
child : TaskBase
|
|
225
|
+
A sub-task that just completed
|
|
226
|
+
|
|
227
|
+
Raises
|
|
228
|
+
------
|
|
229
|
+
NotImplementedError
|
|
230
|
+
This method needs to be implemented by each subclass.
|
|
231
|
+
"""
|
|
232
|
+
raise NotImplementedError
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class AtomicTask(TaskBase):
|
|
236
|
+
"""A Task with no subtasks."""
|
|
237
|
+
|
|
238
|
+
def _get_action(self) -> Action:
|
|
239
|
+
action: Action
|
|
240
|
+
if isinstance(self.action_repr, list):
|
|
241
|
+
action = self.action_repr[0]
|
|
242
|
+
else:
|
|
243
|
+
action = self.action_repr
|
|
244
|
+
return action
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TimerTask(AtomicTask):
|
|
248
|
+
"""A Timer Task."""
|
|
249
|
+
|
|
250
|
+
def __init__(self, id_: Union[int, str], action: CreateTimerAction):
|
|
251
|
+
super().__init__(id_, action)
|
|
252
|
+
self.action_repr: Union[List[CreateTimerAction], CreateTimerAction]
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def is_cancelled(self) -> bool:
|
|
256
|
+
"""Check if the Timer is cancelled.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
bool
|
|
261
|
+
Returns whether a timer has been cancelled or not
|
|
262
|
+
"""
|
|
263
|
+
action: CreateTimerAction = self._get_action()
|
|
264
|
+
return action.is_cancelled
|
|
265
|
+
|
|
266
|
+
def cancel(self):
|
|
267
|
+
"""Cancel a timer.
|
|
268
|
+
|
|
269
|
+
Raises
|
|
270
|
+
------
|
|
271
|
+
ValueError
|
|
272
|
+
Raises an error if the task is already completed and an attempt is made to cancel it
|
|
273
|
+
"""
|
|
274
|
+
if not self.is_completed:
|
|
275
|
+
action: CreateTimerAction = self._get_action()
|
|
276
|
+
action.is_cancelled = True
|
|
277
|
+
else:
|
|
278
|
+
raise ValueError("Cannot cancel a completed task.")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class WhenAllTask(CompoundTask):
|
|
282
|
+
"""A Task representing `when_all` scenarios."""
|
|
283
|
+
|
|
284
|
+
def __init__(self, task: List[TaskBase], replay_schema: ReplaySchema):
|
|
285
|
+
"""Initialize a WhenAllTask.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
task : List[Task]
|
|
290
|
+
The list of child tasks
|
|
291
|
+
replay_schema : ReplaySchema
|
|
292
|
+
The ReplaySchema, which determines the inner action payload representation
|
|
293
|
+
"""
|
|
294
|
+
compound_action_constructor = None
|
|
295
|
+
if replay_schema is ReplaySchema.V2:
|
|
296
|
+
compound_action_constructor = WhenAllAction
|
|
297
|
+
super().__init__(task, compound_action_constructor)
|
|
298
|
+
|
|
299
|
+
def try_set_value(self, child: TaskBase):
|
|
300
|
+
"""Transition a WhenAll Task to a terminal state and set its value.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
child : TaskBase
|
|
305
|
+
A sub-task that just completed
|
|
306
|
+
"""
|
|
307
|
+
if child.state is TaskState.SUCCEEDED:
|
|
308
|
+
# A WhenAll Task only completes when it has no pending tasks
|
|
309
|
+
# i.e _when all_ of its children have completed
|
|
310
|
+
if len(self.pending_tasks) == 0:
|
|
311
|
+
results = list(map(lambda x: x.result, self.children))
|
|
312
|
+
self.set_value(is_error=False, value=results)
|
|
313
|
+
else: # child.state is TaskState.FAILED:
|
|
314
|
+
# a single error is sufficient to fail this task
|
|
315
|
+
if self._first_error is None:
|
|
316
|
+
self._first_error = child.result
|
|
317
|
+
self.set_value(is_error=True, value=self._first_error)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class WhenAnyTask(CompoundTask):
|
|
321
|
+
"""A Task representing `when_any` scenarios."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, task: List[TaskBase], replay_schema: ReplaySchema):
|
|
324
|
+
"""Initialize a WhenAnyTask.
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
task : List[Task]
|
|
329
|
+
The list of child tasks
|
|
330
|
+
replay_schema : ReplaySchema
|
|
331
|
+
The ReplaySchema, which determines the inner action payload representation
|
|
332
|
+
"""
|
|
333
|
+
compound_action_constructor = None
|
|
334
|
+
if replay_schema is ReplaySchema.V2:
|
|
335
|
+
compound_action_constructor = WhenAnyAction
|
|
336
|
+
super().__init__(task, compound_action_constructor)
|
|
337
|
+
|
|
338
|
+
def try_set_value(self, child: TaskBase):
|
|
339
|
+
"""Transition a WhenAny Task to a terminal state and set its value.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
child : TaskBase
|
|
344
|
+
A sub-task that just completed
|
|
345
|
+
"""
|
|
346
|
+
if self.state is TaskState.RUNNING:
|
|
347
|
+
self.set_value(is_error=False, value=child)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class RetryAbleTask(WhenAllTask):
|
|
351
|
+
"""A Task representing `with_retry` scenarios.
|
|
352
|
+
|
|
353
|
+
It inherits from WhenAllTask because retryable scenarios are Tasks
|
|
354
|
+
with equivalent to WhenAll Tasks with dynamically increasing lists
|
|
355
|
+
of children. At every failure, we add a Timer child and a Task child
|
|
356
|
+
to the list of pending tasks.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
def __init__(self, child: TaskBase, retry_options: RetryOptions, context):
|
|
360
|
+
tasks = [child]
|
|
361
|
+
super().__init__(tasks, context._replay_schema)
|
|
362
|
+
|
|
363
|
+
self.retry_options = retry_options
|
|
364
|
+
self.num_attempts = 1
|
|
365
|
+
self.context = context
|
|
366
|
+
self.actions = child.action_repr
|
|
367
|
+
self.is_waiting_on_timer = False
|
|
368
|
+
self.error = None
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def id_(self):
|
|
372
|
+
"""Obtain the task's ID.
|
|
373
|
+
|
|
374
|
+
Since this is an internal-only abstraction, the task ID is represented
|
|
375
|
+
by the ID of its inner/wrapped task _plus_ a suffix: "_retryable_proxy"
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
[type]
|
|
380
|
+
[description]
|
|
381
|
+
"""
|
|
382
|
+
return str(list(map(lambda x: x.id, self.children))) + "_retryable_proxy"
|
|
383
|
+
|
|
384
|
+
def try_set_value(self, child: TaskBase):
|
|
385
|
+
"""Transition a Retryable Task to a terminal state and set its value.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
child : TaskBase
|
|
390
|
+
A sub-task that just completed
|
|
391
|
+
"""
|
|
392
|
+
if self.is_waiting_on_timer:
|
|
393
|
+
# timer fired, re-scheduling original task
|
|
394
|
+
self.is_waiting_on_timer = False
|
|
395
|
+
# As per DTFx semantics: we need to check the number of retires only after the final
|
|
396
|
+
# timer has fired. This means we essentially have to wait for one "extra" timer after
|
|
397
|
+
# the maximum number of attempts has been reached. Removing this extra timer will cause
|
|
398
|
+
# stuck orchestrators as we need to be "in sync" with the replay logic of DTFx.
|
|
399
|
+
if self.num_attempts >= self.retry_options.max_number_of_attempts:
|
|
400
|
+
self.is_waiting_on_timer = True
|
|
401
|
+
# we have reached the maximum number of attempts, set error
|
|
402
|
+
self.set_value(is_error=True, value=self.error)
|
|
403
|
+
else:
|
|
404
|
+
rescheduled_task = self.context._generate_task(
|
|
405
|
+
action=NoOpAction("rescheduled task"), parent=self)
|
|
406
|
+
self.pending_tasks.add(rescheduled_task)
|
|
407
|
+
self.context._add_to_open_tasks(rescheduled_task)
|
|
408
|
+
self.num_attempts += 1
|
|
409
|
+
|
|
410
|
+
return
|
|
411
|
+
if child.state is TaskState.SUCCEEDED:
|
|
412
|
+
if len(self.pending_tasks) == 0:
|
|
413
|
+
# if all pending tasks have completed,
|
|
414
|
+
# and we have a successful child, then
|
|
415
|
+
# we can set the Task's event
|
|
416
|
+
self.set_value(is_error=False, value=child.result)
|
|
417
|
+
|
|
418
|
+
else: # child.state is TaskState.FAILED:
|
|
419
|
+
# increase size of pending tasks by adding a timer task
|
|
420
|
+
# when it completes, we'll retry the original task
|
|
421
|
+
timer_task = self.context._generate_task(
|
|
422
|
+
action=NoOpAction("-WithRetry timer"), parent=self)
|
|
423
|
+
self.pending_tasks.add(timer_task)
|
|
424
|
+
self.context._add_to_open_tasks(timer_task)
|
|
425
|
+
self.is_waiting_on_timer = True
|
|
426
|
+
self.error = child.result
|