Skip to content

Commit 0effc8f

Browse files
Verify accepted updates remain recoverable through published HTTP
1 parent f72a51a commit 0effc8f

1 file changed

Lines changed: 30 additions & 5 deletions

File tree

‎tests/integration/test_condition_recovery.py‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

7785
async 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"])
111121
async 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

Comments
 (0)