durable-workflow 0.4.44__tar.gz → 0.4.45__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 (62) hide show
  1. {durable_workflow-0.4.44/src/durable_workflow.egg-info → durable_workflow-0.4.45}/PKG-INFO +1 -1
  2. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/pyproject.toml +1 -1
  3. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/worker.py +26 -0
  4. {durable_workflow-0.4.44 → durable_workflow-0.4.45/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  5. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_worker.py +65 -0
  6. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/LICENSE +0 -0
  7. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/README.md +0 -0
  8. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/setup.cfg +0 -0
  9. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/__init__.py +0 -0
  10. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/_avro.py +0 -0
  11. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/activity.py +0 -0
  12. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/auth_composition.py +0 -0
  13. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/client.py +0 -0
  14. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/errors.py +0 -0
  15. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/external_storage.py +0 -0
  16. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/external_task_input.py +0 -0
  17. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/external_task_result.py +0 -0
  18. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/history_bundle_verify.py +0 -0
  19. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/interceptors.py +0 -0
  20. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/invocable.py +0 -0
  21. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/metrics.py +0 -0
  22. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/py.typed +0 -0
  23. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/replay_verify.py +0 -0
  24. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/retry_policy.py +0 -0
  25. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/serializer.py +0 -0
  26. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/sync.py +0 -0
  27. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/testing.py +0 -0
  28. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow/workflow.py +0 -0
  29. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  30. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  31. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  32. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow.egg-info/requires.txt +0 -0
  33. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/src/durable_workflow.egg-info/top_level.txt +0 -0
  34. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_activity_context.py +0 -0
  35. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_auth_composition.py +0 -0
  36. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_client.py +0 -0
  37. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_control_plane_parity_fixtures.py +0 -0
  38. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_errors.py +0 -0
  39. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_external_storage.py +0 -0
  40. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_external_task_input.py +0 -0
  41. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_external_task_result.py +0 -0
  42. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_golden_history_replay.py +0 -0
  43. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_history_bundle_verify.py +0 -0
  44. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_history_event_contract.py +0 -0
  45. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_invocable.py +0 -0
  46. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_metrics.py +0 -0
  47. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_order_processing_example.py +0 -0
  48. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_public_boundary_scanner.py +0 -0
  49. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_queries.py +0 -0
  50. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_readme_quickstart.py +0 -0
  51. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_replay.py +0 -0
  52. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_replay_verify.py +0 -0
  53. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_retry_policy.py +0 -0
  54. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_schedules.py +0 -0
  55. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_serializer.py +0 -0
  56. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_signals.py +0 -0
  57. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_sleep.py +0 -0
  58. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_standalone_activity_client.py +0 -0
  59. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_sync.py +0 -0
  60. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_testing_harness.py +0 -0
  61. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_updates.py +0 -0
  62. {durable_workflow-0.4.44 → durable_workflow-0.4.45}/tests/test_wait_condition.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.44
3
+ Version: 0.4.45
4
4
  Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
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 = "0.4.44"
7
+ version = "0.4.45"
8
8
  description = "Python SDK for the Durable Workflow server (language-neutral HTTP protocol)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -775,6 +775,8 @@ class Worker:
775
775
  )
776
776
  except Exception as e:
777
777
  log.warning("failed to complete workflow update task %s: %s", task_id, e)
778
+ await self._fail_workflow_task_after_completion_error(task_id, attempt, e)
779
+ return None
778
780
  return [command]
779
781
 
780
782
  try:
@@ -847,8 +849,32 @@ class Worker:
847
849
  )
848
850
  except Exception as e:
849
851
  log.warning("failed to complete workflow task %s: %s", task_id, e)
852
+ await self._fail_workflow_task_after_completion_error(task_id, attempt, e)
853
+ return None
850
854
  return commands
851
855
 
856
+ async def _fail_workflow_task_after_completion_error(
857
+ self,
858
+ task_id: str,
859
+ attempt: int,
860
+ error: Exception,
861
+ ) -> None:
862
+ try:
863
+ await self.client.fail_workflow_task(
864
+ task_id=task_id,
865
+ lease_owner=self.worker_id,
866
+ workflow_task_attempt=attempt,
867
+ message=f"workflow task completion failed after commands were produced: {error}",
868
+ failure_type=type(error).__name__,
869
+ stack_trace=traceback.format_exc(),
870
+ )
871
+ except Exception as fail_error:
872
+ log.warning(
873
+ "failed to report workflow task %s completion failure: %s",
874
+ task_id,
875
+ fail_error,
876
+ )
877
+
852
878
  async def _run_activity_task(self, task: dict[str, Any]) -> str:
853
879
  task_id: str = task["task_id"]
854
880
  attempt_id: str = task.get("activity_attempt_id") or task.get("attempt_id", "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.44
3
+ Version: 0.4.45
4
4
  Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -501,6 +501,33 @@ class TestWorkflowTaskExecution:
501
501
  assert commands[0]["arguments"]["codec"] == "json"
502
502
  assert serializer.decode(commands[0]["arguments"]["blob"], codec="json") == ["hello"]
503
503
 
504
+ @pytest.mark.asyncio
505
+ async def test_workflow_task_completion_error_fails_task_for_fast_redispatch(
506
+ self, mock_client: AsyncMock
507
+ ) -> None:
508
+ mock_client.complete_workflow_task.side_effect = TimeoutError("completion timed out")
509
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
510
+ task = {
511
+ "task_id": "t-complete-timeout",
512
+ "workflow_type": "test-wf",
513
+ "workflow_task_attempt": 2,
514
+ "history_events": [],
515
+ "arguments": '["hello"]',
516
+ "payload_codec": "json",
517
+ }
518
+
519
+ result = await worker._run_workflow_task(task)
520
+
521
+ assert result is None
522
+ mock_client.complete_workflow_task.assert_awaited_once()
523
+ mock_client.fail_workflow_task.assert_awaited_once()
524
+ call_kwargs = mock_client.fail_workflow_task.await_args.kwargs
525
+ assert call_kwargs["task_id"] == "t-complete-timeout"
526
+ assert call_kwargs["workflow_task_attempt"] == 2
527
+ assert call_kwargs["lease_owner"] == worker.worker_id
528
+ assert call_kwargs["failure_type"] == "TimeoutError"
529
+ assert "completion timed out" in call_kwargs["message"]
530
+
504
531
  @pytest.mark.asyncio
505
532
  async def test_workflow_command_payload_warning_uses_client_policy(
506
533
  self, mock_client: AsyncMock, caplog: pytest.LogCaptureFixture
@@ -673,6 +700,44 @@ class TestWorkflowTaskExecution:
673
700
  ]
674
701
  mock_client.fail_workflow_task.assert_not_called()
675
702
 
703
+ @pytest.mark.asyncio
704
+ async def test_update_task_completion_error_fails_task_for_fast_redispatch(
705
+ self, mock_client: AsyncMock
706
+ ) -> None:
707
+ mock_client.complete_workflow_task.side_effect = TimeoutError("update completion timed out")
708
+ worker = Worker(mock_client, task_queue="q1", workflows=[UpdateWorkflow], activities=[])
709
+ task = {
710
+ "task_id": "t-update-timeout",
711
+ "workflow_type": "update-wf",
712
+ "workflow_task_attempt": 3,
713
+ "workflow_update_id": "upd-worker-1",
714
+ "workflow_wait_kind": "update",
715
+ "history_events": [
716
+ {
717
+ "event_type": "UpdateAccepted",
718
+ "payload": {
719
+ "update_id": "upd-worker-1",
720
+ "update_name": "increment",
721
+ "arguments": serializer.encode([6], codec="json"),
722
+ "payload_codec": "json",
723
+ },
724
+ },
725
+ ],
726
+ "arguments": "[]",
727
+ "payload_codec": "json",
728
+ }
729
+
730
+ result = await worker._run_workflow_task(task)
731
+
732
+ assert result is None
733
+ mock_client.complete_workflow_task.assert_awaited_once()
734
+ mock_client.fail_workflow_task.assert_awaited_once()
735
+ call_kwargs = mock_client.fail_workflow_task.await_args.kwargs
736
+ assert call_kwargs["task_id"] == "t-update-timeout"
737
+ assert call_kwargs["workflow_task_attempt"] == 3
738
+ assert call_kwargs["failure_type"] == "TimeoutError"
739
+ assert "update completion timed out" in call_kwargs["message"]
740
+
676
741
  @pytest.mark.asyncio
677
742
  async def test_query_task_executes_registered_query(self, mock_client: AsyncMock) -> None:
678
743
  worker = Worker(mock_client, task_queue="q1", workflows=[QueryWorkflow], activities=[])