@@ -31,6 +31,10 @@ def wake(self) -> None:
3131 def approve (self ) -> None :
3232 self .approved = True
3333
34+ @workflow .update ("acknowledge" )
35+ def acknowledge (self ) -> str :
36+ return "acknowledged"
37+
3438 def run (self , ctx : Any ) -> Any :
3539 # Deliberately unsafe configuration drift keeps the class fingerprint
3640 # stable, exercising the recorded condition's own identity protection.
@@ -53,6 +57,9 @@ async def execute_one() -> None:
5357 max_concurrent_workflow_tasks = 1 ,
5458 )
5559 await worker ._register ()
60+ if os .environ .get ("CONDITION_RECOVERY_REGISTER_ONLY" ) == "1" :
61+ print (json .dumps ({"registered" : True }), flush = True )
62+ return
5663 expect_empty = os .environ .get ("CONDITION_RECOVERY_EXPECT_EMPTY" ) == "1"
5764 task = await client .poll_workflow_task (
5865 worker_id = worker .worker_id , task_queue = worker .task_queue , timeout = 1 if expect_empty else 20 ,
@@ -66,6 +73,7 @@ async def execute_one() -> None:
6673 print (json .dumps ({
6774 "task_id" : task ["task_id" ], "run_id" : task ["run_id" ],
6875 "attempt" : task ["workflow_task_attempt" ], "commands" : commands ,
76+ "update_id" : task .get ("workflow_update_id" ),
6977 "fingerprint" : worker .workflow_definition_fingerprints ,
7078 "sdk_version" : importlib .metadata .version ("durable-workflow" ),
7179 "replay_source_sha256" : hashlib .sha256 (
@@ -76,13 +84,14 @@ async def execute_one() -> None:
7684
7785async def cold_worker (
7886 server_url : str , token : str , queue : str , mode : str , * , expect_empty : bool = False ,
79- python_executable : str | None = None ,
87+ python_executable : str | None = None , register_only : bool = False ,
8088) -> dict [str , Any ]:
8189 environment = {
8290 ** os .environ , "DURABLE_WORKFLOW_SERVER_URL" : server_url ,
8391 "DURABLE_WORKFLOW_AUTH_TOKEN" : token , "CONDITION_RECOVERY_QUEUE" : queue ,
8492 "CONDITION_RECOVERY_MODE" : mode ,
8593 "CONDITION_RECOVERY_EXPECT_EMPTY" : "1" if expect_empty else "0" ,
94+ "CONDITION_RECOVERY_REGISTER_ONLY" : "1" if register_only else "0" ,
8695 "CONDITION_RECOVERY_WORKER" : f"condition-worker-{ uuid .uuid4 ().hex } " ,
8796 }
8897 process = await asyncio .create_subprocess_exec (
@@ -108,12 +117,17 @@ def history_events(document: dict[str, Any]) -> list[dict[str, Any]]:
108117
109118@pytest .mark .asyncio
110119@pytest .mark .parametrize ("drift" , ["key" , "fingerprint" ])
120+ @pytest .mark .parametrize ("consumer" , ["workflow" , "update" ])
111121async def test_bad_deployment_then_cold_replacement_resumes_original_run (
112- server_url : str , server_token : str , drift : str ,
122+ server_url : str , server_token : str , drift : str , consumer : str ,
113123) -> None :
114124 suffix = uuid .uuid4 ().hex
115125 queue = f"condition-recovery-{ suffix } "
116126 async with Client (server_url , token = server_token , namespace = "default" ) as client :
127+ assert await cold_worker (
128+ server_url , server_token , queue , "correct" , register_only = True ,
129+ python_executable = os .environ .get ("CONDITION_RECOVERY_INITIAL_PYTHON" ),
130+ ) == {"registered" : True }
117131 handle = await client .start_workflow (
118132 workflow_type = "tests.condition-deployment-recovery" , task_queue = queue ,
119133 workflow_id = f"condition-recovery-{ suffix } " , input = [],
@@ -124,7 +138,10 @@ async def test_bad_deployment_then_cold_replacement_resumes_original_run(
124138 )
125139 assert [command ["type" ] for command in initial ["commands" ]] == ["open_condition_wait" ]
126140 original_history = history_events (await handle .get_history ())
127- await handle .signal ("wake" )
141+ if consumer == "update" :
142+ await handle .update ("acknowledge" , wait_for = "accepted" , request_id = f"ack-{ suffix } " )
143+ else :
144+ await handle .signal ("wake" )
128145
129146 rejected = await cold_worker (server_url , server_token , queue , drift )
130147 assert rejected ["commands" ] is None
@@ -134,13 +151,18 @@ async def test_bad_deployment_then_cold_replacement_resumes_original_run(
134151 after_rejection = history_events (await handle .get_history ())
135152 assert after_rejection [:len (original_history )] == original_history
136153 assert all (event ["event_type" ] != "WorkflowFailed" for event in after_rejection )
154+ assert all (event ["event_type" ] != "UpdateFailed" for event in after_rejection )
137155
138156 assert await cold_worker (server_url , server_token , queue , "correct" , expect_empty = True ) == {"empty" : True }
139157 await handle .repair ()
140158 resumed = await cold_worker (server_url , server_token , queue , "correct" )
141159 assert resumed ["run_id" ] == handle .run_id
142160 assert resumed ["fingerprint" ] == initial ["fingerprint" ]
143- assert [command ["type" ] for command in resumed ["commands" ]] == ["open_condition_wait" ]
161+ expected_command = "complete_update" if consumer == "update" else "open_condition_wait"
162+ assert [command ["type" ] for command in resumed ["commands" ]] == [expected_command ]
163+ if consumer == "update" :
164+ assert resumed ["update_id" ] == rejected ["update_id" ]
165+ assert resumed ["update_id" ] is not None
144166 assert resumed ["attempt" ] > rejected ["attempt" ]
145167 await handle .signal ("approve" )
146168 finished = await cold_worker (server_url , server_token , queue , "correct" )
@@ -151,8 +173,11 @@ async def test_bad_deployment_then_cold_replacement_resumes_original_run(
151173 kinds = [event ["event_type" ] for event in final_history ]
152174 assert "WorkflowFailed" not in kinds
153175 assert kinds .count ("WorkflowCompleted" ) == 1
176+ if consumer == "update" :
177+ assert kinds .count ("UpdateCompleted" ) == 1
178+ assert "UpdateFailed" not in kinds
154179 print (json .dumps ({
155- "drift" : drift , "initial" : initial , "rejected" : rejected ,
180+ "drift" : drift , "consumer" : consumer , " initial" : initial , "rejected" : rejected ,
156181 "resumed" : resumed , "finished" : finished ,
157182 "preserved_prefix_events" : len (original_history ),
158183 "workflow_failed_events" : 0 , "workflow_completed_events" : 1 ,
0 commit comments