1
+ from typing import List , Union
1
2
from azure .durable_functions .models .ReplaySchema import ReplaySchema
2
3
from .orchestrator_test_utils \
3
4
import get_orchestration_state_result , assert_orchestration_state_equals , assert_valid_schema
@@ -28,16 +29,63 @@ def generator_function(context):
28
29
29
30
return outputs
30
31
32
+ def generator_function_concurrent_retries (context ):
33
+ outputs = []
34
+
35
+ retry_options = RETRY_OPTIONS
36
+ task1 = context .call_activity_with_retry (
37
+ "Hello" , retry_options , "Tokyo" )
38
+ task2 = context .call_activity_with_retry (
39
+ "Hello" , retry_options , "Seattle" )
40
+ task3 = context .call_activity_with_retry (
41
+ "Hello" , retry_options , "London" )
42
+
43
+ outputs = yield context .task_all ([task1 , task2 , task3 ])
44
+
45
+ return outputs
46
+
47
+ def generator_function_two_concurrent_retries_when_all (context ):
48
+ outputs = []
49
+
50
+ retry_options = RETRY_OPTIONS
51
+ task1 = context .call_activity_with_retry (
52
+ "Hello" , retry_options , "Tokyo" )
53
+ task2 = context .call_activity_with_retry (
54
+ "Hello" , retry_options , "Seattle" )
55
+
56
+ outputs = yield context .task_all ([task1 , task2 ])
57
+
58
+ return outputs
59
+
60
+ def generator_function_two_concurrent_retries_when_any (context ):
61
+ outputs = []
62
+
63
+ retry_options = RETRY_OPTIONS
64
+ task1 = context .call_activity_with_retry (
65
+ "Hello" , retry_options , "Tokyo" )
66
+ task2 = context .call_activity_with_retry (
67
+ "Hello" , retry_options , "Seattle" )
68
+
69
+ outputs = yield context .task_any ([task1 , task2 ])
70
+
71
+ return outputs .result
72
+
31
73
32
74
def base_expected_state (output = None , replay_schema : ReplaySchema = ReplaySchema .V1 ) -> OrchestratorState :
33
75
return OrchestratorState (is_done = False , actions = [], output = output , replay_schema = replay_schema .value )
34
76
35
77
36
- def add_hello_action (state : OrchestratorState , input_ : str ):
78
+ def add_hello_action (state : OrchestratorState , input_ : Union [ List [ str ], str ] ):
37
79
retry_options = RETRY_OPTIONS
38
- action = CallActivityWithRetryAction (
39
- function_name = 'Hello' , retry_options = retry_options , input_ = input_ )
40
- state ._actions .append ([action ])
80
+ actions = []
81
+ inputs = input_
82
+ if not isinstance (input_ , list ):
83
+ inputs = [input_ ]
84
+ for input_ in inputs :
85
+ action = CallActivityWithRetryAction (
86
+ function_name = 'Hello' , retry_options = retry_options , input_ = input_ )
87
+ actions .append (action )
88
+ state ._actions .append (actions )
41
89
42
90
43
91
def add_hello_failed_events (
@@ -63,6 +111,45 @@ def add_retry_timer_events(context_builder: ContextBuilder, id_: int):
63
111
context_builder .add_orchestrator_started_event ()
64
112
context_builder .add_timer_fired_event (id_ = id_ , fire_at = fire_at )
65
113
114
+ def add_two_retriable_events_completing_out_of_order (context_builder : ContextBuilder ,
115
+ failed_reason , failed_details ):
116
+ ## Schedule tasks
117
+ context_builder .add_task_scheduled_event (name = 'Hello' , id_ = 0 ) # Tokyo task
118
+ context_builder .add_task_scheduled_event (name = 'Hello' , id_ = 1 ) # Seattle task
119
+
120
+ context_builder .add_orchestrator_completed_event ()
121
+ context_builder .add_orchestrator_started_event ()
122
+
123
+ ## Task failures and timer-scheduling
124
+
125
+ # tasks fail "out of order"
126
+ context_builder .add_task_failed_event (
127
+ id_ = 1 , reason = failed_reason , details = failed_details ) # Seattle task
128
+ fire_at_1 = context_builder .add_timer_created_event (2 ) # Seattle timer
129
+
130
+ context_builder .add_orchestrator_completed_event ()
131
+ context_builder .add_orchestrator_started_event ()
132
+
133
+ context_builder .add_task_failed_event (
134
+ id_ = 0 , reason = failed_reason , details = failed_details ) # Tokyo task
135
+ fire_at_2 = context_builder .add_timer_created_event (3 ) # Tokyo timer
136
+
137
+ context_builder .add_orchestrator_completed_event ()
138
+ context_builder .add_orchestrator_started_event ()
139
+
140
+ ## fire timers
141
+ context_builder .add_timer_fired_event (id_ = 2 , fire_at = fire_at_1 ) # Seattle timer
142
+ context_builder .add_timer_fired_event (id_ = 3 , fire_at = fire_at_2 ) # Tokyo timer
143
+
144
+ ## Complete events
145
+ context_builder .add_task_scheduled_event (name = 'Hello' , id_ = 4 ) # Seattle task
146
+ context_builder .add_task_scheduled_event (name = 'Hello' , id_ = 5 ) # Tokyo task
147
+
148
+ context_builder .add_orchestrator_completed_event ()
149
+ context_builder .add_orchestrator_started_event ()
150
+ context_builder .add_task_completed_event (id_ = 4 , result = "\" Hello Seattle!\" " )
151
+ context_builder .add_task_completed_event (id_ = 5 , result = "\" Hello Tokyo!\" " )
152
+
66
153
67
154
def test_initial_orchestration_state ():
68
155
context_builder = ContextBuilder ('test_simple_function' )
@@ -217,3 +304,119 @@ def test_failed_tokyo_hit_max_attempts():
217
304
218
305
expected_error_str = f"{ error_msg } { error_label } { state_str } "
219
306
assert expected_error_str == error_str
307
+
308
+ def test_concurrent_retriable_results ():
309
+ failed_reason = 'Reasons'
310
+ failed_details = 'Stuff and Things'
311
+ context_builder = ContextBuilder ('test_concurrent_retriable' )
312
+ add_hello_failed_events (context_builder , 0 , failed_reason , failed_details )
313
+ add_hello_failed_events (context_builder , 1 , failed_reason , failed_details )
314
+ add_hello_failed_events (context_builder , 2 , failed_reason , failed_details )
315
+ add_retry_timer_events (context_builder , 3 )
316
+ add_retry_timer_events (context_builder , 4 )
317
+ add_retry_timer_events (context_builder , 5 )
318
+ add_hello_completed_events (context_builder , 6 , "\" Hello Tokyo!\" " )
319
+ add_hello_completed_events (context_builder , 7 , "\" Hello Seattle!\" " )
320
+ add_hello_completed_events (context_builder , 8 , "\" Hello London!\" " )
321
+
322
+ result = get_orchestration_state_result (
323
+ context_builder , generator_function_concurrent_retries )
324
+
325
+ expected_state = base_expected_state ()
326
+ add_hello_action (expected_state , ['Tokyo' , 'Seattle' , 'London' ])
327
+ expected_state ._output = ["Hello Tokyo!" , "Hello Seattle!" , "Hello London!" ]
328
+ expected_state ._is_done = True
329
+ expected = expected_state .to_json ()
330
+
331
+ assert_valid_schema (result )
332
+ assert_orchestration_state_equals (expected , result )
333
+
334
+ def test_concurrent_retriable_results_unordered_arrival ():
335
+ failed_reason = 'Reasons'
336
+ failed_details = 'Stuff and Things'
337
+ context_builder = ContextBuilder ('test_concurrent_retriable_unordered_results' )
338
+ add_hello_failed_events (context_builder , 0 , failed_reason , failed_details )
339
+ add_hello_failed_events (context_builder , 1 , failed_reason , failed_details )
340
+ add_hello_failed_events (context_builder , 2 , failed_reason , failed_details )
341
+ add_retry_timer_events (context_builder , 3 )
342
+ add_retry_timer_events (context_builder , 4 )
343
+ add_retry_timer_events (context_builder , 5 )
344
+ # events arrive in non-sequential different order
345
+ add_hello_completed_events (context_builder , 8 , "\" Hello London!\" " )
346
+ add_hello_completed_events (context_builder , 6 , "\" Hello Tokyo!\" " )
347
+ add_hello_completed_events (context_builder , 7 , "\" Hello Seattle!\" " )
348
+
349
+ result = get_orchestration_state_result (
350
+ context_builder , generator_function_concurrent_retries )
351
+
352
+ expected_state = base_expected_state ()
353
+ add_hello_action (expected_state , ['Tokyo' , 'Seattle' , 'London' ])
354
+ expected_state ._output = ["Hello Tokyo!" , "Hello Seattle!" , "Hello London!" ]
355
+ expected_state ._is_done = True
356
+ expected = expected_state .to_json ()
357
+
358
+ assert_valid_schema (result )
359
+ assert_orchestration_state_equals (expected , result )
360
+
361
+ def test_concurrent_retriable_results_mixed_arrival ():
362
+ failed_reason = 'Reasons'
363
+ failed_details = 'Stuff and Things'
364
+ context_builder = ContextBuilder ('test_concurrent_retriable_unordered_results' )
365
+ # one task succeeds, the other two fail at first, and succeed on retry
366
+ add_hello_failed_events (context_builder , 1 , failed_reason , failed_details )
367
+ add_hello_completed_events (context_builder , 0 , "\" Hello Tokyo!\" " )
368
+ add_hello_failed_events (context_builder , 2 , failed_reason , failed_details )
369
+ add_retry_timer_events (context_builder , 3 )
370
+ add_retry_timer_events (context_builder , 4 )
371
+ add_hello_completed_events (context_builder , 6 , "\" Hello London!\" " )
372
+ add_hello_completed_events (context_builder , 5 , "\" Hello Seattle!\" " )
373
+
374
+ result = get_orchestration_state_result (
375
+ context_builder , generator_function_concurrent_retries )
376
+
377
+ expected_state = base_expected_state ()
378
+ add_hello_action (expected_state , ['Tokyo' , 'Seattle' , 'London' ])
379
+ expected_state ._output = ["Hello Tokyo!" , "Hello Seattle!" , "Hello London!" ]
380
+ expected_state ._is_done = True
381
+ expected = expected_state .to_json ()
382
+
383
+ assert_valid_schema (result )
384
+ assert_orchestration_state_equals (expected , result )
385
+
386
+ def test_concurrent_retriable_results_alternating_taskIDs_when_all ():
387
+ failed_reason = 'Reasons'
388
+ failed_details = 'Stuff and Things'
389
+ context_builder = ContextBuilder ('test_concurrent_retriable_unordered_results' )
390
+
391
+ add_two_retriable_events_completing_out_of_order (context_builder , failed_reason , failed_details )
392
+
393
+ result = get_orchestration_state_result (
394
+ context_builder , generator_function_two_concurrent_retries_when_all )
395
+
396
+ expected_state = base_expected_state ()
397
+ add_hello_action (expected_state , ['Tokyo' , 'Seattle' ])
398
+ expected_state ._output = ["Hello Tokyo!" , "Hello Seattle!" ]
399
+ expected_state ._is_done = True
400
+ expected = expected_state .to_json ()
401
+
402
+ assert_valid_schema (result )
403
+ assert_orchestration_state_equals (expected , result )
404
+
405
+ def test_concurrent_retriable_results_alternating_taskIDs_when_any ():
406
+ failed_reason = 'Reasons'
407
+ failed_details = 'Stuff and Things'
408
+ context_builder = ContextBuilder ('test_concurrent_retriable_unordered_results' )
409
+
410
+ add_two_retriable_events_completing_out_of_order (context_builder , failed_reason , failed_details )
411
+
412
+ result = get_orchestration_state_result (
413
+ context_builder , generator_function_two_concurrent_retries_when_any )
414
+
415
+ expected_state = base_expected_state ()
416
+ add_hello_action (expected_state , ['Tokyo' , 'Seattle' ])
417
+ expected_state ._output = "Hello Seattle!"
418
+ expected_state ._is_done = True
419
+ expected = expected_state .to_json ()
420
+
421
+ assert_valid_schema (result )
422
+ assert_orchestration_state_equals (expected , result )
0 commit comments