edda-framework 0.7.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.7.0 → edda_framework-0.9.0}/Justfile +110 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/PKG-INFO +109 -9
- {edda_framework-0.7.0 → edda_framework-0.9.0}/README.md +108 -8
- {edda_framework-0.7.0 → edda_framework-0.9.0}/demo_app.py +422 -36
- edda_framework-0.9.0/docs/api/reference.md +143 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/durable-execution/replay.md +1 -1
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/events/wait-event.md +17 -10
- edda_framework-0.9.0/docs/core-features/messages.md +590 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/retry.md +5 -3
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/workflows-activities.md +175 -2
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/events.md +8 -8
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/first-workflow.md +1 -1
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/index.md +6 -4
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/integrations/mcp.md +87 -1
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/integrations/opentelemetry.md +1 -1
- edda_framework-0.9.0/docs/integrations/pydantic-rpc.md +193 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/__init__.py +39 -5
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/app.py +383 -223
- edda_framework-0.9.0/edda/channels.py +1017 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/compensation.py +22 -22
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/context.py +105 -52
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/opentelemetry/hooks.py +7 -2
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/locking.py +130 -67
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/replay.py +312 -82
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/storage/models.py +142 -24
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/storage/protocol.py +539 -118
- edda_framework-0.9.0/edda/storage/sqlalchemy_storage.py +3447 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/app.py +6 -1
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/data_service.py +19 -22
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/workflow.py +43 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/cancellable_workflow.py +3 -4
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/event_waiting_app.py +9 -9
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/event_waiting_workflow.py +1 -1
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/event_waiting_workflow_complete.py +3 -3
- edda_framework-0.9.0/examples/long_running_loop.py +274 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/order_processing_mcp.py +5 -5
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/prompts_example.py +5 -5
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/remote_server_example.py +3 -3
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/simple_mcp_server.py +1 -1
- edda_framework-0.9.0/examples/message_passing.py +263 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/observability_with_logfire.py +6 -6
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/observability_with_opentelemetry.py +3 -4
- edda_framework-0.9.0/examples/pydantic_rpc_integration.py +461 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/pydantic_saga.py +1 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/retry_example.py +18 -19
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/retry_with_compensation.py +29 -32
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/typeddict_example.py +0 -1
- {edda_framework-0.7.0 → edda_framework-0.9.0}/pyproject.toml +7 -2
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/conftest.py +43 -10
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_cancel.py +2 -2
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_server.py +4 -4
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_atomic_wait_event.py +80 -52
- edda_framework-0.9.0/tests/test_auto_migration.py +389 -0
- edda_framework-0.9.0/tests/test_channel_competing.py +432 -0
- edda_framework-0.9.0/tests/test_channel_transactional.py +351 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_distributed_event_delivery.py +28 -15
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_events.py +115 -57
- edda_framework-0.9.0/tests/test_instance_id_routing.py +366 -0
- edda_framework-0.9.0/tests/test_message_cleanup.py +198 -0
- edda_framework-0.9.0/tests/test_message_delivery_lock.py +313 -0
- edda_framework-0.9.0/tests/test_messages.py +477 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_multidb_storage.py +48 -17
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_events.py +34 -27
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_received_event.py +56 -38
- edda_framework-0.9.0/tests/test_recur.py +581 -0
- edda_framework-0.9.0/tests/test_recur_cleanup.py +329 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_stale_workflow_recovery.py +3 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_storage.py +54 -18
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_wait_timer.py +12 -12
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_workflow_cancellation.py +46 -22
- edda_framework-0.9.0/tests/test_workflow_resumption.py +259 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/uv.lock +195 -17
- {edda_framework-0.7.0 → edda_framework-0.9.0}/viewer_app.py +2 -3
- {edda_framework-0.7.0 → edda_framework-0.9.0}/zensical.toml +15 -0
- edda_framework-0.7.0/edda/events.py +0 -505
- edda_framework-0.7.0/edda/storage/sqlalchemy_storage.py +0 -1909
- {edda_framework-0.7.0 → edda_framework-0.9.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/.gitignore +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/.python-version +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/LICENSE +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/detail-page-match-case.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/activity.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/exceptions.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/hooks.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/mcp/decorators.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/mcp/server.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/retry.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/viewer_ui/theme.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/edda/wsgi.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_integration.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/mcp/test_prompts.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_activity.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_app.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_context.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_locking.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_replay.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_viewer_pagination.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.7.0 → edda_framework-0.9.0}/tests/test_workflow_auto_register.py +0 -0
|
@@ -31,18 +31,22 @@ test-file FILE:
|
|
|
31
31
|
|
|
32
32
|
# Format code with black
|
|
33
33
|
format:
|
|
34
|
+
@uv sync --extra dev --quiet
|
|
34
35
|
uv run black edda tests
|
|
35
36
|
|
|
36
37
|
# Check code formatting
|
|
37
38
|
format-check:
|
|
39
|
+
@uv sync --extra dev --quiet
|
|
38
40
|
uv run black --check edda tests
|
|
39
41
|
|
|
40
42
|
# Lint code with ruff
|
|
41
43
|
lint:
|
|
44
|
+
@uv sync --extra dev --quiet
|
|
42
45
|
uv run ruff check edda tests
|
|
43
46
|
|
|
44
47
|
# Type check with mypy
|
|
45
48
|
type-check:
|
|
49
|
+
@uv sync --extra dev --quiet
|
|
46
50
|
uv run mypy edda
|
|
47
51
|
|
|
48
52
|
# Run all checks (format, lint, type-check, test)
|
|
@@ -50,6 +54,7 @@ check: format-check lint type-check test
|
|
|
50
54
|
|
|
51
55
|
# Auto-fix issues (format + lint with auto-fix)
|
|
52
56
|
fix:
|
|
57
|
+
@uv sync --extra dev --quiet
|
|
53
58
|
uv run black edda tests
|
|
54
59
|
uv run ruff check --fix edda tests
|
|
55
60
|
|
|
@@ -116,6 +121,18 @@ viewer-demo DB='demo.db' PORT='8080':
|
|
|
116
121
|
just viewer {{DB}} {{PORT}} "--import-module demo_app"
|
|
117
122
|
|
|
118
123
|
|
|
124
|
+
# Build documentation (clears cache for fresh API reference)
|
|
125
|
+
docs:
|
|
126
|
+
rm -rf .cache site
|
|
127
|
+
uv run zensical build
|
|
128
|
+
|
|
129
|
+
# Serve documentation locally (clears cache for fresh API reference)
|
|
130
|
+
docs-serve:
|
|
131
|
+
@lsof -ti :8000 | xargs kill -15 2>/dev/null || true
|
|
132
|
+
@sleep 1
|
|
133
|
+
rm -rf .cache site
|
|
134
|
+
uv run zensical serve
|
|
135
|
+
|
|
119
136
|
# Clean build artifacts and caches
|
|
120
137
|
clean:
|
|
121
138
|
rm -rf .pytest_cache
|
|
@@ -123,9 +140,102 @@ clean:
|
|
|
123
140
|
rm -rf .coverage
|
|
124
141
|
rm -rf .mypy_cache
|
|
125
142
|
rm -rf .ruff_cache
|
|
143
|
+
rm -rf .cache
|
|
144
|
+
rm -rf site
|
|
126
145
|
rm -rf dist
|
|
127
146
|
rm -rf build
|
|
128
147
|
rm -rf *.egg-info
|
|
129
148
|
find . -type d -name __pycache__ -exec rm -rf {} +
|
|
130
149
|
find . -type f -name "*.pyc" -delete
|
|
131
150
|
rm -f demo.db
|
|
151
|
+
|
|
152
|
+
# Helper recipe for point-to-point test (uses bash for command substitution)
|
|
153
|
+
_test-point-to-point LOG_FILE:
|
|
154
|
+
#!/usr/bin/env bash
|
|
155
|
+
set -e
|
|
156
|
+
echo "=== Test 3: Point-to-Point Mode (Direct Message) ==="
|
|
157
|
+
echo "Starting receiver..."
|
|
158
|
+
curl -s -X POST http://localhost:8001/ \
|
|
159
|
+
-H "Content-Type: application/cloudevents+json" \
|
|
160
|
+
-d '{"specversion":"1.0","type":"direct_message_receiver_workflow","source":"test","id":"receiver-1","data":{"receiver_id":"receiver-1"}}' > /dev/null
|
|
161
|
+
sleep 2
|
|
162
|
+
# Parse instance_id from server log (format: [RECEIVER] Instance ID: workflow-uuid)
|
|
163
|
+
# Strip ANSI color codes before grepping
|
|
164
|
+
INSTANCE_ID=$(sed 's/\x1b\[[0-9;]*m//g' "{{LOG_FILE}}" | grep -o '\[RECEIVER\] Instance ID: [^ ]*' | tail -1 | cut -d' ' -f4)
|
|
165
|
+
echo "Receiver instance_id: $INSTANCE_ID"
|
|
166
|
+
if [ -z "$INSTANCE_ID" ]; then
|
|
167
|
+
echo "ERROR: Could not extract instance_id from server log"
|
|
168
|
+
echo "Log content (stripped):"
|
|
169
|
+
sed 's/\x1b\[[0-9;]*m//g' "{{LOG_FILE}}" | grep -i receiver || echo "(no receiver logs found)"
|
|
170
|
+
exit 1
|
|
171
|
+
fi
|
|
172
|
+
echo "Sending direct message to receiver..."
|
|
173
|
+
curl -s -X POST http://localhost:8001/ \
|
|
174
|
+
-H "Content-Type: application/cloudevents+json" \
|
|
175
|
+
-d "{\"specversion\":\"1.0\",\"type\":\"direct_message_sender_workflow\",\"source\":\"test\",\"id\":\"sender-1\",\"data\":{\"target_instance_id\":\"$INSTANCE_ID\",\"message\":\"Hello from sender!\"}}"
|
|
176
|
+
echo ""
|
|
177
|
+
sleep 5
|
|
178
|
+
# Verify receiver got the message (strip ANSI codes)
|
|
179
|
+
if sed 's/\x1b\[[0-9;]*m//g' "{{LOG_FILE}}" | grep -q "\[RECEIVER\] Received message:"; then
|
|
180
|
+
echo "✓ Point-to-Point message delivered successfully!"
|
|
181
|
+
else
|
|
182
|
+
echo "✗ Point-to-Point message delivery failed"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# Test message passing (competing, broadcast, and point-to-point modes)
|
|
186
|
+
test-messages:
|
|
187
|
+
#!/usr/bin/env bash
|
|
188
|
+
set -e
|
|
189
|
+
LOG_FILE=$(mktemp)
|
|
190
|
+
echo "=== Message Passing Test ==="
|
|
191
|
+
echo "Server log: $LOG_FILE"
|
|
192
|
+
echo "Starting demo app in background..."
|
|
193
|
+
rm -f demo.db
|
|
194
|
+
uv sync --extra server --quiet
|
|
195
|
+
PYTHONUNBUFFERED=1 uv run tsuno demo_app:application --bind 127.0.0.1:8001 --workers 1 > "$LOG_FILE" 2>&1 &
|
|
196
|
+
SERVER_PID=$!
|
|
197
|
+
sleep 2
|
|
198
|
+
|
|
199
|
+
echo ""
|
|
200
|
+
echo "=== Test 1: Competing Mode (Job Worker) ==="
|
|
201
|
+
echo "Starting worker..."
|
|
202
|
+
curl -s -X POST http://localhost:8001/ \
|
|
203
|
+
-H "Content-Type: application/cloudevents+json" \
|
|
204
|
+
-d '{"specversion":"1.0","type":"job_worker_workflow","source":"test","id":"worker-1","data":{"worker_id":"worker-1"}}'
|
|
205
|
+
echo ""
|
|
206
|
+
sleep 1
|
|
207
|
+
echo "Publishing job..."
|
|
208
|
+
curl -s -X POST http://localhost:8001/ \
|
|
209
|
+
-H "Content-Type: application/cloudevents+json" \
|
|
210
|
+
-d '{"specversion":"1.0","type":"job_publisher_workflow","source":"test","id":"job-1","data":{"task":"test-task"}}'
|
|
211
|
+
echo ""
|
|
212
|
+
sleep 2
|
|
213
|
+
|
|
214
|
+
echo ""
|
|
215
|
+
echo "=== Test 2: Broadcast Mode (Notification) ==="
|
|
216
|
+
echo "Starting notification service..."
|
|
217
|
+
curl -s -X POST http://localhost:8001/ \
|
|
218
|
+
-H "Content-Type: application/cloudevents+json" \
|
|
219
|
+
-d '{"specversion":"1.0","type":"notification_service_workflow","source":"test","id":"service-1","data":{"service_id":"service-1"}}'
|
|
220
|
+
echo ""
|
|
221
|
+
sleep 1
|
|
222
|
+
echo "Publishing notification..."
|
|
223
|
+
curl -s -X POST http://localhost:8001/ \
|
|
224
|
+
-H "Content-Type: application/cloudevents+json" \
|
|
225
|
+
-d '{"specversion":"1.0","type":"notification_publisher_workflow","source":"test","id":"notification-1","data":{"message":"Test notification"}}'
|
|
226
|
+
echo ""
|
|
227
|
+
sleep 2
|
|
228
|
+
|
|
229
|
+
just _test-point-to-point "$LOG_FILE"
|
|
230
|
+
|
|
231
|
+
echo ""
|
|
232
|
+
echo "=== Stopping demo app ==="
|
|
233
|
+
kill -15 $SERVER_PID 2>/dev/null || true
|
|
234
|
+
sleep 1
|
|
235
|
+
|
|
236
|
+
echo ""
|
|
237
|
+
echo "=== Server Log ==="
|
|
238
|
+
cat "$LOG_FILE"
|
|
239
|
+
rm -f "$LOG_FILE"
|
|
240
|
+
echo ""
|
|
241
|
+
echo "Done!"
|
|
@@ -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
|
|
|
@@ -85,6 +86,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
85
86
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
86
87
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
87
88
|
- ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
|
|
89
|
+
- 📬 **Channel-based Messaging**: Actor-model style communication with competing (job queue) and broadcast (fan-out) modes
|
|
88
90
|
- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
|
|
89
91
|
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
90
92
|
|
|
@@ -107,14 +109,14 @@ Edda's waiting functions make it ideal for time-based and event-driven business
|
|
|
107
109
|
- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
|
|
108
110
|
|
|
109
111
|
**Waiting functions**:
|
|
110
|
-
- `
|
|
111
|
-
- `
|
|
112
|
+
- `sleep(seconds)`: Wait for a relative duration
|
|
113
|
+
- `sleep_until(target_time)`: Wait until an absolute datetime (e.g., campaign end date)
|
|
112
114
|
- `wait_event(event_type)`: Wait for external events (near real-time response)
|
|
113
115
|
|
|
114
116
|
```python
|
|
115
117
|
@workflow
|
|
116
118
|
async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
|
|
117
|
-
await
|
|
119
|
+
await sleep(ctx, seconds=3*24*60*60) # Wait 3 days
|
|
118
120
|
if not await check_completed(ctx, user_id):
|
|
119
121
|
await send_reminder(ctx, user_id)
|
|
120
122
|
```
|
|
@@ -166,7 +168,7 @@ graph TB
|
|
|
166
168
|
|
|
167
169
|
- Multiple workers can run simultaneously across different pods/servers
|
|
168
170
|
- Each workflow instance runs on only one worker at a time (automatic coordination)
|
|
169
|
-
- `wait_event()` and `
|
|
171
|
+
- `wait_event()` and `sleep()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
|
|
170
172
|
- Automatic crash recovery with stale lock cleanup and workflow auto-resume
|
|
171
173
|
|
|
172
174
|
## Quick Start
|
|
@@ -486,7 +488,10 @@ Multiple workers can safely process workflows using database-based exclusive con
|
|
|
486
488
|
|
|
487
489
|
app = EddaApp(
|
|
488
490
|
db_url="postgresql://localhost/workflows", # Shared database for coordination
|
|
489
|
-
service_name="order-service"
|
|
491
|
+
service_name="order-service",
|
|
492
|
+
# Connection pool settings (optional)
|
|
493
|
+
pool_size=5, # Concurrent connections
|
|
494
|
+
max_overflow=10, # Additional burst capacity
|
|
490
495
|
)
|
|
491
496
|
```
|
|
492
497
|
|
|
@@ -614,10 +619,32 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str):
|
|
|
614
619
|
return payment_event.data
|
|
615
620
|
```
|
|
616
621
|
|
|
617
|
-
**
|
|
622
|
+
**ReceivedEvent attributes**: The `wait_event()` function returns a `ReceivedEvent` object:
|
|
623
|
+
|
|
624
|
+
```python
|
|
625
|
+
event = await wait_event(ctx, "payment.completed")
|
|
626
|
+
amount = event.data["amount"] # Event payload (dict or bytes)
|
|
627
|
+
source = event.metadata.source # CloudEvents source
|
|
628
|
+
event_type = event.metadata.type # CloudEvents type
|
|
629
|
+
extensions = event.extensions # CloudEvents extensions
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Timeout handling with EventTimeoutError**:
|
|
633
|
+
|
|
634
|
+
```python
|
|
635
|
+
from edda import wait_event, EventTimeoutError
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
event = await wait_event(ctx, "payment.completed", timeout_seconds=60)
|
|
639
|
+
except EventTimeoutError:
|
|
640
|
+
# Handle timeout (e.g., cancel order, send reminder)
|
|
641
|
+
await cancel_order(ctx, order_id)
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**sleep() for time-based waiting**:
|
|
618
645
|
|
|
619
646
|
```python
|
|
620
|
-
from edda import
|
|
647
|
+
from edda import sleep
|
|
621
648
|
|
|
622
649
|
@workflow
|
|
623
650
|
async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
@@ -625,7 +652,7 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
|
625
652
|
await create_order(ctx, order_id)
|
|
626
653
|
|
|
627
654
|
# Wait 60 seconds for payment
|
|
628
|
-
await
|
|
655
|
+
await sleep(ctx, seconds=60)
|
|
629
656
|
|
|
630
657
|
# Check payment status
|
|
631
658
|
return await check_payment(ctx, order_id)
|
|
@@ -639,6 +666,79 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
|
639
666
|
|
|
640
667
|
**For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
|
|
641
668
|
|
|
669
|
+
### Channel-based Messaging
|
|
670
|
+
|
|
671
|
+
Edda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:
|
|
672
|
+
|
|
673
|
+
```python
|
|
674
|
+
from edda import workflow, subscribe, receive, publish, send_to, WorkflowContext
|
|
675
|
+
|
|
676
|
+
# Job Worker - processes jobs exclusively (competing mode)
|
|
677
|
+
@workflow
|
|
678
|
+
async def job_worker(ctx: WorkflowContext, worker_id: str):
|
|
679
|
+
# Subscribe with competing mode - each job goes to ONE worker only
|
|
680
|
+
await subscribe(ctx, channel="jobs", mode="competing")
|
|
681
|
+
|
|
682
|
+
while True:
|
|
683
|
+
job = await receive(ctx, channel="jobs") # Get next job
|
|
684
|
+
await process_job(ctx, job.data)
|
|
685
|
+
await ctx.recur(worker_id) # Continue processing
|
|
686
|
+
|
|
687
|
+
# Notification Handler - receives ALL messages (broadcast mode)
|
|
688
|
+
@workflow
|
|
689
|
+
async def notification_handler(ctx: WorkflowContext, handler_id: str):
|
|
690
|
+
# Subscribe with broadcast mode - ALL handlers receive each message
|
|
691
|
+
await subscribe(ctx, channel="notifications", mode="broadcast")
|
|
692
|
+
|
|
693
|
+
while True:
|
|
694
|
+
msg = await receive(ctx, channel="notifications")
|
|
695
|
+
await send_notification(ctx, msg.data)
|
|
696
|
+
await ctx.recur(handler_id)
|
|
697
|
+
|
|
698
|
+
# Publish to channel (all subscribers or one competing subscriber)
|
|
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})
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Delivery modes**:
|
|
706
|
+
- **`competing`**: Each message goes to exactly ONE subscriber (job queue/task distribution)
|
|
707
|
+
- **`broadcast`**: Each message goes to ALL subscribers (notifications/fan-out)
|
|
708
|
+
|
|
709
|
+
**Key features**:
|
|
710
|
+
- **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels
|
|
711
|
+
- **Competing vs Broadcast**: Choose semantics per subscription
|
|
712
|
+
- **Direct messaging**: `send_to()` for workflow-to-workflow communication
|
|
713
|
+
- **Database-backed**: All messages are persisted for durability
|
|
714
|
+
- **Lock-first delivery**: Safe for multi-worker environments
|
|
715
|
+
|
|
716
|
+
### Workflow Recurrence
|
|
717
|
+
|
|
718
|
+
Long-running workflows can use `ctx.recur()` to restart with fresh history while maintaining the same instance ID. This is essential for workflows that run indefinitely (job workers, notification handlers, etc.):
|
|
719
|
+
|
|
720
|
+
```python
|
|
721
|
+
from edda import workflow, subscribe, receive, WorkflowContext
|
|
722
|
+
|
|
723
|
+
@workflow
|
|
724
|
+
async def job_worker(ctx: WorkflowContext, worker_id: str):
|
|
725
|
+
await subscribe(ctx, channel="jobs", mode="competing")
|
|
726
|
+
|
|
727
|
+
# Process one job
|
|
728
|
+
job = await receive(ctx, channel="jobs")
|
|
729
|
+
await process_job(ctx, job.data)
|
|
730
|
+
|
|
731
|
+
# Archive history and restart with same instance_id
|
|
732
|
+
# Prevents unbounded history growth
|
|
733
|
+
await ctx.recur(worker_id)
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Key benefits**:
|
|
737
|
+
- **Prevents history growth**: Archives old history, starts fresh
|
|
738
|
+
- **Maintains instance ID**: Same workflow continues logically
|
|
739
|
+
- **Preserves subscriptions**: Channel subscriptions survive recurrence
|
|
740
|
+
- **Enables infinite loops**: Essential for long-running workers
|
|
741
|
+
|
|
642
742
|
### ASGI Integration
|
|
643
743
|
|
|
644
744
|
Edda runs as an ASGI application:
|
|
@@ -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
|
|
|
@@ -27,6 +28,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
27
28
|
- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
|
|
28
29
|
- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
|
|
29
30
|
- ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
|
|
31
|
+
- 📬 **Channel-based Messaging**: Actor-model style communication with competing (job queue) and broadcast (fan-out) modes
|
|
30
32
|
- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
|
|
31
33
|
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
32
34
|
|
|
@@ -49,14 +51,14 @@ Edda's waiting functions make it ideal for time-based and event-driven business
|
|
|
49
51
|
- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
|
|
50
52
|
|
|
51
53
|
**Waiting functions**:
|
|
52
|
-
- `
|
|
53
|
-
- `
|
|
54
|
+
- `sleep(seconds)`: Wait for a relative duration
|
|
55
|
+
- `sleep_until(target_time)`: Wait until an absolute datetime (e.g., campaign end date)
|
|
54
56
|
- `wait_event(event_type)`: Wait for external events (near real-time response)
|
|
55
57
|
|
|
56
58
|
```python
|
|
57
59
|
@workflow
|
|
58
60
|
async def onboarding_reminder(ctx: WorkflowContext, user_id: str):
|
|
59
|
-
await
|
|
61
|
+
await sleep(ctx, seconds=3*24*60*60) # Wait 3 days
|
|
60
62
|
if not await check_completed(ctx, user_id):
|
|
61
63
|
await send_reminder(ctx, user_id)
|
|
62
64
|
```
|
|
@@ -108,7 +110,7 @@ graph TB
|
|
|
108
110
|
|
|
109
111
|
- Multiple workers can run simultaneously across different pods/servers
|
|
110
112
|
- Each workflow instance runs on only one worker at a time (automatic coordination)
|
|
111
|
-
- `wait_event()` and `
|
|
113
|
+
- `wait_event()` and `sleep()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
|
|
112
114
|
- Automatic crash recovery with stale lock cleanup and workflow auto-resume
|
|
113
115
|
|
|
114
116
|
## Quick Start
|
|
@@ -428,7 +430,10 @@ Multiple workers can safely process workflows using database-based exclusive con
|
|
|
428
430
|
|
|
429
431
|
app = EddaApp(
|
|
430
432
|
db_url="postgresql://localhost/workflows", # Shared database for coordination
|
|
431
|
-
service_name="order-service"
|
|
433
|
+
service_name="order-service",
|
|
434
|
+
# Connection pool settings (optional)
|
|
435
|
+
pool_size=5, # Concurrent connections
|
|
436
|
+
max_overflow=10, # Additional burst capacity
|
|
432
437
|
)
|
|
433
438
|
```
|
|
434
439
|
|
|
@@ -556,10 +561,32 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str):
|
|
|
556
561
|
return payment_event.data
|
|
557
562
|
```
|
|
558
563
|
|
|
559
|
-
**
|
|
564
|
+
**ReceivedEvent attributes**: The `wait_event()` function returns a `ReceivedEvent` object:
|
|
565
|
+
|
|
566
|
+
```python
|
|
567
|
+
event = await wait_event(ctx, "payment.completed")
|
|
568
|
+
amount = event.data["amount"] # Event payload (dict or bytes)
|
|
569
|
+
source = event.metadata.source # CloudEvents source
|
|
570
|
+
event_type = event.metadata.type # CloudEvents type
|
|
571
|
+
extensions = event.extensions # CloudEvents extensions
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Timeout handling with EventTimeoutError**:
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
from edda import wait_event, EventTimeoutError
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
event = await wait_event(ctx, "payment.completed", timeout_seconds=60)
|
|
581
|
+
except EventTimeoutError:
|
|
582
|
+
# Handle timeout (e.g., cancel order, send reminder)
|
|
583
|
+
await cancel_order(ctx, order_id)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**sleep() for time-based waiting**:
|
|
560
587
|
|
|
561
588
|
```python
|
|
562
|
-
from edda import
|
|
589
|
+
from edda import sleep
|
|
563
590
|
|
|
564
591
|
@workflow
|
|
565
592
|
async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
@@ -567,7 +594,7 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
|
567
594
|
await create_order(ctx, order_id)
|
|
568
595
|
|
|
569
596
|
# Wait 60 seconds for payment
|
|
570
|
-
await
|
|
597
|
+
await sleep(ctx, seconds=60)
|
|
571
598
|
|
|
572
599
|
# Check payment status
|
|
573
600
|
return await check_payment(ctx, order_id)
|
|
@@ -581,6 +608,79 @@ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
|
|
|
581
608
|
|
|
582
609
|
**For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
|
|
583
610
|
|
|
611
|
+
### Channel-based Messaging
|
|
612
|
+
|
|
613
|
+
Edda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:
|
|
614
|
+
|
|
615
|
+
```python
|
|
616
|
+
from edda import workflow, subscribe, receive, publish, send_to, WorkflowContext
|
|
617
|
+
|
|
618
|
+
# Job Worker - processes jobs exclusively (competing mode)
|
|
619
|
+
@workflow
|
|
620
|
+
async def job_worker(ctx: WorkflowContext, worker_id: str):
|
|
621
|
+
# Subscribe with competing mode - each job goes to ONE worker only
|
|
622
|
+
await subscribe(ctx, channel="jobs", mode="competing")
|
|
623
|
+
|
|
624
|
+
while True:
|
|
625
|
+
job = await receive(ctx, channel="jobs") # Get next job
|
|
626
|
+
await process_job(ctx, job.data)
|
|
627
|
+
await ctx.recur(worker_id) # Continue processing
|
|
628
|
+
|
|
629
|
+
# Notification Handler - receives ALL messages (broadcast mode)
|
|
630
|
+
@workflow
|
|
631
|
+
async def notification_handler(ctx: WorkflowContext, handler_id: str):
|
|
632
|
+
# Subscribe with broadcast mode - ALL handlers receive each message
|
|
633
|
+
await subscribe(ctx, channel="notifications", mode="broadcast")
|
|
634
|
+
|
|
635
|
+
while True:
|
|
636
|
+
msg = await receive(ctx, channel="notifications")
|
|
637
|
+
await send_notification(ctx, msg.data)
|
|
638
|
+
await ctx.recur(handler_id)
|
|
639
|
+
|
|
640
|
+
# Publish to channel (all subscribers or one competing subscriber)
|
|
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})
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Delivery modes**:
|
|
648
|
+
- **`competing`**: Each message goes to exactly ONE subscriber (job queue/task distribution)
|
|
649
|
+
- **`broadcast`**: Each message goes to ALL subscribers (notifications/fan-out)
|
|
650
|
+
|
|
651
|
+
**Key features**:
|
|
652
|
+
- **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels
|
|
653
|
+
- **Competing vs Broadcast**: Choose semantics per subscription
|
|
654
|
+
- **Direct messaging**: `send_to()` for workflow-to-workflow communication
|
|
655
|
+
- **Database-backed**: All messages are persisted for durability
|
|
656
|
+
- **Lock-first delivery**: Safe for multi-worker environments
|
|
657
|
+
|
|
658
|
+
### Workflow Recurrence
|
|
659
|
+
|
|
660
|
+
Long-running workflows can use `ctx.recur()` to restart with fresh history while maintaining the same instance ID. This is essential for workflows that run indefinitely (job workers, notification handlers, etc.):
|
|
661
|
+
|
|
662
|
+
```python
|
|
663
|
+
from edda import workflow, subscribe, receive, WorkflowContext
|
|
664
|
+
|
|
665
|
+
@workflow
|
|
666
|
+
async def job_worker(ctx: WorkflowContext, worker_id: str):
|
|
667
|
+
await subscribe(ctx, channel="jobs", mode="competing")
|
|
668
|
+
|
|
669
|
+
# Process one job
|
|
670
|
+
job = await receive(ctx, channel="jobs")
|
|
671
|
+
await process_job(ctx, job.data)
|
|
672
|
+
|
|
673
|
+
# Archive history and restart with same instance_id
|
|
674
|
+
# Prevents unbounded history growth
|
|
675
|
+
await ctx.recur(worker_id)
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**Key benefits**:
|
|
679
|
+
- **Prevents history growth**: Archives old history, starts fresh
|
|
680
|
+
- **Maintains instance ID**: Same workflow continues logically
|
|
681
|
+
- **Preserves subscriptions**: Channel subscriptions survive recurrence
|
|
682
|
+
- **Enables infinite loops**: Essential for long-running workers
|
|
683
|
+
|
|
584
684
|
### ASGI Integration
|
|
585
685
|
|
|
586
686
|
Edda runs as an ASGI application:
|