Skip to content

Commit 17ec055

Browse files
fix(client): propagate resolved instance ID and version to schedule request builder (#345)
* fix(client): pass resolved instance ID and version to schedule request builder * test(tracing): verify sync schedule_new_orchestration matches OTel span instance ID * test(tracing): verify async schedule_new_orchestration matches OTel span instance ID * docs(client): document instance ID and version resolution propagation in schedule_new_orchestration --------- Signed-off-by: Hardik Kaurani <hardikkaurani1@gmail.com>
1 parent 6c0865f commit 17ec055

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

‎durabletask/client.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,11 @@ def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInput, TOu
634634
reuse_id_policy: pb.OrchestrationIdReusePolicy | None = None,
635635
tags: dict[str, str] | None = None,
636636
version: str | None = None) -> str:
637+
"""Schedule a new orchestration instance for execution.
637638
639+
If instance_id is not specified, a random UUID hex string is generated upfront
640+
and shared across both the OpenTelemetry producer span and the CreateInstanceRequest.
641+
"""
638642
name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator)
639643
resolved_instance_id = instance_id if instance_id else uuid.uuid4().hex
640644
resolved_version = version if version else self.default_version
@@ -644,9 +648,9 @@ def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInput, TOu
644648
name, resolved_instance_id, version=resolved_version,
645649
):
646650
req = build_schedule_new_orchestration_req(
647-
orchestrator, input=input, instance_id=instance_id, start_at=start_at,
651+
orchestrator, input=input, instance_id=resolved_instance_id, start_at=start_at,
648652
reuse_id_policy=reuse_id_policy, tags=tags,
649-
version=version if version else self.default_version,
653+
version=resolved_version,
650654
data_converter=self._data_converter)
651655

652656
# Inject the active PRODUCER span context, or the ambient context
@@ -1182,7 +1186,11 @@ async def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInpu
11821186
reuse_id_policy: pb.OrchestrationIdReusePolicy | None = None,
11831187
tags: dict[str, str] | None = None,
11841188
version: str | None = None) -> str:
1189+
"""Schedule a new orchestration instance for execution asynchronously.
11851190
1191+
If instance_id is not specified, a random UUID hex string is generated upfront
1192+
and shared across both the OpenTelemetry producer span and the CreateInstanceRequest.
1193+
"""
11861194
name = orchestrator if isinstance(orchestrator, str) else task.get_name(orchestrator)
11871195
resolved_instance_id = instance_id if instance_id else uuid.uuid4().hex
11881196
resolved_version = version if version else self.default_version
@@ -1192,9 +1200,9 @@ async def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInpu
11921200
name, resolved_instance_id, version=resolved_version,
11931201
):
11941202
req = build_schedule_new_orchestration_req(
1195-
orchestrator, input=input, instance_id=instance_id, start_at=start_at,
1203+
orchestrator, input=input, instance_id=resolved_instance_id, start_at=start_at,
11961204
reuse_id_policy=reuse_id_policy, tags=tags,
1197-
version=version if version else self.default_version,
1205+
version=resolved_version,
11981206
data_converter=self._data_converter)
11991207

12001208
parent_trace_ctx = tracing.get_current_trace_context()

‎tests/durabletask/test_tracing.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,28 @@ def test_sync_client_propagates_ambient_context_without_lifecycle_spans(
591591
assert [span.name for span in otel_setup.get_finished_spans()] == [
592592
"host-request"]
593593

594+
def test_sync_client_schedule_new_orchestration_matches_span_instance_id_when_auto_generated(
595+
self, otel_setup: InMemorySpanExporter):
596+
"""When instance_id is omitted, request.instanceId must match the producer span instance ID."""
597+
stub = MagicMock()
598+
stub.StartInstance.side_effect = lambda req: pb.CreateInstanceResponse(instanceId=req.instanceId)
599+
with patch(
600+
"durabletask.client.stubs.TaskHubSidecarServiceStub",
601+
return_value=stub):
602+
grpc_client = client.TaskHubGrpcClient(channel=MagicMock(), emit_trace_spans=True)
603+
604+
returned_instance_id = grpc_client.schedule_new_orchestration("MyOrchestrator")
605+
606+
start_request = stub.StartInstance.call_args.args[0]
607+
assert start_request.instanceId == returned_instance_id
608+
assert start_request.instanceId != ""
609+
610+
spans = otel_setup.get_finished_spans()
611+
create_spans = [s for s in spans if s.name == "create_orchestration:MyOrchestrator"]
612+
assert len(create_spans) == 1
613+
assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == returned_instance_id
614+
assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == start_request.instanceId
615+
594616
@pytest.mark.asyncio
595617
async def test_async_client_propagates_ambient_context_without_lifecycle_spans(
596618
self, otel_setup: InMemorySpanExporter):
@@ -616,6 +638,29 @@ async def test_async_client_propagates_ambient_context_without_lifecycle_spans(
616638
assert [span.name for span in otel_setup.get_finished_spans()] == [
617639
"host-request"]
618640

641+
@pytest.mark.asyncio
642+
async def test_async_client_schedule_new_orchestration_matches_span_instance_id_when_auto_generated(
643+
self, otel_setup: InMemorySpanExporter):
644+
"""When instance_id is omitted, async request.instanceId must match the producer span instance ID."""
645+
stub = MagicMock()
646+
stub.StartInstance = AsyncMock(side_effect=lambda req: pb.CreateInstanceResponse(instanceId=req.instanceId))
647+
with patch(
648+
"durabletask.client.stubs.TaskHubSidecarServiceStub",
649+
return_value=stub):
650+
grpc_client = client.AsyncTaskHubGrpcClient(channel=MagicMock(), emit_trace_spans=True)
651+
652+
returned_instance_id = await grpc_client.schedule_new_orchestration("MyAsyncOrchestrator")
653+
654+
start_request = stub.StartInstance.call_args.args[0]
655+
assert start_request.instanceId == returned_instance_id
656+
assert start_request.instanceId != ""
657+
658+
spans = otel_setup.get_finished_spans()
659+
create_spans = [s for s in spans if s.name == "create_orchestration:MyAsyncOrchestrator"]
660+
assert len(create_spans) == 1
661+
assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == returned_instance_id
662+
assert create_spans[0].attributes[tracing.ATTR_TASK_INSTANCE_ID] == start_request.instanceId
663+
619664

620665
class TestOrchestrationServerSpan:
621666
"""Tests for emit_orchestration_span."""

0 commit comments

Comments
 (0)