Skip to content

Commit 6375996

Browse files
Conformance finding: worker-versioning no-compatible-worker behavior remains unproved (#202)
1 parent db7f9dc commit 6375996

4 files changed

Lines changed: 123 additions & 8 deletions

File tree

‎src/durable_workflow/client.py‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3242,7 +3242,14 @@ async def poll_workflow_task_response(
32423242
return {"task": None, "poll_status": "timeout"}
32433243

32443244
async def poll_workflow_task(
3245-
self, *, worker_id: str, task_queue: str, timeout: float = 35.0
3245+
self,
3246+
*,
3247+
worker_id: str,
3248+
task_queue: str,
3249+
timeout: float = 35.0,
3250+
build_id: str | None = None,
3251+
history_page_size: int | None = None,
3252+
poll_request_id: str | None = None,
32463253
) -> Any:
32473254
"""Long-poll for the next workflow task on ``task_queue``.
32483255
@@ -3254,6 +3261,9 @@ async def poll_workflow_task(
32543261
worker_id=worker_id,
32553262
task_queue=task_queue,
32563263
timeout=timeout,
3264+
build_id=build_id,
3265+
history_page_size=history_page_size,
3266+
poll_request_id=poll_request_id,
32573267
)
32583268

32593269
return data.get("task")
@@ -3341,7 +3351,13 @@ async def workflow_task_history(
33413351
)
33423352

33433353
async def poll_query_task(
3344-
self, *, worker_id: str, task_queue: str, timeout: float = 35.0
3354+
self,
3355+
*,
3356+
worker_id: str,
3357+
task_queue: str,
3358+
timeout: float = 35.0,
3359+
build_id: str | None = None,
3360+
poll_request_id: str | None = None,
33453361
) -> Any:
33463362
"""Long-poll for the next workflow query task on ``task_queue``.
33473363
@@ -3351,8 +3367,11 @@ async def poll_query_task(
33513367
body: dict[str, Any] = {
33523368
"worker_id": worker_id,
33533369
"task_queue": task_queue,
3354-
"poll_request_id": f"query-poll-{uuid.uuid4().hex}",
3370+
"poll_request_id": poll_request_id or f"query-poll-{uuid.uuid4().hex}",
33553371
}
3372+
if build_id:
3373+
body["build_id"] = build_id
3374+
33563375
for _ in range(2):
33573376
try:
33583377
data = await self._request(
@@ -3426,7 +3445,13 @@ async def fail_query_task(
34263445
)
34273446

34283447
async def poll_activity_task(
3429-
self, *, worker_id: str, task_queue: str, timeout: float = 35.0
3448+
self,
3449+
*,
3450+
worker_id: str,
3451+
task_queue: str,
3452+
timeout: float = 35.0,
3453+
build_id: str | None = None,
3454+
poll_request_id: str | None = None,
34303455
) -> Any:
34313456
"""Long-poll for the next activity task on ``task_queue``.
34323457
@@ -3436,8 +3461,11 @@ async def poll_activity_task(
34363461
body: dict[str, Any] = {
34373462
"worker_id": worker_id,
34383463
"task_queue": task_queue,
3439-
"poll_request_id": f"activity-poll-{uuid.uuid4().hex}",
3464+
"poll_request_id": poll_request_id or f"activity-poll-{uuid.uuid4().hex}",
34403465
}
3466+
if build_id:
3467+
body["build_id"] = build_id
3468+
34413469
for _ in range(2):
34423470
try:
34433471
data = await self._request(

‎src/durable_workflow/worker.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,7 @@ async def _poll_workflow_tasks(self) -> None:
15881588
worker_id=self.worker_id,
15891589
task_queue=self.task_queue,
15901590
timeout=self._poll_http_timeout,
1591+
build_id=self.build_id,
15911592
)
15921593
except Exception as e:
15931594
self._wf_semaphore.release()
@@ -1665,6 +1666,7 @@ async def _poll_activity_tasks(self) -> None:
16651666
worker_id=self.worker_id,
16661667
task_queue=self.task_queue,
16671668
timeout=self._poll_http_timeout,
1669+
build_id=self.build_id,
16681670
)
16691671
except Exception as e:
16701672
self._act_semaphore.release()
@@ -1702,6 +1704,7 @@ async def _poll_query_tasks(self, *, client: Client | None = None, track_tasks:
17021704
worker_id=self.worker_id,
17031705
task_queue=self.task_queue,
17041706
timeout=self._poll_http_timeout,
1707+
build_id=self.build_id,
17051708
)
17061709
except Exception as e:
17071710
self._record_poll_metrics("query", "error", time.perf_counter() - poll_start)
@@ -1960,6 +1963,7 @@ async def run_until(
19601963
worker_id=self.worker_id,
19611964
task_queue=self.task_queue,
19621965
timeout=self._poll_http_timeout,
1966+
build_id=self.build_id,
19631967
)
19641968
self._record_poll_metrics(
19651969
"workflow",
@@ -1990,6 +1994,7 @@ async def run_until(
19901994
worker_id=self.worker_id,
19911995
task_queue=self.task_queue,
19921996
timeout=self._poll_http_timeout,
1997+
build_id=self.build_id,
19931998
)
19941999
self._record_poll_metrics(
19952000
"activity",

‎tests/test_client.py‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,10 +2302,15 @@ async def test_poll_workflow_task_keeps_returning_none_for_no_compatible_status(
23022302
"poll_status": "no_compatible_worker",
23032303
}
23042304

2305-
with patch.object(client, "poll_workflow_task_response", new_callable=AsyncMock, return_value=response):
2306-
task = await client.poll_workflow_task(worker_id="worker-v2", task_queue="queue-1")
2305+
with patch.object(client, "poll_workflow_task_response", new_callable=AsyncMock, return_value=response) as mock:
2306+
task = await client.poll_workflow_task(
2307+
worker_id="worker-v2",
2308+
task_queue="queue-1",
2309+
build_id="build-v2",
2310+
)
23072311

23082312
assert task is None
2313+
assert mock.await_args.kwargs["build_id"] == "build-v2"
23092314

23102315
@pytest.mark.asyncio
23112316
async def test_poll_workflow_task_retries_once_with_same_poll_request_id_after_timeout(
@@ -2335,12 +2340,36 @@ async def test_poll_activity_task_sends_poll_request_id(self, client: Client) ->
23352340
response_task = {"task": {"task_id": "activity-task-123"}}
23362341

23372342
with patch.object(client, "_request", new_callable=AsyncMock, return_value=response_task) as mock:
2338-
task = await client.poll_activity_task(worker_id="worker-1", task_queue="queue-1")
2343+
task = await client.poll_activity_task(
2344+
worker_id="worker-1",
2345+
task_queue="queue-1",
2346+
build_id="build-1",
2347+
)
2348+
2349+
assert task == response_task["task"]
2350+
request_body = mock.await_args.kwargs["json"]
2351+
assert request_body["worker_id"] == "worker-1"
2352+
assert request_body["task_queue"] == "queue-1"
2353+
assert request_body["build_id"] == "build-1"
2354+
assert isinstance(request_body["poll_request_id"], str)
2355+
assert request_body["poll_request_id"] != ""
2356+
2357+
@pytest.mark.asyncio
2358+
async def test_poll_query_task_sends_build_id(self, client: Client) -> None:
2359+
response_task = {"task": {"query_task_id": "query-task-123"}}
2360+
2361+
with patch.object(client, "_request", new_callable=AsyncMock, return_value=response_task) as mock:
2362+
task = await client.poll_query_task(
2363+
worker_id="worker-1",
2364+
task_queue="queue-1",
2365+
build_id="build-1",
2366+
)
23392367

23402368
assert task == response_task["task"]
23412369
request_body = mock.await_args.kwargs["json"]
23422370
assert request_body["worker_id"] == "worker-1"
23432371
assert request_body["task_queue"] == "queue-1"
2372+
assert request_body["build_id"] == "build-1"
23442373
assert isinstance(request_body["poll_request_id"], str)
23452374
assert request_body["poll_request_id"] != ""
23462375

‎tests/test_worker.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,59 @@ async def test_register_forwards_build_id_when_configured(
403403
call_kwargs = mock_client.register_worker.call_args.kwargs
404404
assert call_kwargs["build_id"] == "release-2026.04.22-a1"
405405

406+
@pytest.mark.asyncio
407+
async def test_poll_loops_forward_build_id_when_configured(
408+
self, mock_client: AsyncMock
409+
) -> None:
410+
workflow_called = asyncio.Event()
411+
activity_called = asyncio.Event()
412+
worker = Worker(
413+
mock_client,
414+
task_queue="q1",
415+
workflows=[TestWorkflow],
416+
activities=[echo_activity],
417+
worker_id="w-build-poll",
418+
build_id="release-2026.04.22-a1",
419+
poll_timeout=0.01,
420+
)
421+
422+
async def workflow_poll_once(**_: object) -> None:
423+
workflow_called.set()
424+
await worker.stop()
425+
return None
426+
427+
async def activity_poll_once(**_: object) -> None:
428+
activity_called.set()
429+
await worker.stop()
430+
return None
431+
432+
mock_client.poll_workflow_task.side_effect = workflow_poll_once
433+
await worker._poll_workflow_tasks()
434+
workflow_kwargs = mock_client.poll_workflow_task.call_args.kwargs
435+
assert workflow_called.is_set()
436+
assert workflow_kwargs["build_id"] == "release-2026.04.22-a1"
437+
438+
worker._stop.clear()
439+
mock_client.poll_activity_task.side_effect = activity_poll_once
440+
await worker._poll_activity_tasks()
441+
activity_kwargs = mock_client.poll_activity_task.call_args.kwargs
442+
assert activity_called.is_set()
443+
assert activity_kwargs["build_id"] == "release-2026.04.22-a1"
444+
445+
worker._stop.clear()
446+
query_called = asyncio.Event()
447+
448+
async def query_poll_once(**_: object) -> None:
449+
query_called.set()
450+
await worker.stop()
451+
return None
452+
453+
mock_client.poll_query_task.side_effect = query_poll_once
454+
await worker._poll_query_tasks()
455+
query_kwargs = mock_client.poll_query_task.call_args.kwargs
456+
assert query_called.is_set()
457+
assert query_kwargs["build_id"] == "release-2026.04.22-a1"
458+
406459
@pytest.mark.asyncio
407460
async def test_register_omits_build_id_when_not_configured(
408461
self, mock_client: AsyncMock

0 commit comments

Comments
 (0)