edda-framework 0.8.0__tar.gz → 0.9.0__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.
- {edda_framework-0.8.0 → edda_framework-0.9.0}/PKG-INFO +10 -66
- {edda_framework-0.8.0 → edda_framework-0.9.0}/README.md +9 -65
- {edda_framework-0.8.0 → edda_framework-0.9.0}/demo_app.py +6 -10
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/messages.md +74 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/workflows-activities.md +4 -4
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/channels.py +34 -9
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/context.py +28 -1
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/storage/models.py +0 -23
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/storage/protocol.py +18 -36
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/storage/sqlalchemy_storage.py +110 -226
- {edda_framework-0.8.0 → edda_framework-0.9.0}/pyproject.toml +5 -1
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/conftest.py +0 -4
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_auto_migration.py +0 -16
- edda_framework-0.9.0/tests/test_channel_transactional.py +351 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_message_delivery_lock.py +16 -6
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_multidb_storage.py +19 -7
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_recur_cleanup.py +31 -12
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_storage.py +21 -9
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_workflow_cancellation.py +24 -12
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_workflow_resumption.py +9 -3
- {edda_framework-0.8.0 → edda_framework-0.9.0}/uv.lock +1 -1
- {edda_framework-0.8.0 → edda_framework-0.9.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/.gitignore +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/.python-version +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/Justfile +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/LICENSE +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/api/reference.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/examples/events.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/index.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/integrations/mcp.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/integrations/opentelemetry.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/integrations/pydantic-rpc.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/detail-page-match-case.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/activity.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/app.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/compensation.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/exceptions.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/hooks.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/integrations/mcp/decorators.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/integrations/mcp/server.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/integrations/opentelemetry/hooks.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/locking.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/replay.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/retry.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/viewer_ui/theme.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/workflow.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/edda/wsgi.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/long_running_loop.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/mcp/prompts_example.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/message_passing.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/observability_with_opentelemetry.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/pydantic_rpc_integration.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/retry_example.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_cancel.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_integration.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_prompts.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_server.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_activity.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_app.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_channel_competing.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_context.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_events.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_instance_id_routing.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_locking.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_message_cleanup.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_messages.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_recur.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_replay.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_viewer_pagination.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/viewer_app.py +0 -0
- {edda_framework-0.8.0 → edda_framework-0.9.0}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Lightweight Durable Execution Framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/i2y/edda
|
|
6
6
|
Project-URL: Documentation, https://github.com/i2y/edda#readme
|
|
@@ -65,6 +65,7 @@ Description-Content-Type: text/markdown
|
|
|
65
65
|
[](https://opensource.org/licenses/MIT)
|
|
66
66
|
[](https://www.python.org/downloads/)
|
|
67
67
|
[](https://i2y.github.io/edda/)
|
|
68
|
+
[](https://deepwiki.com/i2y/edda)
|
|
68
69
|
|
|
69
70
|
## Overview
|
|
70
71
|
|
|
@@ -665,72 +666,12 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
|
665
666
|
|
|
666
667
|
**For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
|
|
667
668
|
|
|
668
|
-
###
|
|
669
|
+
### Channel-based Messaging
|
|
669
670
|
|
|
670
|
-
Edda provides
|
|
671
|
+
Edda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:
|
|
671
672
|
|
|
672
673
|
```python
|
|
673
|
-
from edda import workflow,
|
|
674
|
-
|
|
675
|
-
# Receiver workflow - waits for approval message
|
|
676
|
-
@workflow
|
|
677
|
-
async def approval_workflow(ctx: WorkflowContext, request_id: str):
|
|
678
|
-
# Wait for message on "approval" channel
|
|
679
|
-
msg = await wait_message(ctx, channel="approval")
|
|
680
|
-
|
|
681
|
-
if msg.data["approved"]:
|
|
682
|
-
return {"status": "approved", "approver": msg.data["approver"]}
|
|
683
|
-
return {"status": "rejected"}
|
|
684
|
-
|
|
685
|
-
# Sender workflow - sends approval decision
|
|
686
|
-
@workflow
|
|
687
|
-
async def manager_workflow(ctx: WorkflowContext, request_id: str):
|
|
688
|
-
# Review and make decision
|
|
689
|
-
decision = await review_request(ctx, request_id)
|
|
690
|
-
|
|
691
|
-
# Send message to waiting workflow
|
|
692
|
-
await send_message_to(
|
|
693
|
-
ctx,
|
|
694
|
-
target_instance_id=request_id,
|
|
695
|
-
channel="approval",
|
|
696
|
-
data={"approved": decision, "approver": "manager-123"},
|
|
697
|
-
)
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
**Group Communication (Erlang pg style)** - for fan-out messaging without knowing receiver instance IDs:
|
|
701
|
-
|
|
702
|
-
```python
|
|
703
|
-
from edda import workflow, join_group, wait_message, publish_to_group
|
|
704
|
-
|
|
705
|
-
# Receiver workflow - joins a group and listens
|
|
706
|
-
@workflow
|
|
707
|
-
async def notification_service(ctx: WorkflowContext, service_id: str):
|
|
708
|
-
# Join group at startup (loose coupling - sender doesn't need to know us)
|
|
709
|
-
await join_group(ctx, group="order_watchers")
|
|
710
|
-
|
|
711
|
-
while True:
|
|
712
|
-
msg = await wait_message(ctx, channel="order.created")
|
|
713
|
-
await send_notification(ctx, msg.data)
|
|
714
|
-
|
|
715
|
-
# Sender workflow - publishes to all group members
|
|
716
|
-
@workflow
|
|
717
|
-
async def order_processor(ctx: WorkflowContext, order_id: str):
|
|
718
|
-
result = await process_order(ctx, order_id)
|
|
719
|
-
|
|
720
|
-
# Broadcast to all watchers (doesn't need to know instance IDs)
|
|
721
|
-
count = await publish_to_group(
|
|
722
|
-
ctx,
|
|
723
|
-
group="order_watchers",
|
|
724
|
-
channel="order.created",
|
|
725
|
-
data={"order_id": order_id, "status": "completed"},
|
|
726
|
-
)
|
|
727
|
-
print(f"Notified {count} watchers")
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
**Channel API with Delivery Modes** - subscribe to channels with explicit delivery semantics:
|
|
731
|
-
|
|
732
|
-
```python
|
|
733
|
-
from edda import workflow, subscribe, receive, publish, WorkflowContext
|
|
674
|
+
from edda import workflow, subscribe, receive, publish, send_to, WorkflowContext
|
|
734
675
|
|
|
735
676
|
# Job Worker - processes jobs exclusively (competing mode)
|
|
736
677
|
@workflow
|
|
@@ -754,8 +695,11 @@ async def notification_handler(ctx: WorkflowContext, handler_id: str):
|
|
|
754
695
|
await send_notification(ctx, msg.data)
|
|
755
696
|
await ctx.recur(handler_id)
|
|
756
697
|
|
|
757
|
-
#
|
|
698
|
+
# Publish to channel (all subscribers or one competing subscriber)
|
|
758
699
|
await publish(ctx, channel="jobs", data={"task": "send_report"})
|
|
700
|
+
|
|
701
|
+
# Direct message to specific workflow instance
|
|
702
|
+
await send_to(ctx, instance_id="workflow-123", channel="approval", data={"approved": True})
|
|
759
703
|
```
|
|
760
704
|
|
|
761
705
|
**Delivery modes**:
|
|
@@ -765,7 +709,7 @@ await publish(ctx, channel="jobs", data={"task": "send_report"})
|
|
|
765
709
|
**Key features**:
|
|
766
710
|
- **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels
|
|
767
711
|
- **Competing vs Broadcast**: Choose semantics per subscription
|
|
768
|
-
- **
|
|
712
|
+
- **Direct messaging**: `send_to()` for workflow-to-workflow communication
|
|
769
713
|
- **Database-backed**: All messages are persisted for durability
|
|
770
714
|
- **Lock-first delivery**: Safe for multi-worker environments
|
|
771
715
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://www.python.org/downloads/)
|
|
9
9
|
[](https://i2y.github.io/edda/)
|
|
10
|
+
[](https://deepwiki.com/i2y/edda)
|
|
10
11
|
|
|
11
12
|
## Overview
|
|
12
13
|
|
|
@@ -607,72 +608,12 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
|
607
608
|
|
|
608
609
|
**For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
|
|
609
610
|
|
|
610
|
-
###
|
|
611
|
+
### Channel-based Messaging
|
|
611
612
|
|
|
612
|
-
Edda provides
|
|
613
|
+
Edda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:
|
|
613
614
|
|
|
614
615
|
```python
|
|
615
|
-
from edda import workflow,
|
|
616
|
-
|
|
617
|
-
# Receiver workflow - waits for approval message
|
|
618
|
-
@workflow
|
|
619
|
-
async def approval_workflow(ctx: WorkflowContext, request_id: str):
|
|
620
|
-
# Wait for message on "approval" channel
|
|
621
|
-
msg = await wait_message(ctx, channel="approval")
|
|
622
|
-
|
|
623
|
-
if msg.data["approved"]:
|
|
624
|
-
return {"status": "approved", "approver": msg.data["approver"]}
|
|
625
|
-
return {"status": "rejected"}
|
|
626
|
-
|
|
627
|
-
# Sender workflow - sends approval decision
|
|
628
|
-
@workflow
|
|
629
|
-
async def manager_workflow(ctx: WorkflowContext, request_id: str):
|
|
630
|
-
# Review and make decision
|
|
631
|
-
decision = await review_request(ctx, request_id)
|
|
632
|
-
|
|
633
|
-
# Send message to waiting workflow
|
|
634
|
-
await send_message_to(
|
|
635
|
-
ctx,
|
|
636
|
-
target_instance_id=request_id,
|
|
637
|
-
channel="approval",
|
|
638
|
-
data={"approved": decision, "approver": "manager-123"},
|
|
639
|
-
)
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
**Group Communication (Erlang pg style)** - for fan-out messaging without knowing receiver instance IDs:
|
|
643
|
-
|
|
644
|
-
```python
|
|
645
|
-
from edda import workflow, join_group, wait_message, publish_to_group
|
|
646
|
-
|
|
647
|
-
# Receiver workflow - joins a group and listens
|
|
648
|
-
@workflow
|
|
649
|
-
async def notification_service(ctx: WorkflowContext, service_id: str):
|
|
650
|
-
# Join group at startup (loose coupling - sender doesn't need to know us)
|
|
651
|
-
await join_group(ctx, group="order_watchers")
|
|
652
|
-
|
|
653
|
-
while True:
|
|
654
|
-
msg = await wait_message(ctx, channel="order.created")
|
|
655
|
-
await send_notification(ctx, msg.data)
|
|
656
|
-
|
|
657
|
-
# Sender workflow - publishes to all group members
|
|
658
|
-
@workflow
|
|
659
|
-
async def order_processor(ctx: WorkflowContext, order_id: str):
|
|
660
|
-
result = await process_order(ctx, order_id)
|
|
661
|
-
|
|
662
|
-
# Broadcast to all watchers (doesn't need to know instance IDs)
|
|
663
|
-
count = await publish_to_group(
|
|
664
|
-
ctx,
|
|
665
|
-
group="order_watchers",
|
|
666
|
-
channel="order.created",
|
|
667
|
-
data={"order_id": order_id, "status": "completed"},
|
|
668
|
-
)
|
|
669
|
-
print(f"Notified {count} watchers")
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
**Channel API with Delivery Modes** - subscribe to channels with explicit delivery semantics:
|
|
673
|
-
|
|
674
|
-
```python
|
|
675
|
-
from edda import workflow, subscribe, receive, publish, WorkflowContext
|
|
616
|
+
from edda import workflow, subscribe, receive, publish, send_to, WorkflowContext
|
|
676
617
|
|
|
677
618
|
# Job Worker - processes jobs exclusively (competing mode)
|
|
678
619
|
@workflow
|
|
@@ -696,8 +637,11 @@ async def notification_handler(ctx: WorkflowContext, handler_id: str):
|
|
|
696
637
|
await send_notification(ctx, msg.data)
|
|
697
638
|
await ctx.recur(handler_id)
|
|
698
639
|
|
|
699
|
-
#
|
|
640
|
+
# Publish to channel (all subscribers or one competing subscriber)
|
|
700
641
|
await publish(ctx, channel="jobs", data={"task": "send_report"})
|
|
642
|
+
|
|
643
|
+
# Direct message to specific workflow instance
|
|
644
|
+
await send_to(ctx, instance_id="workflow-123", channel="approval", data={"approved": True})
|
|
701
645
|
```
|
|
702
646
|
|
|
703
647
|
**Delivery modes**:
|
|
@@ -707,7 +651,7 @@ await publish(ctx, channel="jobs", data={"task": "send_report"})
|
|
|
707
651
|
**Key features**:
|
|
708
652
|
- **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels
|
|
709
653
|
- **Competing vs Broadcast**: Choose semantics per subscription
|
|
710
|
-
- **
|
|
654
|
+
- **Direct messaging**: `send_to()` for workflow-to-workflow communication
|
|
711
655
|
- **Database-backed**: All messages are persisted for durability
|
|
712
656
|
- **Lock-first delivery**: Safe for multi-worker environments
|
|
713
657
|
|
|
@@ -1953,19 +1953,17 @@ async def job_worker_workflow(
|
|
|
1953
1953
|
- Start this workflow multiple times with different worker_id values
|
|
1954
1954
|
- Each worker will subscribe to the "jobs" channel in competing mode
|
|
1955
1955
|
|
|
1956
|
-
2. Publish jobs
|
|
1956
|
+
2. Publish jobs using the job_publisher_workflow:
|
|
1957
1957
|
curl -X POST http://localhost:8001/ \\
|
|
1958
1958
|
-H "Content-Type: application/cloudevents+json" \\
|
|
1959
1959
|
-d '{
|
|
1960
1960
|
"specversion": "1.0",
|
|
1961
|
-
"type": "
|
|
1961
|
+
"type": "job_publisher_workflow",
|
|
1962
1962
|
"source": "demo-client",
|
|
1963
1963
|
"id": "job-1",
|
|
1964
1964
|
"datacontenttype": "application/json",
|
|
1965
1965
|
"data": {
|
|
1966
|
-
"
|
|
1967
|
-
"task": "send_report",
|
|
1968
|
-
"user_id": 123
|
|
1966
|
+
"task": "send_report"
|
|
1969
1967
|
}
|
|
1970
1968
|
}'
|
|
1971
1969
|
|
|
@@ -2043,19 +2041,17 @@ async def notification_service_workflow(
|
|
|
2043
2041
|
- Start this workflow multiple times with different service_id values
|
|
2044
2042
|
- Each instance will subscribe to the "notifications" channel
|
|
2045
2043
|
|
|
2046
|
-
2. Publish a notification:
|
|
2044
|
+
2. Publish a notification using the notification_publisher_workflow:
|
|
2047
2045
|
curl -X POST http://localhost:8001/ \\
|
|
2048
2046
|
-H "Content-Type: application/cloudevents+json" \\
|
|
2049
2047
|
-d '{
|
|
2050
2048
|
"specversion": "1.0",
|
|
2051
|
-
"type": "
|
|
2049
|
+
"type": "notification_publisher_workflow",
|
|
2052
2050
|
"source": "demo-client",
|
|
2053
2051
|
"id": "notification-1",
|
|
2054
2052
|
"datacontenttype": "application/json",
|
|
2055
2053
|
"data": {
|
|
2056
|
-
"
|
|
2057
|
-
"message": "System maintenance scheduled",
|
|
2058
|
-
"priority": "high"
|
|
2054
|
+
"message": "System maintenance scheduled"
|
|
2059
2055
|
}
|
|
2060
2056
|
}'
|
|
2061
2057
|
|
|
@@ -496,6 +496,80 @@ async def temporary_subscriber(ctx: WorkflowContext):
|
|
|
496
496
|
# Continue with other work...
|
|
497
497
|
```
|
|
498
498
|
|
|
499
|
+
## Transactional Message Processing
|
|
500
|
+
|
|
501
|
+
When using channel-based messaging inside activities, both `publish()` and `receive()` participate in the activity's database transaction.
|
|
502
|
+
|
|
503
|
+
### Transactional Publish
|
|
504
|
+
|
|
505
|
+
When `publish()` is called inside an activity, the message is only published **after the transaction commits**:
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
@activity # transactional=True by default
|
|
509
|
+
async def process_order(ctx: WorkflowContext, order_id: str):
|
|
510
|
+
# Do some work...
|
|
511
|
+
result = await do_processing(order_id)
|
|
512
|
+
|
|
513
|
+
# Message is queued for post-commit delivery
|
|
514
|
+
await publish(ctx, "order.completed", {"order_id": order_id})
|
|
515
|
+
|
|
516
|
+
return result # Commit: message is now published
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Behavior:**
|
|
520
|
+
- If the activity **succeeds**: Message is published after commit
|
|
521
|
+
- If the activity **fails**: Message is **NOT** published (rollback)
|
|
522
|
+
|
|
523
|
+
This ensures that messages are only sent when the associated business logic succeeds.
|
|
524
|
+
|
|
525
|
+
### Transactional Receive
|
|
526
|
+
|
|
527
|
+
When `receive()` is called inside an activity, the message claim is part of the transaction:
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
@activity # transactional=True by default
|
|
531
|
+
async def process_job(ctx: WorkflowContext, channel: str):
|
|
532
|
+
msg = await receive(ctx, channel) # Claim is part of transaction
|
|
533
|
+
|
|
534
|
+
# Process the message...
|
|
535
|
+
result = await do_work(msg.data)
|
|
536
|
+
|
|
537
|
+
return result # Commit: claim is finalized
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Behavior:**
|
|
541
|
+
- If the activity **succeeds**: Message claim is committed, message is processed
|
|
542
|
+
- If the activity **fails**: Message claim is rolled back, message returns to queue
|
|
543
|
+
|
|
544
|
+
This provides **at-least-once delivery** semantics - if processing fails, the message will be redelivered to another subscriber.
|
|
545
|
+
|
|
546
|
+
### Recommended Pattern
|
|
547
|
+
|
|
548
|
+
For reliable message processing, wrap `receive()` calls inside activities:
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
@workflow
|
|
552
|
+
async def job_worker(ctx: WorkflowContext, worker_id: str):
|
|
553
|
+
await subscribe(ctx, "jobs", mode="competing")
|
|
554
|
+
|
|
555
|
+
while True:
|
|
556
|
+
# Process job inside activity for transactional guarantees
|
|
557
|
+
await process_job(ctx, "jobs", activity_id="process:1")
|
|
558
|
+
await ctx.recur(worker_id)
|
|
559
|
+
|
|
560
|
+
@activity
|
|
561
|
+
async def process_job(ctx: WorkflowContext, channel: str):
|
|
562
|
+
msg = await receive(ctx, channel) # Part of activity transaction
|
|
563
|
+
|
|
564
|
+
# Do work...
|
|
565
|
+
await execute_task(msg.data)
|
|
566
|
+
|
|
567
|
+
# Publish completion notification (also transactional)
|
|
568
|
+
await publish(ctx, "job.completed", {"job_id": msg.id})
|
|
569
|
+
|
|
570
|
+
return {"processed": msg.id}
|
|
571
|
+
```
|
|
572
|
+
|
|
499
573
|
## Performance Considerations
|
|
500
574
|
|
|
501
575
|
### Database-Backed Durability
|
|
@@ -764,10 +764,10 @@ In long-running loops, every activity adds an entry to the workflow history. Aft
|
|
|
764
764
|
# ❌ Problematic: History grows forever
|
|
765
765
|
@workflow
|
|
766
766
|
async def notification_service(ctx: WorkflowContext):
|
|
767
|
-
await
|
|
767
|
+
await subscribe(ctx, "order.completed", mode="broadcast")
|
|
768
768
|
|
|
769
769
|
while True:
|
|
770
|
-
msg = await
|
|
770
|
+
msg = await receive(ctx, "order.completed")
|
|
771
771
|
await send_notification(ctx, msg.data)
|
|
772
772
|
# After 10,000 iterations: 10,000+ history entries!
|
|
773
773
|
```
|
|
@@ -780,11 +780,11 @@ Use `ctx.recur()` to restart the workflow with fresh history while preserving st
|
|
|
780
780
|
# ✅ Good: Reset history periodically
|
|
781
781
|
@workflow
|
|
782
782
|
async def notification_service(ctx: WorkflowContext, processed_count: int = 0):
|
|
783
|
-
await
|
|
783
|
+
await subscribe(ctx, "order.completed", mode="broadcast")
|
|
784
784
|
|
|
785
785
|
count = 0
|
|
786
786
|
while True:
|
|
787
|
-
msg = await
|
|
787
|
+
msg = await receive(ctx, "order.completed")
|
|
788
788
|
await send_notification(ctx, msg.data, activity_id=f"notify:{msg.id}")
|
|
789
789
|
|
|
790
790
|
count += 1
|
|
@@ -445,15 +445,40 @@ async def publish(
|
|
|
445
445
|
message_id = await storage.publish_to_channel(channel, data, full_metadata)
|
|
446
446
|
|
|
447
447
|
# Wake up waiting subscribers
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
448
|
+
# If in a transaction, defer delivery until after commit to ensure atomicity
|
|
449
|
+
if storage.in_transaction():
|
|
450
|
+
# Capture current values for the closure
|
|
451
|
+
_storage = storage
|
|
452
|
+
_channel = channel
|
|
453
|
+
_message_id = message_id
|
|
454
|
+
_data = data
|
|
455
|
+
_metadata = full_metadata
|
|
456
|
+
_target_instance_id = target_instance_id
|
|
457
|
+
_worker_id = effective_worker_id
|
|
458
|
+
|
|
459
|
+
async def deferred_wake() -> None:
|
|
460
|
+
await _wake_waiting_subscribers(
|
|
461
|
+
_storage,
|
|
462
|
+
_channel,
|
|
463
|
+
_message_id,
|
|
464
|
+
_data,
|
|
465
|
+
_metadata,
|
|
466
|
+
target_instance_id=_target_instance_id,
|
|
467
|
+
worker_id=_worker_id,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
storage.register_post_commit_callback(deferred_wake)
|
|
471
|
+
else:
|
|
472
|
+
# Not in transaction - deliver immediately
|
|
473
|
+
await _wake_waiting_subscribers(
|
|
474
|
+
storage,
|
|
475
|
+
channel,
|
|
476
|
+
message_id,
|
|
477
|
+
data,
|
|
478
|
+
full_metadata,
|
|
479
|
+
target_instance_id=target_instance_id,
|
|
480
|
+
worker_id=effective_worker_id,
|
|
481
|
+
)
|
|
457
482
|
|
|
458
483
|
return message_id
|
|
459
484
|
|
|
@@ -5,7 +5,7 @@ This module provides the WorkflowContext class for workflow execution,
|
|
|
5
5
|
managing state, history, and replay during workflow execution.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from collections.abc import AsyncIterator
|
|
8
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
10
|
from typing import TYPE_CHECKING, Any, cast
|
|
11
11
|
|
|
@@ -451,6 +451,33 @@ class WorkflowContext:
|
|
|
451
451
|
"""
|
|
452
452
|
return self.storage.in_transaction()
|
|
453
453
|
|
|
454
|
+
def register_post_commit(self, callback: Callable[[], Awaitable[None]]) -> None:
|
|
455
|
+
"""
|
|
456
|
+
Register a callback to be executed after the current transaction commits.
|
|
457
|
+
|
|
458
|
+
The callback will be executed after the top-level transaction commits successfully.
|
|
459
|
+
If the transaction is rolled back, the callback will NOT be executed.
|
|
460
|
+
This is useful for deferring side effects (like message delivery) until after
|
|
461
|
+
the transaction has been committed.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
callback: An async function to call after commit.
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
RuntimeError: If not in a transaction.
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
async with ctx.transaction():
|
|
471
|
+
# Save order to database
|
|
472
|
+
await ctx.storage.append_history(...)
|
|
473
|
+
|
|
474
|
+
# Defer message delivery until after commit
|
|
475
|
+
async def deliver_notifications():
|
|
476
|
+
await notify_subscribers(order_id)
|
|
477
|
+
ctx.register_post_commit(deliver_notifications)
|
|
478
|
+
"""
|
|
479
|
+
self.storage.register_post_commit_callback(callback)
|
|
480
|
+
|
|
454
481
|
async def recur(self, **kwargs: Any) -> None:
|
|
455
482
|
"""
|
|
456
483
|
Restart the workflow with fresh history (Erlang-style tail recursion).
|
|
@@ -136,27 +136,6 @@ WORKFLOW_TIMER_SUBSCRIPTIONS_INDEXES = [
|
|
|
136
136
|
"CREATE INDEX IF NOT EXISTS idx_timer_subscriptions_instance ON workflow_timer_subscriptions(instance_id);",
|
|
137
137
|
]
|
|
138
138
|
|
|
139
|
-
# SQL schema for message subscriptions (for wait_message)
|
|
140
|
-
WORKFLOW_MESSAGE_SUBSCRIPTIONS_TABLE = """
|
|
141
|
-
CREATE TABLE IF NOT EXISTS workflow_message_subscriptions (
|
|
142
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
143
|
-
instance_id TEXT NOT NULL,
|
|
144
|
-
channel TEXT NOT NULL,
|
|
145
|
-
activity_id TEXT,
|
|
146
|
-
timeout_at TEXT,
|
|
147
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
148
|
-
FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
|
|
149
|
-
CONSTRAINT unique_instance_channel UNIQUE (instance_id, channel)
|
|
150
|
-
);
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
# Indexes for message subscriptions
|
|
154
|
-
WORKFLOW_MESSAGE_SUBSCRIPTIONS_INDEXES = [
|
|
155
|
-
"CREATE INDEX IF NOT EXISTS idx_message_subscriptions_channel ON workflow_message_subscriptions(channel);",
|
|
156
|
-
"CREATE INDEX IF NOT EXISTS idx_message_subscriptions_timeout ON workflow_message_subscriptions(timeout_at);",
|
|
157
|
-
"CREATE INDEX IF NOT EXISTS idx_message_subscriptions_instance ON workflow_message_subscriptions(instance_id);",
|
|
158
|
-
]
|
|
159
|
-
|
|
160
139
|
# SQL schema for group memberships (Erlang pg style)
|
|
161
140
|
WORKFLOW_GROUP_MEMBERSHIPS_TABLE = """
|
|
162
141
|
CREATE TABLE IF NOT EXISTS workflow_group_memberships (
|
|
@@ -306,7 +285,6 @@ ALL_TABLES = [
|
|
|
306
285
|
WORKFLOW_HISTORY_ARCHIVE_TABLE,
|
|
307
286
|
WORKFLOW_COMPENSATIONS_TABLE,
|
|
308
287
|
WORKFLOW_TIMER_SUBSCRIPTIONS_TABLE,
|
|
309
|
-
WORKFLOW_MESSAGE_SUBSCRIPTIONS_TABLE,
|
|
310
288
|
WORKFLOW_GROUP_MEMBERSHIPS_TABLE,
|
|
311
289
|
OUTBOX_EVENTS_TABLE,
|
|
312
290
|
# Channel-based Message Queue System
|
|
@@ -324,7 +302,6 @@ ALL_INDEXES = (
|
|
|
324
302
|
+ WORKFLOW_HISTORY_ARCHIVE_INDEXES
|
|
325
303
|
+ WORKFLOW_COMPENSATIONS_INDEXES
|
|
326
304
|
+ WORKFLOW_TIMER_SUBSCRIPTIONS_INDEXES
|
|
327
|
-
+ WORKFLOW_MESSAGE_SUBSCRIPTIONS_INDEXES
|
|
328
305
|
+ WORKFLOW_GROUP_MEMBERSHIPS_INDEXES
|
|
329
306
|
+ OUTBOX_EVENTS_INDEXES
|
|
330
307
|
# Channel-based Message Queue System
|
|
@@ -5,6 +5,7 @@ This module defines the StorageProtocol using Python's structural typing (Protoc
|
|
|
5
5
|
Any storage implementation that conforms to this protocol can be used with Edda.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
10
11
|
|
|
@@ -104,6 +105,21 @@ class StorageProtocol(Protocol):
|
|
|
104
105
|
"""
|
|
105
106
|
...
|
|
106
107
|
|
|
108
|
+
def register_post_commit_callback(self, callback: Callable[[], Awaitable[None]]) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Register a callback to be executed after the current transaction commits.
|
|
111
|
+
|
|
112
|
+
The callback will be executed after the top-level transaction commits successfully.
|
|
113
|
+
If the transaction is rolled back, the callback will NOT be executed.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
callback: An async function to call after commit.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
RuntimeError: If not in a transaction.
|
|
120
|
+
"""
|
|
121
|
+
...
|
|
122
|
+
|
|
107
123
|
# -------------------------------------------------------------------------
|
|
108
124
|
# Workflow Definition Methods
|
|
109
125
|
# -------------------------------------------------------------------------
|
|
@@ -713,39 +729,6 @@ class StorageProtocol(Protocol):
|
|
|
713
729
|
# Message Subscription Methods (for wait_message)
|
|
714
730
|
# -------------------------------------------------------------------------
|
|
715
731
|
|
|
716
|
-
async def register_message_subscription_and_release_lock(
|
|
717
|
-
self,
|
|
718
|
-
instance_id: str,
|
|
719
|
-
worker_id: str,
|
|
720
|
-
channel: str,
|
|
721
|
-
timeout_at: datetime | None = None,
|
|
722
|
-
activity_id: str | None = None,
|
|
723
|
-
) -> None:
|
|
724
|
-
"""
|
|
725
|
-
Atomically register message subscription and release workflow lock.
|
|
726
|
-
|
|
727
|
-
This method performs the following operations in a SINGLE database transaction:
|
|
728
|
-
1. Register message subscription (INSERT into workflow_message_subscriptions)
|
|
729
|
-
2. Update current activity (UPDATE workflow_instances.current_activity_id)
|
|
730
|
-
3. Update status to 'waiting_for_event'
|
|
731
|
-
4. Release lock (UPDATE workflow_instances set locked_by=NULL)
|
|
732
|
-
|
|
733
|
-
This ensures that when a workflow calls wait_message(), the subscription is
|
|
734
|
-
registered and the lock is released atomically, preventing race conditions
|
|
735
|
-
in distributed environments (distributed coroutines pattern).
|
|
736
|
-
|
|
737
|
-
Args:
|
|
738
|
-
instance_id: Workflow instance ID
|
|
739
|
-
worker_id: Worker ID that currently holds the lock
|
|
740
|
-
channel: Channel name to wait on
|
|
741
|
-
timeout_at: Optional timeout timestamp
|
|
742
|
-
activity_id: Current activity ID to record
|
|
743
|
-
|
|
744
|
-
Raises:
|
|
745
|
-
RuntimeError: If the worker doesn't hold the lock (sanity check)
|
|
746
|
-
"""
|
|
747
|
-
...
|
|
748
|
-
|
|
749
732
|
async def find_waiting_instances_by_channel(
|
|
750
733
|
self,
|
|
751
734
|
channel: str,
|
|
@@ -911,9 +894,8 @@ class StorageProtocol(Protocol):
|
|
|
911
894
|
|
|
912
895
|
Removes entries from:
|
|
913
896
|
- workflow_timer_subscriptions
|
|
914
|
-
-
|
|
915
|
-
-
|
|
916
|
-
- channel_message_claims (new)
|
|
897
|
+
- channel_subscriptions
|
|
898
|
+
- channel_message_claims
|
|
917
899
|
|
|
918
900
|
Args:
|
|
919
901
|
instance_id: Workflow instance ID to clean up
|