azure-functions-durable 1.2.8__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 -711
- 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 -29
- 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 -333
- 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 -25
- 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.8.dist-info → azure_functions_durable-1.2.10.dist-info}/LICENSE +21 -21
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.dist-info}/METADATA +58 -58
- azure_functions_durable-1.2.10.dist-info/RECORD +100 -0
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.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 -56
- tests/models/test_DurableOrchestrationClient.py +730 -612
- 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 -801
- 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.8.data/data/_manifest/bsi.json +0 -1
- azure_functions_durable-1.2.8.data/data/_manifest/manifest.cat +0 -0
- azure_functions_durable-1.2.8.data/data/_manifest/manifest.spdx.json +0 -12845
- azure_functions_durable-1.2.8.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
- azure_functions_durable-1.2.8.dist-info/RECORD +0 -102
- {azure_functions_durable-1.2.8.dist-info → azure_functions_durable-1.2.10.dist-info}/top_level.txt +0 -0
|
@@ -1,309 +1,309 @@
|
|
|
1
|
-
from tests.test_utils.ContextBuilder import ContextBuilder
|
|
2
|
-
from tests.test_utils.testClasses import SerializableClass
|
|
3
|
-
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
4
|
-
from azure.durable_functions.models.OrchestratorState import OrchestratorState
|
|
5
|
-
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
6
|
-
from .orchestrator_test_utils import get_orchestration_state_result
|
|
7
|
-
from typing import List, Tuple
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
|
|
10
|
-
RETRY_OPTIONS = RetryOptions(5000, 2)
|
|
11
|
-
REASONS = "Stuff"
|
|
12
|
-
DETAILS = "Things"
|
|
13
|
-
RESULT_PREFIX = "Hello "
|
|
14
|
-
CITIES = ["Tokyo", "Seattle", "London"]
|
|
15
|
-
|
|
16
|
-
def generator_function(context: DurableOrchestrationContext):
|
|
17
|
-
"""Orchestrator function for testing retry'ing semantics
|
|
18
|
-
|
|
19
|
-
Parameters
|
|
20
|
-
----------
|
|
21
|
-
context: DurableOrchestrationContext
|
|
22
|
-
Durable orchestration context, exposes the Durable API
|
|
23
|
-
|
|
24
|
-
Returns
|
|
25
|
-
-------
|
|
26
|
-
List[str]:
|
|
27
|
-
Output of activities, a list of hello'd cities
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
outputs = []
|
|
31
|
-
|
|
32
|
-
retry_options = RETRY_OPTIONS
|
|
33
|
-
task1 = yield context.call_activity_with_retry(
|
|
34
|
-
"Hello", retry_options, "Tokyo")
|
|
35
|
-
task2 = yield context.call_activity_with_retry(
|
|
36
|
-
"Hello", retry_options, "Seattle")
|
|
37
|
-
task3 = yield context.call_activity_with_retry(
|
|
38
|
-
"Hello", retry_options, "London")
|
|
39
|
-
|
|
40
|
-
outputs.append(task1)
|
|
41
|
-
outputs.append(task2)
|
|
42
|
-
outputs.append(task3)
|
|
43
|
-
|
|
44
|
-
return outputs
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def generator_function_with_serialization(context: DurableOrchestrationContext):
|
|
48
|
-
"""Orchestrator function for testing retry'ing with serializable input arguments.
|
|
49
|
-
|
|
50
|
-
Parameters
|
|
51
|
-
----------
|
|
52
|
-
context: DurableOrchestrationContext
|
|
53
|
-
Durable orchestration context, exposes the Durable API
|
|
54
|
-
|
|
55
|
-
Returns
|
|
56
|
-
-------
|
|
57
|
-
List[str]:
|
|
58
|
-
Output of activities, a list of hello'd cities
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
outputs = []
|
|
62
|
-
|
|
63
|
-
retry_options = RETRY_OPTIONS
|
|
64
|
-
task1 = yield context.call_activity_with_retry(
|
|
65
|
-
"Hello", retry_options, SerializableClass("Tokyo"))
|
|
66
|
-
task2 = yield context.call_activity_with_retry(
|
|
67
|
-
"Hello", retry_options, SerializableClass("Seatlle"))
|
|
68
|
-
task3 = yield context.call_activity_with_retry(
|
|
69
|
-
"Hello", retry_options, SerializableClass("London"))
|
|
70
|
-
|
|
71
|
-
outputs.append(task1)
|
|
72
|
-
outputs.append(task2)
|
|
73
|
-
outputs.append(task3)
|
|
74
|
-
|
|
75
|
-
return outputs
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_context_with_retries_and_corrupted_completion() -> ContextBuilder:
|
|
79
|
-
"""Get a ContextBuilder whose history contains a late completion event
|
|
80
|
-
for an event that already failed.
|
|
81
|
-
|
|
82
|
-
Returns
|
|
83
|
-
-------
|
|
84
|
-
ContextBuilder:
|
|
85
|
-
The context whose history contains the requested event sequence.
|
|
86
|
-
"""
|
|
87
|
-
context = get_context_with_retries()
|
|
88
|
-
context.add_orchestrator_started_event()
|
|
89
|
-
context.add_task_completed_event(id_=0, result="'Do not pick me up'")
|
|
90
|
-
context.add_orchestrator_completed_event()
|
|
91
|
-
return context
|
|
92
|
-
|
|
93
|
-
def get_context_with_retries(will_fail: bool=False) -> ContextBuilder:
|
|
94
|
-
"""Get a ContextBuilder whose history contains retried events.
|
|
95
|
-
|
|
96
|
-
Parameters
|
|
97
|
-
----------
|
|
98
|
-
will_fail: (bool, optional)
|
|
99
|
-
If set to true, returns a context with a history where the orchestrator fails.
|
|
100
|
-
If false, returns a context with a history where events fail but eventually complete.
|
|
101
|
-
Defaults to False.
|
|
102
|
-
|
|
103
|
-
Returns
|
|
104
|
-
-------
|
|
105
|
-
ContextBuilder:
|
|
106
|
-
The context whose history contains the requested event sequence.
|
|
107
|
-
"""
|
|
108
|
-
context = ContextBuilder()
|
|
109
|
-
num_activities = len(CITIES)
|
|
110
|
-
|
|
111
|
-
def _schedule_events(context: ContextBuilder, id_counter: int) -> Tuple[ContextBuilder, int, List[int]]:
|
|
112
|
-
"""Add scheduled events to the context.
|
|
113
|
-
|
|
114
|
-
Parameters
|
|
115
|
-
----------
|
|
116
|
-
context: ContextBuilder
|
|
117
|
-
Orchestration context mock, to which we'll add the event completion events
|
|
118
|
-
id_counter: int
|
|
119
|
-
The current event counter
|
|
120
|
-
|
|
121
|
-
Returns
|
|
122
|
-
-------
|
|
123
|
-
Tuple[ContextBuilder, int, List[int]]:
|
|
124
|
-
The updated context, the updated counter, a list of event IDs for each scheduled event
|
|
125
|
-
"""
|
|
126
|
-
id_counter = id_counter + 1
|
|
127
|
-
context.add_task_scheduled_event(name='Hello', id_=id_counter)
|
|
128
|
-
return context, id_counter
|
|
129
|
-
|
|
130
|
-
def _fail_events(context: ContextBuilder, id_counter: int) -> Tuple[ContextBuilder, int]:
|
|
131
|
-
"""Add event failed to the context.
|
|
132
|
-
|
|
133
|
-
Parameters
|
|
134
|
-
----------
|
|
135
|
-
context: ContextBuilder
|
|
136
|
-
Orchestration context mock, to which we'll add the event completion events
|
|
137
|
-
id_counter: int
|
|
138
|
-
The current event counter
|
|
139
|
-
|
|
140
|
-
Returns
|
|
141
|
-
-------
|
|
142
|
-
Tuple[ContextBuilder, int]:
|
|
143
|
-
The updated context, the updated id_counter
|
|
144
|
-
"""
|
|
145
|
-
context.add_orchestrator_started_event()
|
|
146
|
-
context.add_task_failed_event(
|
|
147
|
-
id_=id_counter, reason=REASONS, details=DETAILS)
|
|
148
|
-
return context, id_counter
|
|
149
|
-
|
|
150
|
-
def _schedule_timers(context: ContextBuilder, id_counter: int) -> Tuple[ContextBuilder, int, List[datetime]]:
|
|
151
|
-
"""Add timer created events to the context.
|
|
152
|
-
|
|
153
|
-
Parameters
|
|
154
|
-
----------
|
|
155
|
-
context: ContextBuilder
|
|
156
|
-
Orchestration context mock, to which we'll add the event completion events
|
|
157
|
-
id_counter: int
|
|
158
|
-
The current event counter
|
|
159
|
-
|
|
160
|
-
Returns
|
|
161
|
-
-------
|
|
162
|
-
Tuple[ContextBuilder, int, List[datetime]]:
|
|
163
|
-
The updated context, the updated counter, a list of timer deadlines
|
|
164
|
-
"""
|
|
165
|
-
id_counter = id_counter + 1
|
|
166
|
-
deadlines: List[datetime] = []
|
|
167
|
-
deadlines.append((id_counter, context.add_timer_created_event(id_counter)))
|
|
168
|
-
return context, id_counter, deadlines
|
|
169
|
-
|
|
170
|
-
def _fire_timer(context: ContextBuilder, id_counter: int, deadlines: List[datetime]) -> Tuple[ContextBuilder, int]:
|
|
171
|
-
"""Add timer fired events to the context.
|
|
172
|
-
|
|
173
|
-
Parameters
|
|
174
|
-
----------
|
|
175
|
-
context: ContextBuilder
|
|
176
|
-
Orchestration context mock, to which we'll add the event completion events
|
|
177
|
-
id_counter: int
|
|
178
|
-
The current event counter
|
|
179
|
-
deadlines: List[datetime]
|
|
180
|
-
List of dates at which to fire the timers
|
|
181
|
-
|
|
182
|
-
Returns
|
|
183
|
-
-------
|
|
184
|
-
Tuple[ContextBuilder, int]:
|
|
185
|
-
The updated context, the updated id_counter
|
|
186
|
-
"""
|
|
187
|
-
for id_, fire_at in deadlines:
|
|
188
|
-
context.add_timer_fired_event(id_=id_, fire_at=fire_at)
|
|
189
|
-
return context, id_counter
|
|
190
|
-
|
|
191
|
-
def _complete_event(context: ContextBuilder, id_counter: int, city:str) -> Tuple[ContextBuilder, int]:
|
|
192
|
-
"""Add event / task completions to the context.
|
|
193
|
-
|
|
194
|
-
Parameters
|
|
195
|
-
----------
|
|
196
|
-
context: ContextBuilder
|
|
197
|
-
Orchestration context mock, to which we'll add the event completion events
|
|
198
|
-
id_counter: int
|
|
199
|
-
The current event counter
|
|
200
|
-
|
|
201
|
-
Returns
|
|
202
|
-
-------
|
|
203
|
-
Tuple[ContextBuilder, int]
|
|
204
|
-
The updated context, the updated id_counter
|
|
205
|
-
"""
|
|
206
|
-
result = f"\"{RESULT_PREFIX}{city}\""
|
|
207
|
-
context.add_task_completed_event(id_=id_counter, result=result)
|
|
208
|
-
return context, id_counter
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
id_counter = -1
|
|
212
|
-
|
|
213
|
-
for city in CITIES:
|
|
214
|
-
# Schedule the events
|
|
215
|
-
context, id_counter = _schedule_events(context, id_counter)
|
|
216
|
-
context.add_orchestrator_completed_event()
|
|
217
|
-
|
|
218
|
-
# Record failures, schedule timers
|
|
219
|
-
context, id_counter = _fail_events(context, id_counter)
|
|
220
|
-
context, id_counter, deadlines = _schedule_timers(context, id_counter)
|
|
221
|
-
context.add_orchestrator_completed_event()
|
|
222
|
-
|
|
223
|
-
# Fire timers, re-schedule events
|
|
224
|
-
context.add_orchestrator_started_event()
|
|
225
|
-
context, id_counter = _fire_timer(context, id_counter, deadlines)
|
|
226
|
-
context, id_counter = _schedule_events(context, id_counter)
|
|
227
|
-
context.add_orchestrator_completed_event()
|
|
228
|
-
|
|
229
|
-
context.add_orchestrator_started_event()
|
|
230
|
-
|
|
231
|
-
# Either complete the event or, if we want all failed events, then
|
|
232
|
-
# fail the events, schedule timer, and fire time.
|
|
233
|
-
if will_fail:
|
|
234
|
-
context, id_counter = _fail_events(context, id_counter)
|
|
235
|
-
context, id_counter, deadlines = _schedule_timers(context, id_counter)
|
|
236
|
-
context.add_orchestrator_completed_event()
|
|
237
|
-
|
|
238
|
-
context.add_orchestrator_started_event()
|
|
239
|
-
context, id_counter = _fire_timer(context, id_counter, deadlines)
|
|
240
|
-
else:
|
|
241
|
-
context, id_counter = _complete_event(context, id_counter, city)
|
|
242
|
-
|
|
243
|
-
context.add_orchestrator_completed_event()
|
|
244
|
-
return context
|
|
245
|
-
|
|
246
|
-
def test_redundant_completion_doesnt_get_processed():
|
|
247
|
-
"""Tests that our implementation processes the state array
|
|
248
|
-
sequentially, which previous implementations did not guarantee. In this test,
|
|
249
|
-
we add a completion event for a task that was cancelled, meaning that it failed and got
|
|
250
|
-
re-scheduled. Older implementations would pick up this completion event and cause
|
|
251
|
-
non-determinism.
|
|
252
|
-
"""
|
|
253
|
-
context_1 = get_context_with_retries()
|
|
254
|
-
context_2 = get_context_with_retries_and_corrupted_completion()
|
|
255
|
-
|
|
256
|
-
result_1 = get_orchestration_state_result(
|
|
257
|
-
context_1, generator_function)
|
|
258
|
-
|
|
259
|
-
result_2 = get_orchestration_state_result(
|
|
260
|
-
context_2, generator_function)
|
|
261
|
-
|
|
262
|
-
assert "output" in result_1
|
|
263
|
-
assert "output" in result_2
|
|
264
|
-
assert result_1["output"] == result_2["output"]
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def test_failed_tasks_do_not_hang_orchestrator():
|
|
268
|
-
"""Tests that our implementation correctly handles up re-scheduled events,
|
|
269
|
-
which previous implementations failed to correctly handle. """
|
|
270
|
-
context = get_context_with_retries()
|
|
271
|
-
|
|
272
|
-
result = get_orchestration_state_result(
|
|
273
|
-
context, generator_function)
|
|
274
|
-
|
|
275
|
-
expected_output = list(map(lambda x: RESULT_PREFIX + x, CITIES))
|
|
276
|
-
assert "output" in result
|
|
277
|
-
assert result["output"] == expected_output
|
|
278
|
-
|
|
279
|
-
def test_retries_can_fail():
|
|
280
|
-
"""Tests the code path where a retry'ed Task fails"""
|
|
281
|
-
context = get_context_with_retries(will_fail=True)
|
|
282
|
-
|
|
283
|
-
try:
|
|
284
|
-
result = get_orchestration_state_result(
|
|
285
|
-
context, generator_function)
|
|
286
|
-
# We expected an exception
|
|
287
|
-
assert False
|
|
288
|
-
except Exception as e:
|
|
289
|
-
error_label = "\n\n$OutOfProcData$:"
|
|
290
|
-
error_str = str(e)
|
|
291
|
-
|
|
292
|
-
error_msg = f"{REASONS} \n {DETAILS}"
|
|
293
|
-
|
|
294
|
-
expected_error_str = f"{error_msg}{error_label}"
|
|
295
|
-
assert str.startswith(error_str, expected_error_str)
|
|
296
|
-
|
|
297
|
-
def test_retries_with_serializable_input():
|
|
298
|
-
# Tests that retried tasks work with serialized input classes.
|
|
299
|
-
context = get_context_with_retries()
|
|
300
|
-
|
|
301
|
-
result_1 = get_orchestration_state_result(
|
|
302
|
-
context, generator_function)
|
|
303
|
-
|
|
304
|
-
result_2 = get_orchestration_state_result(
|
|
305
|
-
context, generator_function_with_serialization)
|
|
306
|
-
|
|
307
|
-
assert "output" in result_1
|
|
308
|
-
assert "output" in result_2
|
|
1
|
+
from tests.test_utils.ContextBuilder import ContextBuilder
|
|
2
|
+
from tests.test_utils.testClasses import SerializableClass
|
|
3
|
+
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
4
|
+
from azure.durable_functions.models.OrchestratorState import OrchestratorState
|
|
5
|
+
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
6
|
+
from .orchestrator_test_utils import get_orchestration_state_result
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
RETRY_OPTIONS = RetryOptions(5000, 2)
|
|
11
|
+
REASONS = "Stuff"
|
|
12
|
+
DETAILS = "Things"
|
|
13
|
+
RESULT_PREFIX = "Hello "
|
|
14
|
+
CITIES = ["Tokyo", "Seattle", "London"]
|
|
15
|
+
|
|
16
|
+
def generator_function(context: DurableOrchestrationContext):
|
|
17
|
+
"""Orchestrator function for testing retry'ing semantics
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
context: DurableOrchestrationContext
|
|
22
|
+
Durable orchestration context, exposes the Durable API
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
List[str]:
|
|
27
|
+
Output of activities, a list of hello'd cities
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
outputs = []
|
|
31
|
+
|
|
32
|
+
retry_options = RETRY_OPTIONS
|
|
33
|
+
task1 = yield context.call_activity_with_retry(
|
|
34
|
+
"Hello", retry_options, "Tokyo")
|
|
35
|
+
task2 = yield context.call_activity_with_retry(
|
|
36
|
+
"Hello", retry_options, "Seattle")
|
|
37
|
+
task3 = yield context.call_activity_with_retry(
|
|
38
|
+
"Hello", retry_options, "London")
|
|
39
|
+
|
|
40
|
+
outputs.append(task1)
|
|
41
|
+
outputs.append(task2)
|
|
42
|
+
outputs.append(task3)
|
|
43
|
+
|
|
44
|
+
return outputs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generator_function_with_serialization(context: DurableOrchestrationContext):
|
|
48
|
+
"""Orchestrator function for testing retry'ing with serializable input arguments.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
context: DurableOrchestrationContext
|
|
53
|
+
Durable orchestration context, exposes the Durable API
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
List[str]:
|
|
58
|
+
Output of activities, a list of hello'd cities
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
outputs = []
|
|
62
|
+
|
|
63
|
+
retry_options = RETRY_OPTIONS
|
|
64
|
+
task1 = yield context.call_activity_with_retry(
|
|
65
|
+
"Hello", retry_options, SerializableClass("Tokyo"))
|
|
66
|
+
task2 = yield context.call_activity_with_retry(
|
|
67
|
+
"Hello", retry_options, SerializableClass("Seatlle"))
|
|
68
|
+
task3 = yield context.call_activity_with_retry(
|
|
69
|
+
"Hello", retry_options, SerializableClass("London"))
|
|
70
|
+
|
|
71
|
+
outputs.append(task1)
|
|
72
|
+
outputs.append(task2)
|
|
73
|
+
outputs.append(task3)
|
|
74
|
+
|
|
75
|
+
return outputs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_context_with_retries_and_corrupted_completion() -> ContextBuilder:
|
|
79
|
+
"""Get a ContextBuilder whose history contains a late completion event
|
|
80
|
+
for an event that already failed.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
ContextBuilder:
|
|
85
|
+
The context whose history contains the requested event sequence.
|
|
86
|
+
"""
|
|
87
|
+
context = get_context_with_retries()
|
|
88
|
+
context.add_orchestrator_started_event()
|
|
89
|
+
context.add_task_completed_event(id_=0, result="'Do not pick me up'")
|
|
90
|
+
context.add_orchestrator_completed_event()
|
|
91
|
+
return context
|
|
92
|
+
|
|
93
|
+
def get_context_with_retries(will_fail: bool=False) -> ContextBuilder:
|
|
94
|
+
"""Get a ContextBuilder whose history contains retried events.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
will_fail: (bool, optional)
|
|
99
|
+
If set to true, returns a context with a history where the orchestrator fails.
|
|
100
|
+
If false, returns a context with a history where events fail but eventually complete.
|
|
101
|
+
Defaults to False.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
ContextBuilder:
|
|
106
|
+
The context whose history contains the requested event sequence.
|
|
107
|
+
"""
|
|
108
|
+
context = ContextBuilder()
|
|
109
|
+
num_activities = len(CITIES)
|
|
110
|
+
|
|
111
|
+
def _schedule_events(context: ContextBuilder, id_counter: int) -> Tuple[ContextBuilder, int, List[int]]:
|
|
112
|
+
"""Add scheduled events to the context.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
context: ContextBuilder
|
|
117
|
+
Orchestration context mock, to which we'll add the event completion events
|
|
118
|
+
id_counter: int
|
|
119
|
+
The current event counter
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Tuple[ContextBuilder, int, List[int]]:
|
|
124
|
+
The updated context, the updated counter, a list of event IDs for each scheduled event
|
|
125
|
+
"""
|
|
126
|
+
id_counter = id_counter + 1
|
|
127
|
+
context.add_task_scheduled_event(name='Hello', id_=id_counter)
|
|
128
|
+
return context, id_counter
|
|
129
|
+
|
|
130
|
+
def _fail_events(context: ContextBuilder, id_counter: int) -> Tuple[ContextBuilder, int]:
|
|
131
|
+
"""Add event failed to the context.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
context: ContextBuilder
|
|
136
|
+
Orchestration context mock, to which we'll add the event completion events
|
|
137
|
+
id_counter: int
|
|
138
|
+
The current event counter
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
Tuple[ContextBuilder, int]:
|
|
143
|
+
The updated context, the updated id_counter
|
|
144
|
+
"""
|
|
145
|
+
context.add_orchestrator_started_event()
|
|
146
|
+
context.add_task_failed_event(
|
|
147
|
+
id_=id_counter, reason=REASONS, details=DETAILS)
|
|
148
|
+
return context, id_counter
|
|
149
|
+
|
|
150
|
+
def _schedule_timers(context: ContextBuilder, id_counter: int) -> Tuple[ContextBuilder, int, List[datetime]]:
|
|
151
|
+
"""Add timer created events to the context.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
context: ContextBuilder
|
|
156
|
+
Orchestration context mock, to which we'll add the event completion events
|
|
157
|
+
id_counter: int
|
|
158
|
+
The current event counter
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
Tuple[ContextBuilder, int, List[datetime]]:
|
|
163
|
+
The updated context, the updated counter, a list of timer deadlines
|
|
164
|
+
"""
|
|
165
|
+
id_counter = id_counter + 1
|
|
166
|
+
deadlines: List[datetime] = []
|
|
167
|
+
deadlines.append((id_counter, context.add_timer_created_event(id_counter)))
|
|
168
|
+
return context, id_counter, deadlines
|
|
169
|
+
|
|
170
|
+
def _fire_timer(context: ContextBuilder, id_counter: int, deadlines: List[datetime]) -> Tuple[ContextBuilder, int]:
|
|
171
|
+
"""Add timer fired events to the context.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
context: ContextBuilder
|
|
176
|
+
Orchestration context mock, to which we'll add the event completion events
|
|
177
|
+
id_counter: int
|
|
178
|
+
The current event counter
|
|
179
|
+
deadlines: List[datetime]
|
|
180
|
+
List of dates at which to fire the timers
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
Tuple[ContextBuilder, int]:
|
|
185
|
+
The updated context, the updated id_counter
|
|
186
|
+
"""
|
|
187
|
+
for id_, fire_at in deadlines:
|
|
188
|
+
context.add_timer_fired_event(id_=id_, fire_at=fire_at)
|
|
189
|
+
return context, id_counter
|
|
190
|
+
|
|
191
|
+
def _complete_event(context: ContextBuilder, id_counter: int, city:str) -> Tuple[ContextBuilder, int]:
|
|
192
|
+
"""Add event / task completions to the context.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
context: ContextBuilder
|
|
197
|
+
Orchestration context mock, to which we'll add the event completion events
|
|
198
|
+
id_counter: int
|
|
199
|
+
The current event counter
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
Tuple[ContextBuilder, int]
|
|
204
|
+
The updated context, the updated id_counter
|
|
205
|
+
"""
|
|
206
|
+
result = f"\"{RESULT_PREFIX}{city}\""
|
|
207
|
+
context.add_task_completed_event(id_=id_counter, result=result)
|
|
208
|
+
return context, id_counter
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
id_counter = -1
|
|
212
|
+
|
|
213
|
+
for city in CITIES:
|
|
214
|
+
# Schedule the events
|
|
215
|
+
context, id_counter = _schedule_events(context, id_counter)
|
|
216
|
+
context.add_orchestrator_completed_event()
|
|
217
|
+
|
|
218
|
+
# Record failures, schedule timers
|
|
219
|
+
context, id_counter = _fail_events(context, id_counter)
|
|
220
|
+
context, id_counter, deadlines = _schedule_timers(context, id_counter)
|
|
221
|
+
context.add_orchestrator_completed_event()
|
|
222
|
+
|
|
223
|
+
# Fire timers, re-schedule events
|
|
224
|
+
context.add_orchestrator_started_event()
|
|
225
|
+
context, id_counter = _fire_timer(context, id_counter, deadlines)
|
|
226
|
+
context, id_counter = _schedule_events(context, id_counter)
|
|
227
|
+
context.add_orchestrator_completed_event()
|
|
228
|
+
|
|
229
|
+
context.add_orchestrator_started_event()
|
|
230
|
+
|
|
231
|
+
# Either complete the event or, if we want all failed events, then
|
|
232
|
+
# fail the events, schedule timer, and fire time.
|
|
233
|
+
if will_fail:
|
|
234
|
+
context, id_counter = _fail_events(context, id_counter)
|
|
235
|
+
context, id_counter, deadlines = _schedule_timers(context, id_counter)
|
|
236
|
+
context.add_orchestrator_completed_event()
|
|
237
|
+
|
|
238
|
+
context.add_orchestrator_started_event()
|
|
239
|
+
context, id_counter = _fire_timer(context, id_counter, deadlines)
|
|
240
|
+
else:
|
|
241
|
+
context, id_counter = _complete_event(context, id_counter, city)
|
|
242
|
+
|
|
243
|
+
context.add_orchestrator_completed_event()
|
|
244
|
+
return context
|
|
245
|
+
|
|
246
|
+
def test_redundant_completion_doesnt_get_processed():
|
|
247
|
+
"""Tests that our implementation processes the state array
|
|
248
|
+
sequentially, which previous implementations did not guarantee. In this test,
|
|
249
|
+
we add a completion event for a task that was cancelled, meaning that it failed and got
|
|
250
|
+
re-scheduled. Older implementations would pick up this completion event and cause
|
|
251
|
+
non-determinism.
|
|
252
|
+
"""
|
|
253
|
+
context_1 = get_context_with_retries()
|
|
254
|
+
context_2 = get_context_with_retries_and_corrupted_completion()
|
|
255
|
+
|
|
256
|
+
result_1 = get_orchestration_state_result(
|
|
257
|
+
context_1, generator_function)
|
|
258
|
+
|
|
259
|
+
result_2 = get_orchestration_state_result(
|
|
260
|
+
context_2, generator_function)
|
|
261
|
+
|
|
262
|
+
assert "output" in result_1
|
|
263
|
+
assert "output" in result_2
|
|
264
|
+
assert result_1["output"] == result_2["output"]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_failed_tasks_do_not_hang_orchestrator():
|
|
268
|
+
"""Tests that our implementation correctly handles up re-scheduled events,
|
|
269
|
+
which previous implementations failed to correctly handle. """
|
|
270
|
+
context = get_context_with_retries()
|
|
271
|
+
|
|
272
|
+
result = get_orchestration_state_result(
|
|
273
|
+
context, generator_function)
|
|
274
|
+
|
|
275
|
+
expected_output = list(map(lambda x: RESULT_PREFIX + x, CITIES))
|
|
276
|
+
assert "output" in result
|
|
277
|
+
assert result["output"] == expected_output
|
|
278
|
+
|
|
279
|
+
def test_retries_can_fail():
|
|
280
|
+
"""Tests the code path where a retry'ed Task fails"""
|
|
281
|
+
context = get_context_with_retries(will_fail=True)
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
result = get_orchestration_state_result(
|
|
285
|
+
context, generator_function)
|
|
286
|
+
# We expected an exception
|
|
287
|
+
assert False
|
|
288
|
+
except Exception as e:
|
|
289
|
+
error_label = "\n\n$OutOfProcData$:"
|
|
290
|
+
error_str = str(e)
|
|
291
|
+
|
|
292
|
+
error_msg = f"{REASONS} \n {DETAILS}"
|
|
293
|
+
|
|
294
|
+
expected_error_str = f"{error_msg}{error_label}"
|
|
295
|
+
assert str.startswith(error_str, expected_error_str)
|
|
296
|
+
|
|
297
|
+
def test_retries_with_serializable_input():
|
|
298
|
+
# Tests that retried tasks work with serialized input classes.
|
|
299
|
+
context = get_context_with_retries()
|
|
300
|
+
|
|
301
|
+
result_1 = get_orchestration_state_result(
|
|
302
|
+
context, generator_function)
|
|
303
|
+
|
|
304
|
+
result_2 = get_orchestration_state_result(
|
|
305
|
+
context, generator_function_with_serialization)
|
|
306
|
+
|
|
307
|
+
assert "output" in result_1
|
|
308
|
+
assert "output" in result_2
|
|
309
309
|
assert result_1["output"] == result_2["output"]
|