Skip to content

Commit 2bf1a38

Browse files
TD-P002: Python workers do not prove worker protocol 1.1 feature-floor handling (#28)
1 parent be99cde commit 2bf1a38

5 files changed

Lines changed: 47 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Targeting continued alignment with the Durable Workflow v2 protocol
1919
surface advertised by `/api/cluster/info`
2020
(`control_plane.version: "2"`, request-contract version `1`,
21-
`worker_protocol.version: "1.0"`). Remaining v2 follow-ups tracked for
21+
`worker_protocol.version: "1.1"`). Remaining v2 follow-ups tracked for
2222
this line: server-routed Python query execution and pre-accept update
2323
validator routing.
2424

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,13 +457,13 @@ Full documentation is available at
457457

458458
## Compatibility
459459

460-
SDK version 0.2.x is compatible with servers that advertise these protocol
460+
SDK version 0.4.x is compatible with servers that advertise these protocol
461461
manifests from `GET /api/cluster/info`:
462462

463463
- `control_plane.version: "2"`
464464
- `control_plane.request_contract.schema: durable-workflow.v2.control-plane-request.contract` version `1`
465465
- `auth_composition_contract.schema: durable-workflow.v2.auth-composition.contract` version `1`
466-
- `worker_protocol.version: "1.0"`
466+
- `worker_protocol.version: "1.1"`
467467
- `worker_protocol.external_task_input_contract.schema: durable-workflow.v2.external-task-input.contract` version `1`
468468
- `worker_protocol.external_task_result_contract.schema: durable-workflow.v2.external-task-result.contract` version `1`
469469

src/durable_workflow/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from .metrics import CLIENT_REQUEST_DURATION_SECONDS, CLIENT_REQUESTS, NOOP_METRICS, MetricsRecorder
4343
from .retry_policy import TransportRetryPolicy
4444

45-
PROTOCOL_VERSION = "1.0"
45+
PROTOCOL_VERSION = "1.1"
4646
CONTROL_PLANE_VERSION = "2"
4747
CONTROL_PLANE_REQUEST_CONTRACT_SCHEMA = "durable-workflow.v2.control-plane-request.contract"
4848
CONTROL_PLANE_REQUEST_CONTRACT_VERSION = 1

src/durable_workflow/worker.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,14 @@ def _validate_server_compatibility(info: dict[str, Any]) -> None:
169169
if not isinstance(control_plane, dict):
170170
raise RuntimeError(
171171
"Server compatibility error: missing control_plane manifest; "
172-
f"sdk-python 0.2.x requires control_plane.version {CONTROL_PLANE_VERSION}."
172+
f"sdk-python requires control_plane.version {CONTROL_PLANE_VERSION}."
173173
)
174174

175175
control_plane_version = _manifest_version(control_plane)
176176
if control_plane_version != CONTROL_PLANE_VERSION:
177177
raise RuntimeError(
178178
"Server compatibility error: unsupported control_plane.version "
179-
f"{control_plane_version!r}; sdk-python 0.2.x requires {CONTROL_PLANE_VERSION!r}."
179+
f"{control_plane_version!r}; sdk-python requires {CONTROL_PLANE_VERSION!r}."
180180
)
181181

182182
request_contract = control_plane.get("request_contract")
@@ -202,14 +202,14 @@ def _validate_server_compatibility(info: dict[str, Any]) -> None:
202202
if not isinstance(worker_protocol, dict):
203203
raise RuntimeError(
204204
"Server compatibility error: missing worker_protocol manifest; "
205-
f"sdk-python 0.2.x requires worker_protocol.version {PROTOCOL_VERSION}."
205+
f"sdk-python requires worker_protocol.version {PROTOCOL_VERSION}."
206206
)
207207

208208
worker_protocol_version = _manifest_version(worker_protocol)
209209
if worker_protocol_version != PROTOCOL_VERSION:
210210
raise RuntimeError(
211211
"Server compatibility error: unsupported worker_protocol.version "
212-
f"{worker_protocol_version!r}; sdk-python 0.2.x requires {PROTOCOL_VERSION!r}."
212+
f"{worker_protocol_version!r}; sdk-python requires {PROTOCOL_VERSION!r}."
213213
)
214214

215215
auth_composition = info.get("auth_composition_contract")

tests/test_worker.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,45 @@ async def test_register_rejects_unsupported_worker_protocol_version(self, mock_c
341341
await worker._register()
342342
mock_client.register_worker.assert_not_called()
343343

344+
@pytest.mark.asyncio
345+
async def test_register_rejects_worker_protocol_below_payload_codec_floor(
346+
self, mock_client: AsyncMock
347+
) -> None:
348+
mock_client.get_cluster_info = AsyncMock(
349+
return_value=compatible_cluster_info(worker_protocol={"version": "1.0"})
350+
)
351+
worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
352+
with pytest.raises(RuntimeError, match="requires '1.1'"):
353+
await worker._register()
354+
mock_client.register_worker.assert_not_called()
355+
356+
@pytest.mark.asyncio
357+
async def test_register_accepts_protocol_1_1_when_newer_feature_floor_is_unavailable(
358+
self, mock_client: AsyncMock
359+
) -> None:
360+
mock_client.get_cluster_info = AsyncMock(
361+
return_value=compatible_cluster_info(
362+
worker_protocol={
363+
"version": PROTOCOL_VERSION,
364+
"server_capabilities": {
365+
"query_tasks": True,
366+
"worker_session_verbs": [],
367+
"worker_sessions": {
368+
"feature": "worker_sessions",
369+
"supported": False,
370+
"minimum_protocol_version": "1.2",
371+
"unavailable_reason": "worker_protocol_version_below_worker_session_minimum",
372+
},
373+
},
374+
}
375+
)
376+
)
377+
worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
378+
379+
await worker._register()
380+
381+
mock_client.register_worker.assert_awaited_once()
382+
344383
@pytest.mark.asyncio
345384
async def test_register_rejects_missing_auth_composition_contract(self, mock_client: AsyncMock) -> None:
346385
mock_client.get_cluster_info = AsyncMock(return_value=compatible_cluster_info(auth_composition_contract=None))

0 commit comments

Comments
 (0)