From e1900cd25af155fa33b8a5faa22e0b1407c60581 Mon Sep 17 00:00:00 2001 From: Chris <64613756+wuchris-ch@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:50:37 -0700 Subject: [PATCH 1/2] Allow replacing a worker's client when both use the default runtime Fixes #657. --- changelog/fixed/relay-racing-raccoon.md | 1 + temporalio/service.py | 3 +++ temporalio/worker/_worker.py | 6 +++-- tests/worker/test_workflow.py | 34 +++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 changelog/fixed/relay-racing-raccoon.md diff --git a/changelog/fixed/relay-racing-raccoon.md b/changelog/fixed/relay-racing-raccoon.md new file mode 100644 index 000000000..d2d84df6b --- /dev/null +++ b/changelog/fixed/relay-racing-raccoon.md @@ -0,0 +1 @@ +Setting `Worker.client` no longer raises "New client is not on the same runtime as the existing client" when both clients connected without a `runtime`, since both use the default runtime. The check now compares the runtime each client actually connected with. diff --git a/temporalio/service.py b/temporalio/service.py index 926dfbd46..5c390f92a 100644 --- a/temporalio/service.py +++ b/temporalio/service.py @@ -418,6 +418,8 @@ def __init__(self, config: ConnectConfig) -> None: self._bridge_config = config._to_bridge_config() self._bridge_client: temporalio.bridge.client.Client | None = None self._bridge_client_connect_lock = asyncio.Lock() + # Runtime we connected with, so the default one if none was set + self._runtime: temporalio.runtime.Runtime | None = None async def _connected_client(self) -> temporalio.bridge.client.Client: # Fast path avoids touching the lock once connected. This keeps the @@ -433,6 +435,7 @@ async def _connected_client(self) -> temporalio.bridge.client.Client: runtime._core_runtime, self._bridge_config, ) + self._runtime = runtime return self._bridge_client @property diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index 39faf615a..3c6effd80 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -491,7 +491,9 @@ def _init_from_config(self, client: temporalio.client.Client, config: WorkerConf self._activity_worker: _ActivityWorker | None = None self._runtime = ( - bridge_client.config.runtime or temporalio.runtime.Runtime.default() + bridge_client._runtime + or bridge_client.config.runtime + or temporalio.runtime.Runtime.default() ) activities = config.get("activities") if activities: @@ -743,7 +745,7 @@ def client(self, value: temporalio.client.Client) -> None: must be using the same runtime as the current client. """ bridge_client = _extract_bridge_client_for_worker(value) - if self._runtime is not bridge_client.config.runtime: + if self._runtime is not bridge_client._runtime: raise ValueError( "New client is not on the same runtime as the existing client" ) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index 80ac3d27b..d89b1d902 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -6669,6 +6669,40 @@ async def test_workflow_replace_worker_client_diff_runtimes_fail( worker.client = other_client +async def test_workflow_replace_worker_client_implicit_default_runtime( + env: WorkflowEnvironment, +): + # Neither client sets a runtime, so both are on the default one + worker_client = await env.connect_client(runtime=None) + new_client = await env.connect_client(runtime=None) + async with new_worker(worker_client, HelloWorkflow) as worker: + worker.client = new_client + assert worker.client is new_client + result = await new_client.execute_workflow( + HelloWorkflow.run, + "Temporal", + id=f"workflow-{uuid.uuid4()}", + task_queue=worker.task_queue, + ) + assert result == "Hello, Temporal!" + + +async def test_workflow_replace_worker_client_implicit_default_runtime_mismatch_fail( + env: WorkflowEnvironment, +): + # No runtime means the default one, which isn't the worker's + worker_client = await env.connect_client( + runtime=Runtime(telemetry=TelemetryConfig()) + ) + default_runtime_client = await env.connect_client(runtime=None) + async with new_worker(worker_client, HelloWorkflow) as worker: + with pytest.raises( + ValueError, + match="New client is not on the same runtime as the existing client", + ): + worker.client = default_runtime_client + + @activity.defn(dynamic=True) async def return_name_activity(_args: Sequence[RawValue]) -> str: return activity.info().activity_type From e4baa5abd9098d1396d4a30d236d993e6c6bc1ec Mon Sep 17 00:00:00 2001 From: Chris <64613756+wuchris-ch@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:26:14 -0700 Subject: [PATCH 2/2] Drop the workflow run from the default runtime test --- tests/worker/test_workflow.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/worker/test_workflow.py b/tests/worker/test_workflow.py index d89b1d902..01a2fcea8 100644 --- a/tests/worker/test_workflow.py +++ b/tests/worker/test_workflow.py @@ -6678,13 +6678,6 @@ async def test_workflow_replace_worker_client_implicit_default_runtime( async with new_worker(worker_client, HelloWorkflow) as worker: worker.client = new_client assert worker.client is new_client - result = await new_client.execute_workflow( - HelloWorkflow.run, - "Temporal", - id=f"workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert result == "Hello, Temporal!" async def test_workflow_replace_worker_client_implicit_default_runtime_mismatch_fail(