durable-workflow 2.3.7__tar.gz → 2.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {durable_workflow-2.3.7/src/durable_workflow.egg-info → durable_workflow-2.3.9}/PKG-INFO +1 -1
  2. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/pyproject.toml +3 -3
  3. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/client.py +1 -1
  4. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/worker.py +1 -27
  5. {durable_workflow-2.3.7 → durable_workflow-2.3.9/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  6. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
  7. durable_workflow-2.3.9/tests/test_client_timeouts.py +93 -0
  8. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_worker.py +11 -4
  9. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/LICENSE +0 -0
  10. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/README.md +0 -0
  11. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/setup.cfg +0 -0
  12. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/__init__.py +0 -0
  13. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/_avro.py +0 -0
  14. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/activity.py +0 -0
  15. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/auth_composition.py +0 -0
  16. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/errors.py +0 -0
  17. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/external_storage.py +0 -0
  18. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/external_task_input.py +0 -0
  19. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/external_task_result.py +0 -0
  20. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/history_bundle_verify.py +0 -0
  21. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/interceptors.py +0 -0
  22. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/invocable.py +0 -0
  23. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/metrics.py +0 -0
  24. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/nexus.py +0 -0
  25. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/py.typed +0 -0
  26. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/python_conformance.py +0 -0
  27. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/replay_conformance.py +0 -0
  28. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/replay_verify.py +0 -0
  29. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/retry_policy.py +0 -0
  30. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/serializer.py +0 -0
  31. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/sync.py +0 -0
  32. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/testing.py +0 -0
  33. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/worker_session.py +0 -0
  34. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/workflow.py +0 -0
  35. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  36. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  37. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  38. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow.egg-info/requires.txt +0 -0
  39. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/src/durable_workflow.egg-info/top_level.txt +0 -0
  40. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_activity_context.py +0 -0
  41. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_auth_composition.py +0 -0
  42. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_avro_value_benchmark.py +0 -0
  43. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_avro_value_protocol.py +0 -0
  44. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_ci_checkout.py +0 -0
  45. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_ci_integration_endpoint.py +0 -0
  46. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_client.py +0 -0
  47. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_codec_regression_corpus.py +0 -0
  48. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_control_plane_parity_fixtures.py +0 -0
  49. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_docstring_cross_references.py +0 -0
  50. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_durable_selection.py +0 -0
  51. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_errors.py +0 -0
  52. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_external_storage.py +0 -0
  53. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_external_task_input.py +0 -0
  54. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_external_task_result.py +0 -0
  55. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_golden_history_replay.py +0 -0
  56. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_history_bundle_verify.py +0 -0
  57. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_history_event_contract.py +0 -0
  58. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_invocable.py +0 -0
  59. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_message_streams.py +0 -0
  60. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_metrics.py +0 -0
  61. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_order_processing_example.py +0 -0
  62. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_parallel_completion_order_replay.py +0 -0
  63. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_parallel_saga_authoring.py +0 -0
  64. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_public_boundary_scanner.py +0 -0
  65. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_pypi_project_surface.py +0 -0
  66. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_python_conformance.py +0 -0
  67. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_queries.py +0 -0
  68. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_readme_quickstart.py +0 -0
  69. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_redrive_replay.py +0 -0
  70. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_release_metadata.py +0 -0
  71. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_replay.py +0 -0
  72. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_replay_conformance.py +0 -0
  73. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_replay_regression_corpus.py +0 -0
  74. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_replay_verify.py +0 -0
  75. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_retry_policy.py +0 -0
  76. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_runtime_external_payload_transport.py +0 -0
  77. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_schedules.py +0 -0
  78. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_serializer.py +0 -0
  79. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_signals.py +0 -0
  80. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_sleep.py +0 -0
  81. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_standalone_activity_client.py +0 -0
  82. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_storage_admission.py +0 -0
  83. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_sync.py +0 -0
  84. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_testing_harness.py +0 -0
  85. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_update_signal_condition_replay.py +0 -0
  86. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_updates.py +0 -0
  87. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_wait_condition.py +0 -0
  88. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_worker_session.py +0 -0
  89. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_workflow_result_timeout.py +0 -0
  90. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_workflow_streams.py +0 -0
  91. {durable_workflow-2.3.7 → durable_workflow-2.3.9}/tests/test_workflow_updates_conformance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.3.7"
7
+ version = "2.3.9"
8
8
  description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
71
71
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
72
72
 
73
73
  [tool.durable-workflow]
74
- product-train = "2.3.7"
75
- registry-version = "2.3.7"
74
+ product-train = "2.3.9"
75
+ registry-version = "2.3.9"
76
76
  supported-server-versions = "2.4.8"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -1794,7 +1794,7 @@ class Client:
1794
1794
  f"/api{path}",
1795
1795
  headers=self._discovery_headers() if discovery else self._headers(worker=worker),
1796
1796
  json=request_json,
1797
- timeout=timeout,
1797
+ timeout=self.timeout if timeout is None else timeout,
1798
1798
  )
1799
1799
  # Raise HTTPStatusError for 4xx/5xx so retry policy can catch it
1800
1800
  resp.raise_for_status()
@@ -951,30 +951,6 @@ def _server_supports_update_validation_tasks(info: dict[str, Any]) -> bool:
951
951
  )
952
952
 
953
953
 
954
- def _server_long_poll_timeout(info: dict[str, Any]) -> float | None:
955
- worker_protocol = info.get("worker_protocol")
956
- if not isinstance(worker_protocol, dict):
957
- return None
958
-
959
- capabilities = worker_protocol.get("server_capabilities")
960
- if not isinstance(capabilities, dict):
961
- return None
962
-
963
- timeout = capabilities.get("long_poll_timeout")
964
- if isinstance(timeout, bool):
965
- return None
966
- if isinstance(timeout, int | float):
967
- return float(timeout) if timeout > 0 else None
968
- if isinstance(timeout, str):
969
- try:
970
- parsed = float(timeout)
971
- except ValueError:
972
- return None
973
- return parsed if parsed > 0 else None
974
-
975
- return None
976
-
977
-
978
954
  def _contract_version_matches(value: Any, expected: int) -> bool:
979
955
  if isinstance(value, int):
980
956
  return value == expected
@@ -1041,6 +1017,7 @@ class Worker:
1041
1017
  raise ValueError("heartbeat_interval must be positive")
1042
1018
 
1043
1019
  self._poll_timeout = poll_timeout
1020
+ # Client supplies HTTP grace separately from this requested poll window.
1044
1021
  self._poll_http_timeout = poll_timeout
1045
1022
  self.max_concurrent_workflow_tasks = max_concurrent_workflow_tasks
1046
1023
  self.max_concurrent_activity_tasks = max_concurrent_activity_tasks
@@ -1191,9 +1168,6 @@ class Worker:
1191
1168
  "multiplexed workflow/update-validation polling. Refusing registration so validated "
1192
1169
  "updates cannot be accepted without validator approval or exceed worker capacity."
1193
1170
  )
1194
- server_long_poll_timeout = _server_long_poll_timeout(info)
1195
- if server_long_poll_timeout is not None:
1196
- self._poll_http_timeout = max(self._poll_http_timeout, server_long_poll_timeout + 5.0)
1197
1171
  log.debug(
1198
1172
  "server compatibility accepted: app_version=%s control_plane=%s worker_protocol=%s",
1199
1173
  info.get("version", "unknown"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.3.7
3
+ Version: 2.3.9
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ tests/test_avro_value_protocol.py
40
40
  tests/test_ci_checkout.py
41
41
  tests/test_ci_integration_endpoint.py
42
42
  tests/test_client.py
43
+ tests/test_client_timeouts.py
43
44
  tests/test_codec_regression_corpus.py
44
45
  tests/test_control_plane_parity_fixtures.py
45
46
  tests/test_docstring_cross_references.py
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from durable_workflow.client import Client
12
+ from durable_workflow.retry_policy import TransportRetryPolicy
13
+
14
+
15
+ @asynccontextmanager
16
+ async def response_server(*, delay: float | None) -> AsyncIterator[str]:
17
+ """A real HTTP peer, with response progress controlled by the test."""
18
+ writers: list[asyncio.StreamWriter] = []
19
+ handlers: set[asyncio.Task[None]] = set()
20
+
21
+ async def respond(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
22
+ task = asyncio.current_task()
23
+ assert task is not None
24
+ handlers.add(task)
25
+ writers.append(writer)
26
+ try:
27
+ headers = await reader.readuntil(b"\r\n\r\n")
28
+ for header in headers.split(b"\r\n"):
29
+ if header.lower().startswith(b"content-length:"):
30
+ await reader.readexactly(int(header.split(b":", 1)[1]))
31
+ if delay is None:
32
+ await reader.read()
33
+ return
34
+ await asyncio.sleep(delay)
35
+ body = json.dumps({"task": None, "poll_status": "empty"}).encode()
36
+ writer.write(
37
+ b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n"
38
+ + f"Content-Length: {len(body)}\r\nConnection: close\r\n\r\n".encode()
39
+ + body
40
+ )
41
+ await writer.drain()
42
+ finally:
43
+ writer.close()
44
+ await writer.wait_closed()
45
+ handlers.discard(task)
46
+
47
+ server = await asyncio.start_server(respond, "127.0.0.1", 0)
48
+ try:
49
+ assert server.sockets
50
+ yield f"http://127.0.0.1:{server.sockets[0].getsockname()[1]}"
51
+ finally:
52
+ server.close()
53
+ await server.wait_closed()
54
+ for writer in writers:
55
+ writer.close()
56
+ if handlers:
57
+ await asyncio.gather(*handlers)
58
+
59
+
60
+ @pytest.mark.asyncio
61
+ @pytest.mark.parametrize("operation", ["control", "discovery", "worker"])
62
+ async def test_configured_timeout_bounds_a_stalled_response(operation: str) -> None:
63
+ async with (
64
+ response_server(delay=None) as endpoint,
65
+ Client(endpoint, timeout=0.05, retry_policy=TransportRetryPolicy(max_attempts=1)) as client,
66
+ ):
67
+ if operation == "control":
68
+ request = client.health()
69
+ elif operation == "discovery":
70
+ request = client.get_cluster_info()
71
+ else:
72
+ request = client.complete_workflow_task(
73
+ task_id="synthetic-timeout-task",
74
+ lease_owner="synthetic-timeout-worker",
75
+ workflow_task_attempt=1,
76
+ commands=[],
77
+ )
78
+ with pytest.raises(httpx.ReadTimeout):
79
+ await asyncio.wait_for(request, timeout=2)
80
+
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_explicit_poll_timeout_overrides_a_shorter_client_default() -> None:
84
+ async with (
85
+ response_server(delay=0.1) as endpoint,
86
+ Client(endpoint, timeout=0.01, retry_policy=TransportRetryPolicy(max_attempts=1)) as client,
87
+ ):
88
+ task = await client.poll_workflow_task(
89
+ worker_id="synthetic-timeout-worker",
90
+ task_queue="synthetic-timeout-queue",
91
+ timeout=0,
92
+ )
93
+ assert task is None
@@ -711,7 +711,14 @@ class TestWorkerRegistration:
711
711
  )
712
712
 
713
713
  @pytest.mark.asyncio
714
- async def test_register_keeps_http_timeout_above_server_long_poll(self, mock_client: AsyncMock) -> None:
714
+ @pytest.mark.parametrize(("poll_loop", "poll_method"), [
715
+ ("_poll_workflow_tasks", "poll_workflow_task"),
716
+ ("_poll_activity_tasks", "poll_activity_task"),
717
+ ("_poll_query_tasks", "poll_query_task"),
718
+ ])
719
+ async def test_registration_preserves_configured_poll_window(
720
+ self, mock_client: AsyncMock, poll_loop: str, poll_method: str,
721
+ ) -> None:
715
722
  mock_client.get_cluster_info = AsyncMock(
716
723
  return_value=compatible_cluster_info(
717
724
  worker_protocol={
@@ -736,12 +743,12 @@ class TestWorkerRegistration:
736
743
  worker._stop.set()
737
744
  return None
738
745
 
739
- mock_client.poll_workflow_task.side_effect = poll_once
746
+ getattr(mock_client, poll_method).side_effect = poll_once
740
747
 
741
748
  await worker._register()
742
- await worker._poll_workflow_tasks()
749
+ await getattr(worker, poll_loop)()
743
750
 
744
- assert mock_client.poll_workflow_task.call_args.kwargs["timeout"] == 17.0
751
+ assert getattr(mock_client, poll_method).call_args.kwargs["timeout"] == 0.01
745
752
 
746
753
  @pytest.mark.asyncio
747
754
  async def test_register_keeps_baseline_capabilities_when_server_does_not_support_query_tasks(