durable-workflow 2.3.5__tar.gz → 2.3.7__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 (90) hide show
  1. {durable_workflow-2.3.5/src/durable_workflow.egg-info → durable_workflow-2.3.7}/PKG-INFO +1 -1
  2. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/pyproject.toml +3 -3
  3. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/external_storage.py +1 -1
  4. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/worker.py +14 -14
  5. {durable_workflow-2.3.5 → durable_workflow-2.3.7/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  6. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_external_storage.py +21 -1
  7. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_replay_regression_corpus.py +50 -2
  8. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_worker.py +27 -0
  9. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/LICENSE +0 -0
  10. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/README.md +0 -0
  11. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/setup.cfg +0 -0
  12. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/__init__.py +0 -0
  13. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/_avro.py +0 -0
  14. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/activity.py +0 -0
  15. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/auth_composition.py +0 -0
  16. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/client.py +0 -0
  17. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/errors.py +0 -0
  18. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/external_task_input.py +0 -0
  19. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/external_task_result.py +0 -0
  20. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/history_bundle_verify.py +0 -0
  21. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/interceptors.py +0 -0
  22. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/invocable.py +0 -0
  23. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/metrics.py +0 -0
  24. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/nexus.py +0 -0
  25. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/py.typed +0 -0
  26. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/python_conformance.py +0 -0
  27. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/replay_conformance.py +0 -0
  28. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/replay_verify.py +0 -0
  29. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/retry_policy.py +0 -0
  30. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/serializer.py +0 -0
  31. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/sync.py +0 -0
  32. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/testing.py +0 -0
  33. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/worker_session.py +0 -0
  34. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/workflow.py +0 -0
  35. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  36. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  37. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  38. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  39. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow.egg-info/requires.txt +0 -0
  40. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/src/durable_workflow.egg-info/top_level.txt +0 -0
  41. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_activity_context.py +0 -0
  42. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_auth_composition.py +0 -0
  43. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_avro_value_benchmark.py +0 -0
  44. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_avro_value_protocol.py +0 -0
  45. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_ci_checkout.py +0 -0
  46. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_ci_integration_endpoint.py +0 -0
  47. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_client.py +0 -0
  48. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_codec_regression_corpus.py +0 -0
  49. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_control_plane_parity_fixtures.py +0 -0
  50. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_docstring_cross_references.py +0 -0
  51. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_durable_selection.py +0 -0
  52. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_errors.py +0 -0
  53. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_external_task_input.py +0 -0
  54. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_external_task_result.py +0 -0
  55. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_golden_history_replay.py +0 -0
  56. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_history_bundle_verify.py +0 -0
  57. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_history_event_contract.py +0 -0
  58. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_invocable.py +0 -0
  59. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_message_streams.py +0 -0
  60. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_metrics.py +0 -0
  61. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_order_processing_example.py +0 -0
  62. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_parallel_completion_order_replay.py +0 -0
  63. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_parallel_saga_authoring.py +0 -0
  64. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_public_boundary_scanner.py +0 -0
  65. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_pypi_project_surface.py +0 -0
  66. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_python_conformance.py +0 -0
  67. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_queries.py +0 -0
  68. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_readme_quickstart.py +0 -0
  69. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_redrive_replay.py +0 -0
  70. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_release_metadata.py +0 -0
  71. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_replay.py +0 -0
  72. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_replay_conformance.py +0 -0
  73. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_replay_verify.py +0 -0
  74. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_retry_policy.py +0 -0
  75. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_runtime_external_payload_transport.py +0 -0
  76. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_schedules.py +0 -0
  77. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_serializer.py +0 -0
  78. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_signals.py +0 -0
  79. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_sleep.py +0 -0
  80. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_standalone_activity_client.py +0 -0
  81. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_storage_admission.py +0 -0
  82. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_sync.py +0 -0
  83. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_testing_harness.py +0 -0
  84. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_update_signal_condition_replay.py +0 -0
  85. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_updates.py +0 -0
  86. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_wait_condition.py +0 -0
  87. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_worker_session.py +0 -0
  88. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_workflow_result_timeout.py +0 -0
  89. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/tests/test_workflow_streams.py +0 -0
  90. {durable_workflow-2.3.5 → durable_workflow-2.3.7}/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.5
3
+ Version: 2.3.7
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.5"
7
+ version = "2.3.7"
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.5"
75
- registry-version = "2.3.5"
74
+ product-train = "2.3.7"
75
+ registry-version = "2.3.7"
76
76
  supported-server-versions = "2.4.8"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -250,7 +250,7 @@ class ExternalPayloadCache:
250
250
  contract as a fresh driver fetch.
251
251
  """
252
252
 
253
- def __init__(self, *, max_entries: int = 128, max_bytes: int = 16 * 1024 * 1024) -> None:
253
+ def __init__(self, *, max_entries: int = 1024, max_bytes: int = 16 * 1024 * 1024) -> None:
254
254
  if max_entries < 1:
255
255
  raise ValueError("external payload cache max_entries must be at least 1")
256
256
  if max_bytes < 1:
@@ -98,6 +98,7 @@ from .workflow import (
98
98
  log = logging.getLogger("durable_workflow.worker")
99
99
 
100
100
  QUERY_TASKS_CAPABILITY = "query_tasks"
101
+ WORKFLOW_HISTORY_PAGE_SIZE = 500
101
102
  UPDATE_VALIDATION_TASKS_CAPABILITY = "update_validation_tasks"
102
103
  WORKFLOW_UPDATES_CAPABILITY = "workflow_updates"
103
104
  MESSAGE_STREAMS_CAPABILITY = "message_streams"
@@ -1637,22 +1638,19 @@ class Worker:
1637
1638
  wf_type: str = task.get("workflow_type", "")
1638
1639
  history = task.get("history_events", [])
1639
1640
 
1640
- # Paginate history if needed
1641
+ # The worker requests bounded history pages when polling. Do not replay
1642
+ # an incomplete history if fetching a later page fails.
1641
1643
  next_page_token = task.get("next_history_page_token")
1642
1644
  while next_page_token:
1643
- try:
1644
- page_data = await self.client.workflow_task_history(
1645
- task_id=task_id,
1646
- next_history_page_token=next_page_token,
1647
- lease_owner=self.worker_id,
1648
- workflow_task_attempt=attempt,
1649
- )
1650
- if page_data and page_data.get("history_events"):
1651
- history.extend(page_data["history_events"])
1652
- next_page_token = page_data.get("next_history_page_token") if page_data else None
1653
- except Exception as e:
1654
- log.warning("failed to fetch history page for task %s: %s", task_id, e)
1655
- break
1645
+ page_data = await self.client.workflow_task_history(
1646
+ task_id=task_id,
1647
+ next_history_page_token=next_page_token,
1648
+ lease_owner=self.worker_id,
1649
+ workflow_task_attempt=attempt,
1650
+ )
1651
+ if page_data and page_data.get("history_events"):
1652
+ history.extend(page_data["history_events"])
1653
+ next_page_token = page_data.get("next_history_page_token") if page_data else None
1656
1654
 
1657
1655
  start_input: list[Any] = []
1658
1656
  codec = task.get("payload_codec")
@@ -2535,6 +2533,7 @@ class Worker:
2535
2533
  timeout=self._poll_http_timeout,
2536
2534
  build_id=self.build_id,
2537
2535
  task_kinds=self._workflow_poll_task_kinds(),
2536
+ history_page_size=WORKFLOW_HISTORY_PAGE_SIZE,
2538
2537
  )
2539
2538
  except asyncio.CancelledError:
2540
2539
  self._release_workflow_capacity()
@@ -3224,6 +3223,7 @@ class Worker:
3224
3223
  timeout=self._poll_http_timeout,
3225
3224
  build_id=self.build_id,
3226
3225
  task_kinds=self._workflow_poll_task_kinds(),
3226
+ history_page_size=WORKFLOW_HISTORY_PAGE_SIZE,
3227
3227
  )
3228
3228
  except BaseException:
3229
3229
  self._release_workflow_capacity()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.3.5
3
+ Version: 2.3.7
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
@@ -1,6 +1,6 @@
1
1
  import hashlib
2
2
  from pathlib import Path
3
- from unittest.mock import AsyncMock
3
+ from unittest.mock import AsyncMock, patch
4
4
 
5
5
  import pytest
6
6
 
@@ -740,6 +740,26 @@ def test_fetch_external_payload_cache_reuses_verified_bytes(tmp_path: Path) -> N
740
740
  assert len(cache) == 1
741
741
 
742
742
 
743
+ def test_default_cache_reuses_large_replay_reference_set(tmp_path: Path) -> None:
744
+ storage = LocalFilesystemExternalStorage(tmp_path)
745
+ cache = ExternalPayloadCache()
746
+ references = [
747
+ store_external_payload(storage, f"signal-{index}".encode(), codec="avro")
748
+ for index in range(1000)
749
+ ]
750
+
751
+ with patch.object(storage, "get", wraps=storage.get) as get:
752
+ for _ in range(2):
753
+ for index, reference in enumerate(references):
754
+ assert fetch_external_payload(storage, reference, cache=cache) == (
755
+ f"signal-{index}".encode()
756
+ )
757
+
758
+ assert get.call_count == len(references)
759
+ assert len(cache) == len(references)
760
+ assert cache.current_bytes <= cache.max_bytes == 16 * 1024 * 1024
761
+
762
+
743
763
  def test_delete_external_payload_removes_blob_and_cache_entry(tmp_path: Path) -> None:
744
764
  storage = LocalFilesystemExternalStorage(tmp_path)
745
765
  cache = ExternalPayloadCache(max_entries=2, max_bytes=1024)
@@ -4,11 +4,12 @@ import json
4
4
  from collections.abc import Mapping, Sequence
5
5
  from pathlib import Path
6
6
  from typing import Any
7
+ from unittest.mock import AsyncMock
7
8
 
8
9
  import pytest
9
10
 
10
- from durable_workflow import Replayer, serializer, workflow
11
- from durable_workflow.client import WorkflowStreamAppendItem
11
+ from durable_workflow import Replayer, Worker, serializer, workflow
12
+ from durable_workflow.client import Client, WorkflowStreamAppendItem
12
13
  from durable_workflow.errors import NonDeterministicReplayError, WorkflowPayloadDecodeError
13
14
  from durable_workflow.workflow import WorkflowContext, commands_to_server_commands, query_state
14
15
  from tests.test_golden_history_replay import (
@@ -156,6 +157,17 @@ class RecordedSideEffectWorkflow:
156
157
  return (yield ctx.side_effect(unexpected_execution))
157
158
 
158
159
 
160
+ @workflow.defn(name="tests.replay.paged-recorded-side-effects")
161
+ class PagedRecordedSideEffectsWorkflow:
162
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
163
+ def unexpected_execution() -> int:
164
+ raise AssertionError("recorded side effect callable ran during replay")
165
+
166
+ first = yield ctx.side_effect(unexpected_execution)
167
+ second = yield ctx.side_effect(unexpected_execution)
168
+ return first + second
169
+
170
+
159
171
  @workflow.defn(name="tests.replay.message-stream-consumer")
160
172
  class MessageStreamConsumerWorkflow:
161
173
  def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
@@ -212,6 +224,7 @@ WORKFLOWS = [
212
224
  NestedParallelPathWorkflow,
213
225
  ParallelMetadataProducerWorkflow,
214
226
  ParallelResultBindingWorkflow,
227
+ PagedRecordedSideEffectsWorkflow,
215
228
  PostConditionReceiversWorkflow,
216
229
  RecordedSideEffectWorkflow,
217
230
  SelectionAwaitMarkerWorkflow,
@@ -394,6 +407,41 @@ def test_checked_in_replay_regression_corpus_uses_official_replayer(
394
407
  _execute_fixture(fixture)
395
408
 
396
409
 
410
+ @pytest.mark.asyncio
411
+ async def test_worker_replays_recorded_side_effects_across_history_pages() -> None:
412
+ fixture = json.loads((FIXTURE_DIR / "paged-recorded-side-effects.json").read_text(encoding="utf-8"))
413
+ history = fixture["history"]
414
+ client = AsyncMock(spec=Client)
415
+ client.workflow_task_history.return_value = {
416
+ "history_events": history[1:],
417
+ "next_history_page_token": None,
418
+ }
419
+ worker = Worker(client, task_queue="paged-history", workflows=[PagedRecordedSideEffectsWorkflow])
420
+
421
+ commands = await worker._run_workflow_task_core({
422
+ "task_id": "task-paged-side-effects",
423
+ "workflow_id": "workflow-paged-side-effects",
424
+ "run_id": "run-paged-side-effects",
425
+ "workflow_type": fixture["workflow"]["type"],
426
+ "workflow_task_attempt": 1,
427
+ "history_events": history[:1],
428
+ "next_history_page_token": "second-page",
429
+ "arguments": serializer.encode([], codec="avro"),
430
+ "payload_codec": "avro",
431
+ })
432
+
433
+ client.workflow_task_history.assert_awaited_once_with(
434
+ task_id="task-paged-side-effects",
435
+ next_history_page_token="second-page",
436
+ lease_owner=worker.worker_id,
437
+ workflow_task_attempt=1,
438
+ )
439
+ client.complete_workflow_task.assert_awaited_once()
440
+ assert commands is not None
441
+ assert [command["type"] for command in commands] == ["complete_workflow"]
442
+ assert serializer.decode_envelope(commands[0]["result"]) == 198
443
+
444
+
397
445
  @pytest.mark.parametrize(
398
446
  "fixture",
399
447
  [
@@ -836,6 +836,7 @@ class TestWorkerRegistration:
836
836
  workflow_kwargs = mock_client.poll_workflow_task.call_args.kwargs
837
837
  assert workflow_called.is_set()
838
838
  assert workflow_kwargs["build_id"] == "release-2026.04.22-a1"
839
+ assert workflow_kwargs["history_page_size"] == 500
839
840
 
840
841
  worker._stop.clear()
841
842
  mock_client.poll_activity_task.side_effect = activity_poll_once
@@ -1189,6 +1190,31 @@ class TestWorkerRegistration:
1189
1190
 
1190
1191
 
1191
1192
  class TestWorkflowTaskExecution:
1193
+ @pytest.mark.asyncio
1194
+ async def test_history_page_failure_does_not_replay_partial_history(
1195
+ self, mock_client: AsyncMock
1196
+ ) -> None:
1197
+ mock_client.workflow_task_history.side_effect = RuntimeError("history page unavailable")
1198
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow])
1199
+ task = {
1200
+ "task_id": "t-paged-history",
1201
+ "workflow_type": "test-wf",
1202
+ "workflow_task_attempt": 1,
1203
+ "history_events": [{"event_id": "first-page"}],
1204
+ "next_history_page_token": "next-page",
1205
+ }
1206
+
1207
+ with pytest.raises(RuntimeError, match="history page unavailable"):
1208
+ await worker._run_workflow_task_core(task)
1209
+
1210
+ mock_client.workflow_task_history.assert_awaited_once_with(
1211
+ task_id="t-paged-history",
1212
+ next_history_page_token="next-page",
1213
+ lease_owner=worker.worker_id,
1214
+ workflow_task_attempt=1,
1215
+ )
1216
+ mock_client.complete_workflow_task.assert_not_awaited()
1217
+
1192
1218
  @pytest.mark.asyncio
1193
1219
  async def test_local_activity_runs_in_workflow_worker_and_records_heartbeats(
1194
1220
  self, mock_client: AsyncMock
@@ -4948,6 +4974,7 @@ class TestRunUntil:
4948
4974
  assert worker._stop.is_set()
4949
4975
  mock_client.register_worker.assert_awaited_once()
4950
4976
  assert mock_client.describe_workflow.await_count == 2
4977
+ assert mock_client.poll_workflow_task.call_args.kwargs["history_page_size"] == 500
4951
4978
 
4952
4979
  @pytest.mark.asyncio
4953
4980
  async def test_run_until_times_out_and_stops_worker(self, mock_client: AsyncMock) -> None: