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.
Files changed (104) 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 +260 -249
  5. azure/durable_functions/decorators/metadata.py +109 -109
  6. azure/durable_functions/entity.py +129 -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 +812 -781
  11. azure/durable_functions/models/DurableOrchestrationContext.py +761 -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 +9 -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 +540 -426
  22. azure/durable_functions/models/TaskOrchestrationExecutor.py +352 -336
  23. azure/durable_functions/models/TokenSource.py +56 -56
  24. azure/durable_functions/models/__init__.py +26 -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 +94 -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 +80 -69
  54. azure/durable_functions/models/utils/json_utils.py +96 -56
  55. azure/durable_functions/orchestrator.py +73 -71
  56. azure/durable_functions/testing/OrchestratorGeneratorWrapper.py +42 -0
  57. azure/durable_functions/testing/__init__.py +6 -0
  58. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE +21 -21
  59. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA +59 -58
  60. azure_functions_durable-1.3.0.dist-info/RECORD +103 -0
  61. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.dist-info}/WHEEL +1 -1
  62. tests/models/test_DecoratorMetadata.py +135 -135
  63. tests/models/test_Decorators.py +107 -107
  64. tests/models/test_DurableOrchestrationBindings.py +68 -68
  65. tests/models/test_DurableOrchestrationClient.py +730 -730
  66. tests/models/test_DurableOrchestrationContext.py +102 -102
  67. tests/models/test_DurableOrchestrationStatus.py +59 -59
  68. tests/models/test_OrchestrationState.py +28 -28
  69. tests/models/test_RpcManagementOptions.py +79 -79
  70. tests/models/test_TokenSource.py +10 -10
  71. tests/orchestrator/models/OrchestrationInstance.py +18 -18
  72. tests/orchestrator/orchestrator_test_utils.py +130 -130
  73. tests/orchestrator/schemas/OrchetrationStateSchema.py +66 -66
  74. tests/orchestrator/test_call_http.py +235 -176
  75. tests/orchestrator/test_continue_as_new.py +67 -67
  76. tests/orchestrator/test_create_timer.py +126 -126
  77. tests/orchestrator/test_entity.py +397 -395
  78. tests/orchestrator/test_external_event.py +53 -53
  79. tests/orchestrator/test_fan_out_fan_in.py +175 -175
  80. tests/orchestrator/test_is_replaying_flag.py +101 -101
  81. tests/orchestrator/test_retries.py +308 -308
  82. tests/orchestrator/test_sequential_orchestrator.py +841 -841
  83. tests/orchestrator/test_sequential_orchestrator_custom_status.py +119 -119
  84. tests/orchestrator/test_sequential_orchestrator_with_retry.py +465 -465
  85. tests/orchestrator/test_serialization.py +30 -30
  86. tests/orchestrator/test_sub_orchestrator.py +95 -95
  87. tests/orchestrator/test_sub_orchestrator_with_retry.py +129 -129
  88. tests/orchestrator/test_task_any.py +60 -60
  89. tests/tasks/tasks_test_utils.py +17 -17
  90. tests/tasks/test_long_timers.py +70 -0
  91. tests/tasks/test_new_uuid.py +34 -34
  92. tests/test_utils/ContextBuilder.py +174 -174
  93. tests/test_utils/EntityContextBuilder.py +56 -56
  94. tests/test_utils/constants.py +1 -1
  95. tests/test_utils/json_utils.py +30 -30
  96. tests/test_utils/testClasses.py +56 -56
  97. tests/utils/__init__.py +1 -0
  98. tests/utils/test_entity_utils.py +24 -0
  99. azure_functions_durable-1.2.9.data/data/_manifest/bsi.json +0 -1
  100. azure_functions_durable-1.2.9.data/data/_manifest/manifest.cat +0 -0
  101. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json +0 -11985
  102. azure_functions_durable-1.2.9.data/data/_manifest/manifest.spdx.json.sha256 +0 -1
  103. azure_functions_durable-1.2.9.dist-info/RECORD +0 -102
  104. {azure_functions_durable-1.2.9.dist-info → azure_functions_durable-1.3.0.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"]