azure-functions-durable 1.2.9__py3-none-any.whl → 1.3.0__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 +260 -249
- azure/durable_functions/decorators/metadata.py +109 -109
- azure/durable_functions/entity.py +129 -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 +812 -781
- azure/durable_functions/models/DurableOrchestrationContext.py +761 -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 +9 -8
- azure/durable_functions/models/RetryOptions.py +69 -69
- azure/durable_functions/models/RpcManagementOptions.py +86 -86
- azure/durable_functions/models/Task.py +540 -426
- azure/durable_functions/models/TaskOrchestrationExecutor.py +352 -336
- azure/durable_functions/models/TokenSource.py +56 -56
- azure/durable_functions/models/__init__.py +26 -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 +94 -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 +80 -69
- azure/durable_functions/models/utils/json_utils.py +96 -56
- azure/durable_functions/orchestrator.py +73 -71
- azure/durable_functions/testing/OrchestratorGeneratorWrapper.py +42 -0
- azure/durable_functions/testing/__init__.py +6 -0
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE +21 -21
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA +59 -58
- azure_functions_durable-1.3.0.dist-info/RECORD +103 -0
- {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/WHEEL +1 -1
- 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 +397 -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_long_timers.py +70 -0
- 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.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,426 +1,540 @@
|
|
|
1
|
-
from
|
|
2
|
-
from azure.durable_functions.models.actions.
|
|
3
|
-
from azure.durable_functions.models.
|
|
4
|
-
from azure.durable_functions.models.
|
|
5
|
-
from azure.durable_functions.models.
|
|
6
|
-
from azure.durable_functions.models.actions.
|
|
7
|
-
from azure.durable_functions.models.actions.
|
|
8
|
-
from azure.durable_functions.models.actions.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
self.
|
|
131
|
-
self.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
self.
|
|
177
|
-
self.
|
|
178
|
-
self.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
f"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
self.
|
|
215
|
-
self.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
action
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
self.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
self.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"""
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
|
|
3
|
+
from azure.durable_functions.models.actions.CompoundAction import CompoundAction
|
|
4
|
+
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
5
|
+
from azure.durable_functions.models.ReplaySchema import ReplaySchema
|
|
6
|
+
from azure.durable_functions.models.actions.Action import Action
|
|
7
|
+
from azure.durable_functions.models.actions.WhenAnyAction import WhenAnyAction
|
|
8
|
+
from azure.durable_functions.models.actions.WhenAllAction import WhenAllAction
|
|
9
|
+
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
from typing import Any, List, Optional, Set, Type, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskState(enum.Enum):
|
|
16
|
+
"""The possible states that a Task can be in."""
|
|
17
|
+
|
|
18
|
+
RUNNING = 0
|
|
19
|
+
SUCCEEDED = 1
|
|
20
|
+
FAILED = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TaskBase:
|
|
24
|
+
"""The base class of all Tasks.
|
|
25
|
+
|
|
26
|
+
Contains shared logic that drives all of its sub-classes. Should never be
|
|
27
|
+
instantiated on its own.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, id_: Union[int, str], actions: Union[List[Action], Action]):
|
|
31
|
+
"""Initialize the TaskBase.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
id_ : int
|
|
36
|
+
An ID for the task
|
|
37
|
+
actions : List[Any]
|
|
38
|
+
The list of DF actions representing this Task.
|
|
39
|
+
Needed for reconstruction in the extension.
|
|
40
|
+
"""
|
|
41
|
+
self.id: Union[int, str] = id_
|
|
42
|
+
self.state = TaskState.RUNNING
|
|
43
|
+
self.parent: Optional[CompoundTask] = None
|
|
44
|
+
self._api_name: str
|
|
45
|
+
|
|
46
|
+
api_action: Union[Action, Type[CompoundAction]]
|
|
47
|
+
if isinstance(actions, list):
|
|
48
|
+
if len(actions) == 1:
|
|
49
|
+
api_action = actions[0]
|
|
50
|
+
else:
|
|
51
|
+
api_action = CompoundAction
|
|
52
|
+
else:
|
|
53
|
+
api_action = actions
|
|
54
|
+
|
|
55
|
+
self._api_name = api_action.__class__.__name__
|
|
56
|
+
|
|
57
|
+
self.result: Any = None
|
|
58
|
+
self.action_repr: Union[List[Action], Action] = actions
|
|
59
|
+
self.is_played = False
|
|
60
|
+
self._is_scheduled_flag = False
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def _is_scheduled(self) -> bool:
|
|
64
|
+
return self._is_scheduled_flag
|
|
65
|
+
|
|
66
|
+
def _set_is_scheduled(self, is_scheduled: bool):
|
|
67
|
+
self._is_scheduled_flag = is_scheduled
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_completed(self) -> bool:
|
|
71
|
+
"""Get indicator of whether the task completed.
|
|
72
|
+
|
|
73
|
+
Note that completion is not equivalent to success.
|
|
74
|
+
"""
|
|
75
|
+
return not (self.state is TaskState.RUNNING)
|
|
76
|
+
|
|
77
|
+
def set_is_played(self, is_played: bool):
|
|
78
|
+
"""Set the is_played flag for the Task.
|
|
79
|
+
|
|
80
|
+
Needed for updating the orchestrator's is_replaying flag.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
is_played : bool
|
|
85
|
+
Whether the latest event for this Task has been played before.
|
|
86
|
+
"""
|
|
87
|
+
self.is_played = is_played
|
|
88
|
+
|
|
89
|
+
def change_state(self, state: TaskState):
|
|
90
|
+
"""Transition a running Task to a terminal state: success or failure.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
state : TaskState
|
|
95
|
+
The terminal state to assign to this Task
|
|
96
|
+
|
|
97
|
+
Raises
|
|
98
|
+
------
|
|
99
|
+
Exception
|
|
100
|
+
When the input state is RUNNING
|
|
101
|
+
"""
|
|
102
|
+
if state is TaskState.RUNNING:
|
|
103
|
+
raise Exception("Cannot change Task to the RUNNING state.")
|
|
104
|
+
self.state = state
|
|
105
|
+
|
|
106
|
+
def set_value(self, is_error: bool, value: Any):
|
|
107
|
+
"""Set the value of this Task: either an exception of a result.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
is_error : bool
|
|
112
|
+
Whether the value represents an exception of a result.
|
|
113
|
+
value : Any
|
|
114
|
+
The value of this Task
|
|
115
|
+
|
|
116
|
+
Raises
|
|
117
|
+
------
|
|
118
|
+
Exception
|
|
119
|
+
When the Task failed but its value was not an Exception
|
|
120
|
+
"""
|
|
121
|
+
new_state = self.state
|
|
122
|
+
if is_error:
|
|
123
|
+
if not isinstance(value, Exception):
|
|
124
|
+
if not (isinstance(value, TaskBase) and isinstance(value.result, Exception)):
|
|
125
|
+
err_message = f"Task ID {self.id} failed but it's value was not an Exception"
|
|
126
|
+
raise Exception(err_message)
|
|
127
|
+
new_state = TaskState.FAILED
|
|
128
|
+
else:
|
|
129
|
+
new_state = TaskState.SUCCEEDED
|
|
130
|
+
self.change_state(new_state)
|
|
131
|
+
self.result = value
|
|
132
|
+
self.propagate()
|
|
133
|
+
|
|
134
|
+
def propagate(self):
|
|
135
|
+
"""Notify parent Task of this Task's state change."""
|
|
136
|
+
has_completed = not (self.state is TaskState.RUNNING)
|
|
137
|
+
has_parent = not (self.parent is None)
|
|
138
|
+
if has_completed and has_parent:
|
|
139
|
+
self.parent.handle_completion(self)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class CompoundTask(TaskBase):
|
|
143
|
+
"""A Task of Tasks.
|
|
144
|
+
|
|
145
|
+
Contains shared logic that drives all of its sub-classes.
|
|
146
|
+
Should never be instantiated on its own.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, tasks: List[TaskBase], compound_action_constructor=None):
|
|
150
|
+
"""Instantiate CompoundTask attributes.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
tasks : List[Task]
|
|
155
|
+
The children/sub-tasks of this Task
|
|
156
|
+
compound_action_constructor : Union[WhenAllAction, WhenAnyAction, None]
|
|
157
|
+
Either None or, a WhenAllAction or WhenAnyAction constructor.
|
|
158
|
+
It is None when using the V1 replay protocol, where no Compound Action
|
|
159
|
+
objects size and compound actions are represented as arrays of actions.
|
|
160
|
+
It is not None when using the V2 replay protocol.
|
|
161
|
+
"""
|
|
162
|
+
super().__init__(-1, [])
|
|
163
|
+
child_actions = []
|
|
164
|
+
for task in tasks:
|
|
165
|
+
task.parent = self
|
|
166
|
+
action_repr = task.action_repr
|
|
167
|
+
if isinstance(action_repr, list):
|
|
168
|
+
child_actions.extend(action_repr)
|
|
169
|
+
else:
|
|
170
|
+
if not task._is_scheduled:
|
|
171
|
+
child_actions.append(action_repr)
|
|
172
|
+
if compound_action_constructor is None:
|
|
173
|
+
self.action_repr = child_actions
|
|
174
|
+
else: # replay_schema >= ReplaySchema.V2
|
|
175
|
+
self.action_repr = compound_action_constructor(child_actions)
|
|
176
|
+
self._first_error: Optional[Exception] = None
|
|
177
|
+
self.pending_tasks: Set[TaskBase] = set(tasks)
|
|
178
|
+
self.completed_tasks: List[TaskBase] = []
|
|
179
|
+
self.children = tasks
|
|
180
|
+
|
|
181
|
+
if len(self.children) == 0:
|
|
182
|
+
self.state = TaskState.SUCCEEDED
|
|
183
|
+
|
|
184
|
+
# Sub-tasks may have already completed, so we process them
|
|
185
|
+
for child in self.children:
|
|
186
|
+
if not (child.state is TaskState.RUNNING):
|
|
187
|
+
self.handle_completion(child)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def _is_scheduled(self) -> bool:
|
|
191
|
+
return all([child._is_scheduled for child in self.children])
|
|
192
|
+
|
|
193
|
+
def handle_completion(self, child: TaskBase):
|
|
194
|
+
"""Manage sub-task completion events.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
child : TaskBase
|
|
199
|
+
The sub-task that completed
|
|
200
|
+
|
|
201
|
+
Raises
|
|
202
|
+
------
|
|
203
|
+
Exception
|
|
204
|
+
When the calling sub-task was not registered
|
|
205
|
+
with this Task's pending sub-tasks.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
self.pending_tasks.remove(child)
|
|
209
|
+
except KeyError:
|
|
210
|
+
raise Exception(
|
|
211
|
+
f"Parent Task {self.id} does not have pending sub-task with ID {child.id}."
|
|
212
|
+
f"This most likely means that Task {child.id} completed twice.")
|
|
213
|
+
|
|
214
|
+
self.completed_tasks.append(child)
|
|
215
|
+
self.set_is_played(child.is_played)
|
|
216
|
+
self.try_set_value(child)
|
|
217
|
+
|
|
218
|
+
def try_set_value(self, child: TaskBase):
|
|
219
|
+
"""Transition a CompoundTask to a terminal state and set its value.
|
|
220
|
+
|
|
221
|
+
Should be implemented by sub-classes.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
child : TaskBase
|
|
226
|
+
A sub-task that just completed
|
|
227
|
+
|
|
228
|
+
Raises
|
|
229
|
+
------
|
|
230
|
+
NotImplementedError
|
|
231
|
+
This method needs to be implemented by each subclass.
|
|
232
|
+
"""
|
|
233
|
+
raise NotImplementedError
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class AtomicTask(TaskBase):
|
|
237
|
+
"""A Task with no subtasks."""
|
|
238
|
+
|
|
239
|
+
def _get_action(self) -> Action:
|
|
240
|
+
action: Action
|
|
241
|
+
if isinstance(self.action_repr, list):
|
|
242
|
+
action = self.action_repr[0]
|
|
243
|
+
else:
|
|
244
|
+
action = self.action_repr
|
|
245
|
+
return action
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TimerTask(AtomicTask):
|
|
249
|
+
"""A Timer Task."""
|
|
250
|
+
|
|
251
|
+
def __init__(self, id_: Union[int, str], action: CreateTimerAction):
|
|
252
|
+
super().__init__(id_, action)
|
|
253
|
+
self.action_repr: Union[List[CreateTimerAction], CreateTimerAction]
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def is_cancelled(self) -> bool:
|
|
257
|
+
"""Check if the Timer is cancelled.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
bool
|
|
262
|
+
Returns whether a timer has been cancelled or not
|
|
263
|
+
"""
|
|
264
|
+
action: CreateTimerAction = self._get_action()
|
|
265
|
+
return action.is_cancelled
|
|
266
|
+
|
|
267
|
+
def cancel(self):
|
|
268
|
+
"""Cancel a timer.
|
|
269
|
+
|
|
270
|
+
Raises
|
|
271
|
+
------
|
|
272
|
+
ValueError
|
|
273
|
+
Raises an error if the task is already completed and an attempt is made to cancel it
|
|
274
|
+
"""
|
|
275
|
+
if not self.is_completed:
|
|
276
|
+
action: CreateTimerAction = self._get_action()
|
|
277
|
+
action.is_cancelled = True
|
|
278
|
+
else:
|
|
279
|
+
raise ValueError("Cannot cancel a completed task.")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class WhenAllTask(CompoundTask):
|
|
283
|
+
"""A Task representing `when_all` scenarios."""
|
|
284
|
+
|
|
285
|
+
def __init__(self, task: List[TaskBase], replay_schema: ReplaySchema):
|
|
286
|
+
"""Initialize a WhenAllTask.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
task : List[Task]
|
|
291
|
+
The list of child tasks
|
|
292
|
+
replay_schema : ReplaySchema
|
|
293
|
+
The ReplaySchema, which determines the inner action payload representation
|
|
294
|
+
"""
|
|
295
|
+
compound_action_constructor = None
|
|
296
|
+
if replay_schema.value >= ReplaySchema.V2.value:
|
|
297
|
+
compound_action_constructor = WhenAllAction
|
|
298
|
+
super().__init__(task, compound_action_constructor)
|
|
299
|
+
|
|
300
|
+
def try_set_value(self, child: TaskBase):
|
|
301
|
+
"""Transition a WhenAll Task to a terminal state and set its value.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
child : TaskBase
|
|
306
|
+
A sub-task that just completed
|
|
307
|
+
"""
|
|
308
|
+
if child.state is TaskState.SUCCEEDED:
|
|
309
|
+
# A WhenAll Task only completes when it has no pending tasks
|
|
310
|
+
# i.e _when all_ of its children have completed
|
|
311
|
+
if len(self.pending_tasks) == 0:
|
|
312
|
+
results = list(map(lambda x: x.result, self.children))
|
|
313
|
+
self.set_value(is_error=False, value=results)
|
|
314
|
+
else: # child.state is TaskState.FAILED:
|
|
315
|
+
# a single error is sufficient to fail this task
|
|
316
|
+
if self._first_error is None:
|
|
317
|
+
self._first_error = child.result
|
|
318
|
+
self.set_value(is_error=True, value=self._first_error)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class LongTimerTask(WhenAllTask):
|
|
322
|
+
"""A Timer Task for intervals longer than supported by the storage backend."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, id_, action: CreateTimerAction, orchestration_context):
|
|
325
|
+
"""Initialize a LongTimerTask.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
id_ : int
|
|
330
|
+
An ID for the task
|
|
331
|
+
action : CreateTimerAction
|
|
332
|
+
The action this task represents
|
|
333
|
+
orchestration_context: DurableOrchestrationContext
|
|
334
|
+
The orchestration context this task was created in
|
|
335
|
+
"""
|
|
336
|
+
current_time = orchestration_context.current_utc_datetime
|
|
337
|
+
final_fire_time = action.fire_at
|
|
338
|
+
duration_until_fire = final_fire_time - current_time
|
|
339
|
+
|
|
340
|
+
if duration_until_fire > orchestration_context._maximum_short_timer_duration:
|
|
341
|
+
next_fire_time = current_time + orchestration_context._long_timer_interval_duration
|
|
342
|
+
else:
|
|
343
|
+
next_fire_time = final_fire_time
|
|
344
|
+
|
|
345
|
+
next_timer_action = CreateTimerAction(next_fire_time)
|
|
346
|
+
next_timer_task = TimerTask(None, next_timer_action)
|
|
347
|
+
super().__init__([next_timer_task], orchestration_context._replay_schema)
|
|
348
|
+
|
|
349
|
+
self.id = id_
|
|
350
|
+
self.action = action
|
|
351
|
+
self._orchestration_context = orchestration_context
|
|
352
|
+
self._max_short_timer_duration = self._orchestration_context._maximum_short_timer_duration
|
|
353
|
+
self._long_timer_interval = self._orchestration_context._long_timer_interval_duration
|
|
354
|
+
|
|
355
|
+
def is_canceled(self) -> bool:
|
|
356
|
+
"""Check if the LongTimer is cancelled.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
bool
|
|
361
|
+
Returns whether the timer has been cancelled or not
|
|
362
|
+
"""
|
|
363
|
+
return self.action.is_cancelled
|
|
364
|
+
|
|
365
|
+
def cancel(self):
|
|
366
|
+
"""Cancel a timer.
|
|
367
|
+
|
|
368
|
+
Raises
|
|
369
|
+
------
|
|
370
|
+
ValueError
|
|
371
|
+
Raises an error if the task is already completed and an attempt is made to cancel it
|
|
372
|
+
"""
|
|
373
|
+
if (self.result):
|
|
374
|
+
raise Exception("Cannot cancel a completed task.")
|
|
375
|
+
self.action.is_cancelled = True
|
|
376
|
+
|
|
377
|
+
def try_set_value(self, child: TimerTask):
|
|
378
|
+
"""Transition this LongTimer Task to a terminal state and set its value.
|
|
379
|
+
|
|
380
|
+
If the LongTimer has not yet reached the designated completion time, starts a new
|
|
381
|
+
TimerTask for the next interval and does not close.
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
child : TimerTask
|
|
386
|
+
A timer sub-task that just completed
|
|
387
|
+
"""
|
|
388
|
+
current_time = self._orchestration_context.current_utc_datetime
|
|
389
|
+
final_fire_time = self.action.fire_at
|
|
390
|
+
if final_fire_time > current_time:
|
|
391
|
+
next_timer = self.get_next_timer_task(final_fire_time, current_time)
|
|
392
|
+
self.add_new_child(next_timer)
|
|
393
|
+
return super().try_set_value(child)
|
|
394
|
+
|
|
395
|
+
def get_next_timer_task(self, final_fire_time: datetime, current_time: datetime) -> TimerTask:
|
|
396
|
+
"""Create a TimerTask to represent the next interval of the LongTimer.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
final_fire_time : datetime.datetime
|
|
401
|
+
The final firing time of the LongTimer
|
|
402
|
+
current_time : datetime.datetime
|
|
403
|
+
The current time
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
TimerTask
|
|
408
|
+
A TimerTask representing the next interval of the LongTimer
|
|
409
|
+
"""
|
|
410
|
+
duration_until_fire = final_fire_time - current_time
|
|
411
|
+
if duration_until_fire > self._max_short_timer_duration:
|
|
412
|
+
next_fire_time = current_time + self._long_timer_interval
|
|
413
|
+
else:
|
|
414
|
+
next_fire_time = final_fire_time
|
|
415
|
+
return TimerTask(None, CreateTimerAction(next_fire_time))
|
|
416
|
+
|
|
417
|
+
def add_new_child(self, child_timer: TimerTask):
|
|
418
|
+
"""Add the TimerTask to this task's children.
|
|
419
|
+
|
|
420
|
+
Also register the TimerTask with the orchestration context.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
child_timer : TimerTask
|
|
425
|
+
The newly created TimerTask to add
|
|
426
|
+
"""
|
|
427
|
+
child_timer.parent = self
|
|
428
|
+
self.pending_tasks.add(child_timer)
|
|
429
|
+
self._orchestration_context._add_to_open_tasks(child_timer)
|
|
430
|
+
self._orchestration_context._add_to_actions(child_timer.action_repr)
|
|
431
|
+
child_timer._set_is_scheduled(True)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class WhenAnyTask(CompoundTask):
|
|
435
|
+
"""A Task representing `when_any` scenarios."""
|
|
436
|
+
|
|
437
|
+
def __init__(self, task: List[TaskBase], replay_schema: ReplaySchema):
|
|
438
|
+
"""Initialize a WhenAnyTask.
|
|
439
|
+
|
|
440
|
+
Parameters
|
|
441
|
+
----------
|
|
442
|
+
task : List[Task]
|
|
443
|
+
The list of child tasks
|
|
444
|
+
replay_schema : ReplaySchema
|
|
445
|
+
The ReplaySchema, which determines the inner action payload representation
|
|
446
|
+
"""
|
|
447
|
+
compound_action_constructor = None
|
|
448
|
+
if replay_schema.value >= ReplaySchema.V2.value:
|
|
449
|
+
compound_action_constructor = WhenAnyAction
|
|
450
|
+
super().__init__(task, compound_action_constructor)
|
|
451
|
+
|
|
452
|
+
def try_set_value(self, child: TaskBase):
|
|
453
|
+
"""Transition a WhenAny Task to a terminal state and set its value.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
child : TaskBase
|
|
458
|
+
A sub-task that just completed
|
|
459
|
+
"""
|
|
460
|
+
if self.state is TaskState.RUNNING:
|
|
461
|
+
self.set_value(is_error=False, value=child)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class RetryAbleTask(WhenAllTask):
|
|
465
|
+
"""A Task representing `with_retry` scenarios.
|
|
466
|
+
|
|
467
|
+
It inherits from WhenAllTask because retryable scenarios are Tasks
|
|
468
|
+
with equivalent to WhenAll Tasks with dynamically increasing lists
|
|
469
|
+
of children. At every failure, we add a Timer child and a Task child
|
|
470
|
+
to the list of pending tasks.
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
def __init__(self, child: TaskBase, retry_options: RetryOptions, context):
|
|
474
|
+
tasks = [child]
|
|
475
|
+
super().__init__(tasks, context._replay_schema)
|
|
476
|
+
|
|
477
|
+
self.retry_options = retry_options
|
|
478
|
+
self.num_attempts = 1
|
|
479
|
+
self.context = context
|
|
480
|
+
self.actions = child.action_repr
|
|
481
|
+
self.is_waiting_on_timer = False
|
|
482
|
+
self.error = None
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def id_(self):
|
|
486
|
+
"""Obtain the task's ID.
|
|
487
|
+
|
|
488
|
+
Since this is an internal-only abstraction, the task ID is represented
|
|
489
|
+
by the ID of its inner/wrapped task _plus_ a suffix: "_retryable_proxy"
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
[type]
|
|
494
|
+
[description]
|
|
495
|
+
"""
|
|
496
|
+
return str(list(map(lambda x: x.id, self.children))) + "_retryable_proxy"
|
|
497
|
+
|
|
498
|
+
def try_set_value(self, child: TaskBase):
|
|
499
|
+
"""Transition a Retryable Task to a terminal state and set its value.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
child : TaskBase
|
|
504
|
+
A sub-task that just completed
|
|
505
|
+
"""
|
|
506
|
+
if self.is_waiting_on_timer:
|
|
507
|
+
# timer fired, re-scheduling original task
|
|
508
|
+
self.is_waiting_on_timer = False
|
|
509
|
+
# As per DTFx semantics: we need to check the number of retires only after the final
|
|
510
|
+
# timer has fired. This means we essentially have to wait for one "extra" timer after
|
|
511
|
+
# the maximum number of attempts has been reached. Removing this extra timer will cause
|
|
512
|
+
# stuck orchestrators as we need to be "in sync" with the replay logic of DTFx.
|
|
513
|
+
if self.num_attempts >= self.retry_options.max_number_of_attempts:
|
|
514
|
+
self.is_waiting_on_timer = True
|
|
515
|
+
# we have reached the maximum number of attempts, set error
|
|
516
|
+
self.set_value(is_error=True, value=self.error)
|
|
517
|
+
else:
|
|
518
|
+
rescheduled_task = self.context._generate_task(
|
|
519
|
+
action=NoOpAction("rescheduled task"), parent=self)
|
|
520
|
+
self.pending_tasks.add(rescheduled_task)
|
|
521
|
+
self.context._add_to_open_tasks(rescheduled_task)
|
|
522
|
+
self.num_attempts += 1
|
|
523
|
+
|
|
524
|
+
return
|
|
525
|
+
if child.state is TaskState.SUCCEEDED:
|
|
526
|
+
if len(self.pending_tasks) == 0:
|
|
527
|
+
# if all pending tasks have completed,
|
|
528
|
+
# and we have a successful child, then
|
|
529
|
+
# we can set the Task's event
|
|
530
|
+
self.set_value(is_error=False, value=child.result)
|
|
531
|
+
|
|
532
|
+
else: # child.state is TaskState.FAILED:
|
|
533
|
+
# increase size of pending tasks by adding a timer task
|
|
534
|
+
# when it completes, we'll retry the original task
|
|
535
|
+
timer_task = self.context._generate_task(
|
|
536
|
+
action=NoOpAction("-WithRetry timer"), parent=self)
|
|
537
|
+
self.pending_tasks.add(timer_task)
|
|
538
|
+
self.context._add_to_open_tasks(timer_task)
|
|
539
|
+
self.is_waiting_on_timer = True
|
|
540
|
+
self.error = child.result
|