@@ -1857,6 +1857,158 @@ def orchestrator(ctx: task.OrchestrationContext, input: int):
18571857 assert event .eventRaised .input .value == json .dumps (42 + i )
18581858
18591859
1860+ @pytest .mark .parametrize ("save_events" , [True , False ])
1861+ @pytest .mark .parametrize ("replayed_events" , [0 , 1 , 3 ])
1862+ @pytest .mark .parametrize ("timer_fires_first" , [True , False ])
1863+ def test_continue_as_new_preserves_global_event_order (
1864+ save_events : bool , replayed_events : int , timer_fires_first : bool ):
1865+ def orchestrator (ctx : task .OrchestrationContext , _ ):
1866+ yield ctx .create_timer (ctx .current_utc_datetime + timedelta (seconds = 1 ))
1867+ ctx .continue_as_new (None , save_events = save_events )
1868+
1869+ registry = worker ._Registry ()
1870+ name = registry .add_orchestrator (orchestrator )
1871+ start_time = datetime (2026 , 1 , 1 )
1872+ fire_at = start_time + timedelta (seconds = 1 )
1873+ raised_events = [
1874+ helpers .new_event_raised_event ("A" , "1" ),
1875+ helpers .new_event_raised_event ("b" , "2" ),
1876+ helpers .new_event_raised_event ("a" , "3" ),
1877+ ]
1878+ old_events = [
1879+ helpers .new_orchestrator_started_event (start_time ),
1880+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID ),
1881+ helpers .new_timer_created_event (1 , fire_at ),
1882+ * raised_events [:replayed_events ],
1883+ ]
1884+ timer_fired = helpers .new_timer_fired_event (1 , fire_at )
1885+ new_events = raised_events [replayed_events :]
1886+ new_events .insert (0 if timer_fires_first else len (new_events ), timer_fired )
1887+
1888+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , JsonDataConverter ())
1889+ result = executor .execute (TEST_INSTANCE_ID , old_events , new_events )
1890+
1891+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result .actions )
1892+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_CONTINUED_AS_NEW
1893+ assert result .actions [0 ].id == 2
1894+ expected = [("a" , "1" ), ("b" , "2" ), ("a" , "3" )] if save_events else []
1895+ assert [
1896+ (event .eventRaised .name , event .eventRaised .input .value )
1897+ for event in complete_action .carryoverEvents
1898+ ] == expected
1899+
1900+
1901+ @pytest .mark .parametrize ("replay_consumption" , [True , False ])
1902+ def test_continue_as_new_preserves_order_after_selective_event_consumption (
1903+ replay_consumption : bool ):
1904+ def orchestrator (ctx : task .OrchestrationContext , _ ):
1905+ live_result = yield ctx .wait_for_external_event ("LIVE" )
1906+ cancelled_wait = ctx .wait_for_external_event ("C" )
1907+ cancelled_wait .cancel ()
1908+ yield ctx .create_timer (ctx .current_utc_datetime + timedelta (seconds = 1 ))
1909+ first = yield ctx .wait_for_external_event ("a" , data_type = int )
1910+ second = yield ctx .wait_for_external_event ("A" , data_type = int )
1911+ third = yield ctx .wait_for_external_event ("c" , data_type = int )
1912+ yield ctx .create_timer (ctx .current_utc_datetime + timedelta (seconds = 2 ))
1913+ ctx .continue_as_new ([live_result , first , second , third ], save_events = True )
1914+
1915+ registry = worker ._Registry ()
1916+ name = registry .add_orchestrator (orchestrator )
1917+ start_time = datetime (2026 , 1 , 1 )
1918+ first_fire_at = start_time + timedelta (seconds = 1 )
1919+ second_fire_at = start_time + timedelta (seconds = 2 )
1920+ history = [
1921+ helpers .new_orchestrator_started_event (start_time ),
1922+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID ),
1923+ helpers .new_event_raised_event ("live" , "0" ),
1924+ helpers .new_timer_created_event (1 , first_fire_at ),
1925+ helpers .new_event_raised_event ("A" , "1" ),
1926+ helpers .new_event_raised_event ("b" , "2" ),
1927+ helpers .new_event_raised_event ("a" , "3" ),
1928+ helpers .new_event_raised_event ("C" , "4" ),
1929+ helpers .new_event_raised_event ("B" , "5" ),
1930+ helpers .new_event_raised_event ("A" , "6" ),
1931+ helpers .new_timer_fired_event (1 , first_fire_at ),
1932+ helpers .new_timer_created_event (2 , second_fire_at ),
1933+ ]
1934+ trailing_events = [
1935+ helpers .new_event_raised_event ("c" , "7" ),
1936+ helpers .new_event_raised_event ("a" , "8" ),
1937+ helpers .new_event_raised_event ("b" , "9" ),
1938+ helpers .new_timer_fired_event (2 , second_fire_at ),
1939+ ]
1940+ old_events = history if replay_consumption else []
1941+ new_events = trailing_events if replay_consumption else history + trailing_events
1942+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , JsonDataConverter ())
1943+ result = executor .execute (TEST_INSTANCE_ID , old_events , new_events )
1944+
1945+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result .actions )
1946+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_CONTINUED_AS_NEW
1947+ assert result .actions [0 ].id == 3
1948+ assert json .loads (complete_action .result .value ) == [0 , 1 , 3 , 4 ]
1949+ assert [
1950+ (event .eventRaised .name , event .eventRaised .input .value )
1951+ for event in complete_action .carryoverEvents
1952+ ] == [("b" , "2" ), ("b" , "5" ), ("a" , "6" ), ("c" , "7" ), ("a" , "8" ), ("b" , "9" )]
1953+
1954+
1955+ @pytest .mark .parametrize ("consumed_payload" , [None , "null" , "23" ])
1956+ def test_continue_as_new_preserves_order_and_raw_event_payloads (consumed_payload : str | None ):
1957+ class RecordingConverter (JsonDataConverter ):
1958+ def __init__ (self ):
1959+ self .serialized : list [Any ] = []
1960+ self .deserialized : list [tuple [str | None , type | None ]] = []
1961+
1962+ def serialize (self , value : Any ) -> str | None :
1963+ self .serialized .append (value )
1964+ return None if value is None else json .dumps ({"wrapped" : value })
1965+
1966+ def deserialize (self , data : str | None , target_type : type | None = None ) -> Any :
1967+ self .deserialized .append ((data , target_type ))
1968+ return super ().deserialize (data , target_type )
1969+
1970+ def orchestrator (ctx : task .OrchestrationContext , _ ):
1971+ yield ctx .create_timer (ctx .current_utc_datetime + timedelta (seconds = 1 ))
1972+ consumed = yield ctx .wait_for_external_event ("Consume" , data_type = int )
1973+ ctx .continue_as_new (consumed , save_events = True )
1974+
1975+ registry = worker ._Registry ()
1976+ name = registry .add_orchestrator (orchestrator )
1977+ start_time = datetime (2026 , 1 , 1 )
1978+ fire_at = start_time + timedelta (seconds = 1 )
1979+ expected = [
1980+ ("a" , '{ "value" : [1, 2] }' ),
1981+ ("b" , None ),
1982+ ("a" , "null" ),
1983+ ("b" , '"hello\\ u0020world"' ),
1984+ ("a" , '{ "value" : [1, 2] }' ),
1985+ ("c" , "false" ),
1986+ ("b" , "0" ),
1987+ ("a" , '""' ),
1988+ ]
1989+ old_events = [
1990+ helpers .new_orchestrator_started_event (start_time ),
1991+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID ),
1992+ helpers .new_timer_created_event (1 , fire_at ),
1993+ helpers .new_event_raised_event ("consume" , consumed_payload ),
1994+ * [helpers .new_event_raised_event (name , payload ) for name , payload in expected ],
1995+ ]
1996+ converter = RecordingConverter ()
1997+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , converter )
1998+ result = executor .execute (
1999+ TEST_INSTANCE_ID , old_events , [helpers .new_timer_fired_event (1 , fire_at )])
2000+
2001+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result .actions )
2002+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_CONTINUED_AS_NEW
2003+ assert [
2004+ (event .eventRaised .name ,
2005+ event .eventRaised .input .value if event .eventRaised .HasField ("input" ) else None )
2006+ for event in complete_action .carryoverEvents
2007+ ] == expected
2008+ assert converter .deserialized == [(consumed_payload , int )]
2009+ assert converter .serialized == [json .loads (consumed_payload ) if consumed_payload else None ]
2010+
2011+
18602012def test_fan_out ():
18612013 """Tests that a fan-out pattern correctly schedules N tasks"""
18622014 def hello (_ , name : str ):
0 commit comments