1+ from typing import List , Union
12from azure .durable_functions .models .ReplaySchema import ReplaySchema
23from .orchestrator_test_utils \
34 import get_orchestration_state_result , assert_orchestration_state_equals , assert_valid_schema
@@ -28,16 +29,63 @@ def generator_function(context):
2829
2930 return outputs
3031
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+
3173
3274def base_expected_state (output = None , replay_schema : ReplaySchema = ReplaySchema .V1 ) -> OrchestratorState :
3375 return OrchestratorState (is_done = False , actions = [], output = output , replay_schema = replay_schema .value )
3476
3577
36- def add_hello_action (state : OrchestratorState , input_ : str ):
78+ def add_hello_action (state : OrchestratorState , input_ : Union [ List [ str ], str ] ):
3779 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 )
4189
4290
4391def add_hello_failed_events (
@@ -63,6 +111,45 @@ def add_retry_timer_events(context_builder: ContextBuilder, id_: int):
63111 context_builder .add_orchestrator_started_event ()
64112 context_builder .add_timer_fired_event (id_ = id_ , fire_at = fire_at )
65113
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+
66153
67154def test_initial_orchestration_state ():
68155 context_builder = ContextBuilder ('test_simple_function' )
@@ -217,3 +304,119 @@ def test_failed_tokyo_hit_max_attempts():
217304
218305 expected_error_str = f"{ error_msg } { error_label } { state_str } "
219306 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