Skip to content

Commit c44a6d8

Browse files
authored
Update bug fix: prevent update from a stale workflow handle (#703)
* Add test that update respects first_execution_run_id * Send first_execution_run_id with Update requests Fixes #682
1 parent 341d949 commit c44a6d8

File tree

2 files changed

+54
-10
lines changed

2 files changed

+54
-10
lines changed

temporalio/client.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,11 +1882,6 @@ async def execute_update(
18821882
.. warning::
18831883
This API is experimental
18841884
1885-
.. warning::
1886-
WorkflowHandles created as a result of :py:meth:`Client.start_workflow` will
1887-
send updates to the latest workflow with the same workflow ID even if it is
1888-
unrelated to the started workflow.
1889-
18901885
Args:
18911886
update: Update function or name on the workflow.
18921887
arg: Single argument to the update.
@@ -1994,11 +1989,6 @@ async def start_update(
19941989
.. warning::
19951990
This API is experimental
19961991
1997-
.. warning::
1998-
WorkflowHandles created as a result of :py:meth:`Client.start_workflow` will
1999-
send updates to the latest workflow with the same workflow ID even if it is
2000-
unrelated to the started workflow.
2001-
20021992
Args:
20031993
update: Update function or name on the workflow.
20041994
arg: Single argument to the update.
@@ -2060,6 +2050,7 @@ async def _start_update(
20602050
StartWorkflowUpdateInput(
20612051
id=self._id,
20622052
run_id=self._run_id,
2053+
first_execution_run_id=self.first_execution_run_id,
20632054
update_id=id,
20642055
update=update_name,
20652056
args=temporalio.common._arg_or_args(arg, args),
@@ -4728,6 +4719,7 @@ class StartWorkflowUpdateInput:
47284719

47294720
id: str
47304721
run_id: Optional[str]
4722+
first_execution_run_id: Optional[str]
47314723
update_id: Optional[str]
47324724
update: str
47334725
args: Sequence[Any]
@@ -5360,6 +5352,7 @@ async def start_workflow_update(
53605352
workflow_id=input.id,
53615353
run_id=input.run_id or "",
53625354
),
5355+
first_execution_run_id=input.first_execution_run_id or "",
53635356
request=temporalio.api.update.v1.Request(
53645357
meta=temporalio.api.update.v1.Meta(
53655358
update_id=input.update_id or str(uuid.uuid4()),

tests/worker/test_workflow.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4425,6 +4425,57 @@ async def test_workflow_update_task_fails(client: Client, env: WorkflowEnvironme
44254425
assert bad_validator_fail_ct == 2
44264426

44274427

4428+
@workflow.defn
4429+
class UpdateRespectsFirstExecutionRunIdWorkflow:
4430+
def __init__(self) -> None:
4431+
self.update_received = False
4432+
4433+
@workflow.run
4434+
async def run(self) -> None:
4435+
await workflow.wait_condition(lambda: self.update_received)
4436+
4437+
@workflow.update
4438+
async def update(self) -> None:
4439+
self.update_received = True
4440+
4441+
4442+
async def test_workflow_update_respects_first_execution_run_id(
4443+
client: Client, env: WorkflowEnvironment
4444+
):
4445+
if env.supports_time_skipping:
4446+
pytest.skip(
4447+
"Java test server: https://github.com/temporalio/sdk-java/issues/1903"
4448+
)
4449+
# Start one workflow, obtain the run ID (r1), and let it complete. Start a second
4450+
# workflow with the same workflow ID, and try to send an update using the handle from
4451+
# r1.
4452+
workflow_id = f"update-respects-first-execution-run-id-{uuid.uuid4()}"
4453+
async with new_worker(client, UpdateRespectsFirstExecutionRunIdWorkflow) as worker:
4454+
4455+
async def start_workflow(workflow_id: str) -> WorkflowHandle:
4456+
return await client.start_workflow(
4457+
UpdateRespectsFirstExecutionRunIdWorkflow.run,
4458+
id=workflow_id,
4459+
task_queue=worker.task_queue,
4460+
)
4461+
4462+
wf_execution_1_handle = await start_workflow(workflow_id)
4463+
await wf_execution_1_handle.execute_update(
4464+
UpdateRespectsFirstExecutionRunIdWorkflow.update
4465+
)
4466+
await wf_execution_1_handle.result()
4467+
await start_workflow(workflow_id)
4468+
4469+
# Execution 1 has closed. This would succeed if the update incorrectly targets
4470+
# the second execution
4471+
with pytest.raises(RPCError) as exc_info:
4472+
await wf_execution_1_handle.execute_update(
4473+
UpdateRespectsFirstExecutionRunIdWorkflow.update
4474+
)
4475+
assert exc_info.value.status == RPCStatusCode.NOT_FOUND
4476+
assert "workflow execution not found" in str(exc_info.value)
4477+
4478+
44284479
@workflow.defn
44294480
class ImmediatelyCompleteUpdateAndWorkflow:
44304481
def __init__(self) -> None:

0 commit comments

Comments
 (0)