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.
Files changed (101) hide show
  1. azure/durable_functions/__init__.py +81 -81
  2. azure/durable_functions/constants.py +9 -9
  3. azure/durable_functions/decorators/__init__.py +3 -3
  4. azure/durable_functions/decorators/durable_app.py +249 -249
  5. azure/durable_functions/decorators/metadata.py +109 -109
  6. azure/durable_functions/entity.py +125 -125
  7. azure/durable_functions/models/DurableEntityContext.py +201 -201
  8. azure/durable_functions/models/DurableHttpRequest.py +58 -58
  9. azure/durable_functions/models/DurableOrchestrationBindings.py +66 -66
  10. azure/durable_functions/models/DurableOrchestrationClient.py +781 -781
  11. azure/durable_functions/models/DurableOrchestrationContext.py +722 -707
  12. azure/durable_functions/models/DurableOrchestrationStatus.py +156 -156
  13. azure/durable_functions/models/EntityStateResponse.py +23 -23
  14. azure/durable_functions/models/FunctionContext.py +7 -7
  15. azure/durable_functions/models/OrchestrationRuntimeStatus.py +32 -32
  16. azure/durable_functions/models/OrchestratorState.py +117 -116
  17. azure/durable_functions/models/PurgeHistoryResult.py +33 -33
  18. azure/durable_functions/models/ReplaySchema.py +8 -8
  19. azure/durable_functions/models/RetryOptions.py +69 -69
  20. azure/durable_functions/models/RpcManagementOptions.py +86 -86
  21. azure/durable_functions/models/Task.py +426 -426
  22. azure/durable_functions/models/TaskOrchestrationExecutor.py +346 -336
  23. azure/durable_functions/models/TokenSource.py +56 -56
  24. azure/durable_functions/models/__init__.py +24 -24
  25. azure/durable_functions/models/actions/Action.py +23 -23
  26. azure/durable_functions/models/actions/ActionType.py +18 -18
  27. azure/durable_functions/models/actions/CallActivityAction.py +41 -41
  28. azure/durable_functions/models/actions/CallActivityWithRetryAction.py +45 -45
  29. azure/durable_functions/models/actions/CallEntityAction.py +46 -46
  30. azure/durable_functions/models/actions/CallHttpAction.py +35 -35
  31. azure/durable_functions/models/actions/CallSubOrchestratorAction.py +40 -40
  32. azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py +44 -44
  33. azure/durable_functions/models/actions/CompoundAction.py +35 -35
  34. azure/durable_functions/models/actions/ContinueAsNewAction.py +36 -36
  35. azure/durable_functions/models/actions/CreateTimerAction.py +48 -48
  36. azure/durable_functions/models/actions/NoOpAction.py +35 -35
  37. azure/durable_functions/models/actions/SignalEntityAction.py +47 -47
  38. azure/durable_functions/models/actions/WaitForExternalEventAction.py +63 -63
  39. azure/durable_functions/models/actions/WhenAllAction.py +14 -14
  40. azure/durable_functions/models/actions/WhenAnyAction.py +14 -14
  41. azure/durable_functions/models/actions/__init__.py +24 -24
  42. azure/durable_functions/models/entities/EntityState.py +74 -74
  43. azure/durable_functions/models/entities/OperationResult.py +76 -76
  44. azure/durable_functions/models/entities/RequestMessage.py +53 -53
  45. azure/durable_functions/models/entities/ResponseMessage.py +48 -48
  46. azure/durable_functions/models/entities/Signal.py +62 -62
  47. azure/durable_functions/models/entities/__init__.py +17 -17
  48. azure/durable_functions/models/history/HistoryEvent.py +92 -92
  49. azure/durable_functions/models/history/HistoryEventType.py +27 -27
  50. azure/durable_functions/models/history/__init__.py +8 -8
  51. azure/durable_functions/models/utils/__init__.py +7 -7
  52. azure/durable_functions/models/utils/entity_utils.py +103 -91
  53. azure/durable_functions/models/utils/http_utils.py +69 -69
  54. azure/durable_functions/models/utils/json_utils.py +56 -56
  55. azure/durable_functions/orchestrator.py +71 -71
  56. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/LICENSE +21 -21
  57. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/METADATA +58 -58
  58. azure_functions_durable-1.2.10.dist-info/RECORD +100 -0
  59. tests/models/test_DecoratorMetadata.py +135 -135
  60. tests/models/test_Decorators.py +107 -107
  61. tests/models/test_DurableOrchestrationBindings.py +68 -68
  62. tests/models/test_DurableOrchestrationClient.py +730 -730
  63. tests/models/test_DurableOrchestrationContext.py +102 -102
  64. tests/models/test_DurableOrchestrationStatus.py +59 -59
  65. tests/models/test_OrchestrationState.py +28 -28
  66. tests/models/test_RpcManagementOptions.py +79 -79
  67. tests/models/test_TokenSource.py +10 -10
  68. tests/orchestrator/models/OrchestrationInstance.py +18 -18
  69. tests/orchestrator/orchestrator_test_utils.py +130 -130
  70. tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
  71. tests/orchestrator/test_call_http.py +235 -176
  72. tests/orchestrator/test_continue_as_new.py +67 -67
  73. tests/orchestrator/test_create_timer.py +126 -126
  74. tests/orchestrator/test_entity.py +395 -395
  75. tests/orchestrator/test_external_event.py +53 -53
  76. tests/orchestrator/test_fan_out_fan_in.py +175 -175
  77. tests/orchestrator/test_is_replaying_flag.py +101 -101
  78. tests/orchestrator/test_retries.py +308 -308
  79. tests/orchestrator/test_sequential_orchestrator.py +841 -841
  80. tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
  81. tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
  82. tests/orchestrator/test_serialization.py +30 -30
  83. tests/orchestrator/test_sub_orchestrator.py +95 -95
  84. tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
  85. tests/orchestrator/test_task_any.py +60 -60
  86. tests/tasks/tasks_test_utils.py +17 -17
  87. tests/tasks/test_new_uuid.py +34 -34
  88. tests/test_utils/ContextBuilder.py +174 -174
  89. tests/test_utils/EntityContextBuilder.py +56 -56
  90. tests/test_utils/constants.py +1 -1
  91. tests/test_utils/json_utils.py +30 -30
  92. tests/test_utils/testClasses.py +56 -56
  93. tests/utils/__init__.py +1 -0
  94. tests/utils/test_entity_utils.py +24 -0
  95. azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
  96. azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
  97. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
  98. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
  99. azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
  100. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.2.10.dist-info}/WHEEL +0 -0
  101. {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