Skip to content

Commit a093c79

Browse files
Conformance finding: worker-versioning PHP/Python build-id pinning remains unproved (#192)
1 parent 639a5cb commit a093c79

4 files changed

Lines changed: 58 additions & 0 deletions

File tree

‎src/durable_workflow/client.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,6 +2138,8 @@ async def start_workflow(
21382138
priority: int | None = None,
21392139
fairness_key: str | None = None,
21402140
fairness_weight: int | None = None,
2141+
build_id: str | None = None,
2142+
compatibility: str | None = None,
21412143
) -> WorkflowHandle:
21422144
"""Start a new workflow instance and return a handle bound to it.
21432145
@@ -2166,6 +2168,11 @@ async def start_workflow(
21662168
(``1..1000``, default ``1``) lets a class take a proportionally
21672169
larger share of dispatch slots versus other classes on the same
21682170
queue.
2171+
2172+
``build_id`` pins the new run to a specific worker build when
2173+
multiple worker versions share a task queue. ``compatibility`` is an
2174+
alias accepted by the server for SDK-neutral callers; when both are
2175+
provided, the server gives ``build_id`` precedence.
21692176
"""
21702177
body: dict[str, Any] = {
21712178
"workflow_id": workflow_id,
@@ -2198,6 +2205,10 @@ async def start_workflow(
21982205
body["fairness_key"] = fairness_key
21992206
if fairness_weight is not None:
22002207
body["fairness_weight"] = fairness_weight
2208+
if build_id is not None:
2209+
body["build_id"] = build_id
2210+
if compatibility is not None:
2211+
body["compatibility"] = compatibility
22012212
data = await self._request("POST", "/workflows", json=body, context=workflow_id)
22022213
return WorkflowHandle(
22032214
self,

‎src/durable_workflow/sync.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ def start_workflow(
273273
priority: int | None = None,
274274
fairness_key: str | None = None,
275275
fairness_weight: int | None = None,
276+
build_id: str | None = None,
277+
compatibility: str | None = None,
276278
) -> SyncWorkflowHandle:
277279
handle = _run(
278280
self._async.start_workflow(
@@ -288,6 +290,8 @@ def start_workflow(
288290
priority=priority,
289291
fairness_key=fairness_key,
290292
fairness_weight=fairness_weight,
293+
build_id=build_id,
294+
compatibility=compatibility,
291295
)
292296
)
293297
return SyncWorkflowHandle(handle)

‎tests/test_client.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,26 @@ async def test_priority_and_fairness_are_forwarded_on_start(self, client: Client
263263
assert body["fairness_key"] == "tenant-a"
264264
assert body["fairness_weight"] == 3
265265

266+
@pytest.mark.asyncio
267+
async def test_build_id_and_compatibility_are_forwarded_on_start(self, client: Client) -> None:
268+
resp = _mock_response(201, {
269+
"workflow_id": "wf-build",
270+
"run_id": "run-build",
271+
"workflow_type": "greeter",
272+
})
273+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp) as mock:
274+
await client.start_workflow(
275+
workflow_type="greeter",
276+
task_queue="shared",
277+
workflow_id="wf-build",
278+
build_id="php-v1",
279+
compatibility="ignored-when-build-id-present",
280+
)
281+
282+
body = mock.call_args.kwargs.get("json") or mock.call_args[1].get("json")
283+
assert body["build_id"] == "php-v1"
284+
assert body["compatibility"] == "ignored-when-build-id-present"
285+
266286
@pytest.mark.asyncio
267287
async def test_priority_and_fairness_are_omitted_when_unset(self, client: Client) -> None:
268288
resp = _mock_response(201, {

‎tests/test_sync.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,29 @@ def test_priority_and_fairness_are_forwarded_on_start(self) -> None:
9898
assert body["fairness_key"] == "tenant-a"
9999
assert body["fairness_weight"] == 3
100100

101+
def test_build_id_is_forwarded_on_start(self) -> None:
102+
client = Client("http://localhost:8080")
103+
resp = _mock_response(
104+
201,
105+
{
106+
"workflow_id": "wf-build",
107+
"run_id": "run-build",
108+
"workflow_type": "greeter",
109+
},
110+
)
111+
with patch.object(
112+
client._async._http, "request", new_callable=AsyncMock, return_value=resp
113+
) as mock:
114+
client.start_workflow(
115+
workflow_type="greeter",
116+
task_queue="shared",
117+
workflow_id="wf-build",
118+
build_id="python-v1",
119+
)
120+
121+
body = mock.call_args.kwargs.get("json") or mock.call_args[1].get("json")
122+
assert body["build_id"] == "python-v1"
123+
101124
def test_priority_and_fairness_are_omitted_when_unset(self) -> None:
102125
client = Client("http://localhost:8080")
103126
resp = _mock_response(

0 commit comments

Comments
 (0)