From 8653a501072d000140b3118ec45e3b7754186344 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Thu, 5 Nov 2026 22:49:16 +0530 Subject: [PATCH 1/4] fix(client): pass resolved instance ID and version to schedule request builder Signed-off-by: Hardik Kaurani --- durabletask/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/durabletask/client.py b/durabletask/client.py index d5269463..d0d6c022 100644 --- a/durabletask/client.py +++ b/durabletask/client.py @@ -644,9 +644,9 @@ def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInput, TOu name, resolved_instance_id, version=resolved_version, ): req = build_schedule_new_orchestration_req( - orchestrator, input=input, instance_id=instance_id, start_at=start_at, + orchestrator, input=input, instance_id=resolved_instance_id, start_at=start_at, reuse_id_policy=reuse_id_policy, tags=tags, - version=version if version else self.default_version, + version=resolved_version, data_converter=self._data_converter) # Inject the active PRODUCER span context, or the ambient context @@ -1192,9 +1192,9 @@ async def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInpu name, resolved_instance_id, version=resolved_version, ): req = build_schedule_new_orchestration_req( - orchestrator, input=input, instance_id=instance_id, start_at=start_at, + orchestrator, input=input, instance_id=resolved_instance_id, start_at=start_at, reuse_id_policy=reuse_id_policy, tags=tags, - version=version if version else self.default_version, + version=resolved_version, data_converter=self._data_converter) parent_trace_ctx = tracing.get_current_trace_context() From 25ca468bb8ed5fdf6f4a289cda3c43e625e44abe Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Thu, 5 Nov 2026 22:49:53 +0530 Subject: [PATCH 2/4] test(tracing): verify sync schedule_new_orchestration matches OTel span instance ID Signed-off-by: Hardik Kaurani --- tests/durabletask/test_tracing.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/durabletask/test_tracing.py b/tests/durabletask/test_tracing.py index dfdeae25..e431d20c 100644 --- a/tests/durabletask/test_tracing.py +++ b/tests/durabletask/test_tracing.py @@ -591,6 +591,28 @@ def test_sync_client_propagates_ambient_context_without_lifecycle_spans( assert [span.name for span in otel_setup.get_finished_spans()] == [ "host-request"] + def test_sync_client_schedule_new_orchestration_matches_span_instance_id_when_auto_generated( + self, otel_setup: InMemorySpanExporter): + """When instance_id is omitted, request.instanceId must match the producer span instance ID.""" + stub = MagicMock() + stub.StartInstance.side_effect = lambda req: pb.CreateInstanceResponse(instanceId=req.instanceId) + with patch( + "durabletask.client.stubs.TaskHubSidecarServiceStub", + return_value=stub): + grpc_client = client.TaskHubGrpcClient(channel=MagicMock(), emit_trace_spans=True) + + returned_instance_id = grpc_client.schedule_new_orchestration("MyOrchestrator") + + start_request = stub.StartInstance.call_args.args[0] + assert start_request.instanceId == returned_instance_id + assert start_request.instanceId != "" + + spans = otel_setup.get_finished_spans() + create_spans = [s for s in spans if s.name == "create_orchestration:MyOrchestrator"] + assert len(create_spans) == 1 + assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == returned_instance_id + assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == start_request.instanceId + @pytest.mark.asyncio async def test_async_client_propagates_ambient_context_without_lifecycle_spans( self, otel_setup: InMemorySpanExporter): From 622b1a4393860e43a4a476bd78de80e3383aaa46 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Thu, 5 Nov 2026 22:50:50 +0530 Subject: [PATCH 3/4] test(tracing): verify async schedule_new_orchestration matches OTel span instance ID Signed-off-by: Hardik Kaurani --- tests/durabletask/test_tracing.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/durabletask/test_tracing.py b/tests/durabletask/test_tracing.py index e431d20c..441727aa 100644 --- a/tests/durabletask/test_tracing.py +++ b/tests/durabletask/test_tracing.py @@ -638,6 +638,29 @@ async def test_async_client_propagates_ambient_context_without_lifecycle_spans( assert [span.name for span in otel_setup.get_finished_spans()] == [ "host-request"] + @pytest.mark.asyncio + async def test_async_client_schedule_new_orchestration_matches_span_instance_id_when_auto_generated( + self, otel_setup: InMemorySpanExporter): + """When instance_id is omitted, async request.instanceId must match the producer span instance ID.""" + stub = MagicMock() + stub.StartInstance = AsyncMock(side_effect=lambda req: pb.CreateInstanceResponse(instanceId=req.instanceId)) + with patch( + "durabletask.client.stubs.TaskHubSidecarServiceStub", + return_value=stub): + grpc_client = client.AsyncTaskHubGrpcClient(channel=MagicMock(), emit_trace_spans=True) + + returned_instance_id = await grpc_client.schedule_new_orchestration("MyAsyncOrchestrator") + + start_request = stub.StartInstance.call_args.args[0] + assert start_request.instanceId == returned_instance_id + assert start_request.instanceId != "" + + spans = otel_setup.get_finished_spans() + create_spans = [s for s in spans if s.name == "create_orchestration:MyAsyncOrchestrator"] + assert len(create_spans) == 1 + assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == returned_instance_id + assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == start_request.instanceId + class TestOrchestrationServerSpan: """Tests for emit_orchestration_span.""" From 8333dcc45800013d5a4f4118845bf862bc9aa92a Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Thu, 5 Nov 2026 22:51:31 +0530 Subject: [PATCH 4/4] docs(client): document instance ID and version resolution propagation in schedule_new_orchestration Signed-off-by: Hardik Kaurani --- durabletask/client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/durabletask/client.py b/durabletask/client.py index d0d6c022..64d9525b 100644 --- a/durabletask/client.py +++ b/durabletask/client.py @@ -634,7 +634,11 @@ def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInput, TOu reuse_id_policy: pb.OrchestrationIdReusePolicy | None = None, tags: dict[str, str] | None = None, version: str | None = None) -> str: + """Schedule a new orchestration instance for execution. + If instance_id is not specified, a random UUID hex string is generated upfront + and shared across both the OpenTelemetry producer span and the CreateInstanceRequest. + """ name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator) resolved_instance_id = instance_id if instance_id else uuid.uuid4().hex resolved_version = version if version else self.default_version @@ -1182,7 +1186,11 @@ async def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInpu reuse_id_policy: pb.OrchestrationIdReusePolicy | None = None, tags: dict[str, str] | None = None, version: str | None = None) -> str: + """Schedule a new orchestration instance for execution asynchronously. + If instance_id is not specified, a random UUID hex string is generated upfront + and shared across both the OpenTelemetry producer span and the CreateInstanceRequest. + """ name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator) resolved_instance_id = instance_id if instance_id else uuid.uuid4().hex resolved_version = version if version else self.default_version