@adonis-agora/durable 0.49.1 → 0.50.0
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.
- package/CHANGELOG.md +251 -0
- package/README.md +53 -22
- package/dist/assets/spa/assets/index-DupjrK7N.js +16 -0
- package/dist/assets/spa/assets/index-DupjrK7N.js.map +1 -0
- package/dist/assets/spa/index.html +1 -1
- package/dist/commands/durable_worker.d.ts.map +1 -1
- package/dist/commands/durable_worker.js +9 -8
- package/dist/commands/durable_worker.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +1 -0
- package/dist/configure.js.map +1 -1
- package/dist/providers/dashboard_provider.d.ts +2 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +50 -6
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/providers/durable_provider.d.ts.map +1 -1
- package/dist/providers/durable_provider.js +143 -34
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/admission-redis/redis-admission-backend.d.ts.map +1 -1
- package/dist/src/admission-redis/redis-admission-backend.js +3 -1
- package/dist/src/admission-redis/redis-admission-backend.js.map +1 -1
- package/dist/src/ambient-step.d.ts +24 -0
- package/dist/src/ambient-step.d.ts.map +1 -0
- package/dist/src/ambient-step.js +42 -0
- package/dist/src/ambient-step.js.map +1 -0
- package/dist/src/base-workflow.d.ts +10 -1
- package/dist/src/base-workflow.d.ts.map +1 -1
- package/dist/src/base-workflow.js.map +1 -1
- package/dist/src/codec-state-store.d.ts +28 -2
- package/dist/src/codec-state-store.d.ts.map +1 -1
- package/dist/src/codec-state-store.js +61 -3
- package/dist/src/codec-state-store.js.map +1 -1
- package/dist/src/commands/worker.d.ts +4 -1
- package/dist/src/commands/worker.d.ts.map +1 -1
- package/dist/src/commands/worker.js +27 -3
- package/dist/src/commands/worker.js.map +1 -1
- package/dist/src/config_types.d.ts +6 -0
- package/dist/src/config_types.d.ts.map +1 -1
- package/dist/src/control-plane-redis/redis-control-plane.d.ts.map +1 -1
- package/dist/src/control-plane-redis/redis-control-plane.js +22 -6
- package/dist/src/control-plane-redis/redis-control-plane.js.map +1 -1
- package/dist/src/control-planes/factory.d.ts +6 -0
- package/dist/src/control-planes/factory.d.ts.map +1 -1
- package/dist/src/control-planes/factory.js +1 -0
- package/dist/src/control-planes/factory.js.map +1 -1
- package/dist/src/dashboard/gateway-adapter.d.ts +15 -5
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
- package/dist/src/dashboard/gateway-adapter.js +71 -7
- package/dist/src/dashboard/gateway-adapter.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts +53 -1
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +200 -20
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +2 -2
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +2 -2
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/dashboard/openapi.d.ts.map +1 -1
- package/dist/src/dashboard/openapi.js +71 -1
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/define_config.d.ts +122 -8
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/diagnostics-bridge.d.ts +16 -3
- package/dist/src/diagnostics-bridge.d.ts.map +1 -1
- package/dist/src/diagnostics-bridge.js +9 -3
- package/dist/src/diagnostics-bridge.js.map +1 -1
- package/dist/src/discovery-roots.d.ts +92 -0
- package/dist/src/discovery-roots.d.ts.map +1 -0
- package/dist/src/discovery-roots.js +150 -0
- package/dist/src/discovery-roots.js.map +1 -0
- package/dist/src/engine.d.ts +418 -20
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +1523 -159
- package/dist/src/engine.js.map +1 -1
- package/dist/src/entities.d.ts +36 -1
- package/dist/src/entities.d.ts.map +1 -1
- package/dist/src/entities.js +65 -4
- package/dist/src/entities.js.map +1 -1
- package/dist/src/errors.d.ts +36 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +44 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/event-trigger-bridge.d.ts +8 -0
- package/dist/src/event-trigger-bridge.d.ts.map +1 -1
- package/dist/src/event-trigger-bridge.js +24 -3
- package/dist/src/event-trigger-bridge.js.map +1 -1
- package/dist/src/handshake/announced.d.ts +27 -0
- package/dist/src/handshake/announced.d.ts.map +1 -1
- package/dist/src/handshake/announced.js +62 -0
- package/dist/src/handshake/announced.js.map +1 -1
- package/dist/src/handshake/directory.d.ts +92 -0
- package/dist/src/handshake/directory.d.ts.map +1 -0
- package/dist/src/handshake/directory.js +139 -0
- package/dist/src/handshake/directory.js.map +1 -0
- package/dist/src/hooks/barrel-sources.d.ts +28 -0
- package/dist/src/hooks/barrel-sources.d.ts.map +1 -0
- package/dist/src/hooks/barrel-sources.js +92 -0
- package/dist/src/hooks/barrel-sources.js.map +1 -0
- package/dist/src/hooks/steps.d.ts +12 -3
- package/dist/src/hooks/steps.d.ts.map +1 -1
- package/dist/src/hooks/steps.js +7 -11
- package/dist/src/hooks/steps.js.map +1 -1
- package/dist/src/hooks/workflows.d.ts +12 -3
- package/dist/src/hooks/workflows.d.ts.map +1 -1
- package/dist/src/hooks/workflows.js +7 -13
- package/dist/src/hooks/workflows.js.map +1 -1
- package/dist/src/index.d.ts +12 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +12 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +309 -21
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js +10 -0
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/metrics.d.ts.map +1 -1
- package/dist/src/metrics.js +5 -0
- package/dist/src/metrics.js.map +1 -1
- package/dist/src/namespaces.d.ts +17 -0
- package/dist/src/namespaces.d.ts.map +1 -0
- package/dist/src/namespaces.js +24 -0
- package/dist/src/namespaces.js.map +1 -0
- package/dist/src/otel/durable-metrics.d.ts.map +1 -1
- package/dist/src/otel/durable-metrics.js +5 -0
- package/dist/src/otel/durable-metrics.js.map +1 -1
- package/dist/src/otel/durable-otel.d.ts.map +1 -1
- package/dist/src/otel/durable-otel.js +5 -0
- package/dist/src/otel/durable-otel.js.map +1 -1
- package/dist/src/pollers.d.ts +10 -1
- package/dist/src/pollers.d.ts.map +1 -1
- package/dist/src/pollers.js +15 -5
- package/dist/src/pollers.js.map +1 -1
- package/dist/src/protocol.d.ts +12 -0
- package/dist/src/protocol.d.ts.map +1 -1
- package/dist/src/protocol.js +27 -1
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/remote-event.d.ts +6 -0
- package/dist/src/remote-event.d.ts.map +1 -0
- package/dist/src/remote-event.js +19 -0
- package/dist/src/remote-event.js.map +1 -0
- package/dist/src/remote-workflow-executor.d.ts +34 -12
- package/dist/src/remote-workflow-executor.d.ts.map +1 -1
- package/dist/src/remote-workflow-executor.js +42 -40
- package/dist/src/remote-workflow-executor.js.map +1 -1
- package/dist/src/run-detail-summary.d.ts +90 -3
- package/dist/src/run-detail-summary.d.ts.map +1 -1
- package/dist/src/run-detail-summary.js +360 -2
- package/dist/src/run-detail-summary.js.map +1 -1
- package/dist/src/run-facets.d.ts +48 -0
- package/dist/src/run-facets.d.ts.map +1 -0
- package/dist/src/run-facets.js +100 -0
- package/dist/src/run-facets.js.map +1 -0
- package/dist/src/run-gateway/interface.d.ts +27 -1
- package/dist/src/run-gateway/interface.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.d.ts +32 -4
- package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.js +101 -5
- package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/run-request-responder.d.ts +18 -5
- package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
- package/dist/src/run-gateway/run-request-responder.js +155 -32
- package/dist/src/run-gateway/run-request-responder.js.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.d.ts +23 -1
- package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.js +38 -0
- package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
- package/dist/src/search-attributes-schema.d.ts +44 -0
- package/dist/src/search-attributes-schema.d.ts.map +1 -0
- package/dist/src/search-attributes-schema.js +51 -0
- package/dist/src/search-attributes-schema.js.map +1 -0
- package/dist/src/singleton-admission.d.ts +75 -0
- package/dist/src/singleton-admission.d.ts.map +1 -0
- package/dist/src/singleton-admission.js +156 -0
- package/dist/src/singleton-admission.js.map +1 -0
- package/dist/src/singleton-gate.d.ts +23 -8
- package/dist/src/singleton-gate.d.ts.map +1 -1
- package/dist/src/singleton-gate.js +59 -12
- package/dist/src/singleton-gate.js.map +1 -1
- package/dist/src/standard-schema.d.ts +83 -0
- package/dist/src/standard-schema.d.ts.map +1 -0
- package/dist/src/standard-schema.js +31 -0
- package/dist/src/standard-schema.js.map +1 -0
- package/dist/src/step-discovery.d.ts +21 -11
- package/dist/src/step-discovery.d.ts.map +1 -1
- package/dist/src/step-discovery.js +86 -45
- package/dist/src/step-discovery.js.map +1 -1
- package/dist/src/step-ref.d.ts +38 -12
- package/dist/src/step-ref.d.ts.map +1 -1
- package/dist/src/step-ref.js +5 -13
- package/dist/src/step-ref.js.map +1 -1
- package/dist/src/stores/factory.d.ts +6 -0
- package/dist/src/stores/factory.d.ts.map +1 -1
- package/dist/src/stores/factory.js +4 -1
- package/dist/src/stores/factory.js.map +1 -1
- package/dist/src/stores/lucid-helpers.d.ts +2 -0
- package/dist/src/stores/lucid-helpers.d.ts.map +1 -1
- package/dist/src/stores/lucid-helpers.js +7 -0
- package/dist/src/stores/lucid-helpers.js.map +1 -1
- package/dist/src/stores/lucid-mappers.d.ts +25 -0
- package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
- package/dist/src/stores/lucid-mappers.js +81 -18
- package/dist/src/stores/lucid-mappers.js.map +1 -1
- package/dist/src/stores/lucid-schema.d.ts +59 -1
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +271 -25
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +61 -2
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +346 -41
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/telescope/dashboard.d.ts.map +1 -1
- package/dist/src/telescope/dashboard.js +30 -1
- package/dist/src/telescope/dashboard.js.map +1 -1
- package/dist/src/telescope/data-providers.d.ts +19 -1
- package/dist/src/telescope/data-providers.d.ts.map +1 -1
- package/dist/src/telescope/data-providers.js +119 -3
- package/dist/src/telescope/data-providers.js.map +1 -1
- package/dist/src/telescope/extension.d.ts.map +1 -1
- package/dist/src/telescope/extension.js +2 -1
- package/dist/src/telescope/extension.js.map +1 -1
- package/dist/src/telescope/index.d.ts +1 -1
- package/dist/src/telescope/index.d.ts.map +1 -1
- package/dist/src/telescope/index.js +1 -1
- package/dist/src/telescope/index.js.map +1 -1
- package/dist/src/testing/in-memory-state-store.d.ts +44 -15
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +204 -41
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/checkpoint-aggregation-contract.d.ts +16 -0
- package/dist/src/testing-kit/checkpoint-aggregation-contract.d.ts.map +1 -0
- package/dist/src/testing-kit/checkpoint-aggregation-contract.js +228 -0
- package/dist/src/testing-kit/checkpoint-aggregation-contract.js.map +1 -0
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +355 -3
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/transport-pool.d.ts +15 -1
- package/dist/src/transport-pool.d.ts.map +1 -1
- package/dist/src/transport-pool.js +50 -4
- package/dist/src/transport-pool.js.map +1 -1
- package/dist/src/transports/adaptive-concurrency.d.ts +168 -0
- package/dist/src/transports/adaptive-concurrency.d.ts.map +1 -0
- package/dist/src/transports/adaptive-concurrency.js +353 -0
- package/dist/src/transports/adaptive-concurrency.js.map +1 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.d.ts +1 -165
- package/dist/src/transports/bullmq/adaptive-concurrency.d.ts.map +1 -1
- package/dist/src/transports/bullmq/adaptive-concurrency.js +3 -351
- package/dist/src/transports/bullmq/adaptive-concurrency.js.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts +49 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +330 -38
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/dist/src/transports/bullmq/deps.d.ts +18 -2
- package/dist/src/transports/bullmq/deps.d.ts.map +1 -1
- package/dist/src/transports/bullmq/deps.js +44 -2
- package/dist/src/transports/bullmq/deps.js.map +1 -1
- package/dist/src/transports/db-helpers.d.ts +39 -0
- package/dist/src/transports/db-helpers.d.ts.map +1 -1
- package/dist/src/transports/db-helpers.js +73 -0
- package/dist/src/transports/db-helpers.js.map +1 -1
- package/dist/src/transports/db-remote.d.ts +53 -0
- package/dist/src/transports/db-remote.d.ts.map +1 -0
- package/dist/src/transports/db-remote.js +147 -0
- package/dist/src/transports/db-remote.js.map +1 -0
- package/dist/src/transports/db-schema.d.ts +44 -1
- package/dist/src/transports/db-schema.d.ts.map +1 -1
- package/dist/src/transports/db-schema.js +227 -88
- package/dist/src/transports/db-schema.js.map +1 -1
- package/dist/src/transports/db.d.ts +189 -6
- package/dist/src/transports/db.d.ts.map +1 -1
- package/dist/src/transports/db.js +1291 -137
- package/dist/src/transports/db.js.map +1 -1
- package/dist/src/transports/event-emitter.d.ts +8 -0
- package/dist/src/transports/event-emitter.d.ts.map +1 -1
- package/dist/src/transports/event-emitter.js +36 -3
- package/dist/src/transports/event-emitter.js.map +1 -1
- package/dist/src/transports/factory.d.ts +110 -5
- package/dist/src/transports/factory.d.ts.map +1 -1
- package/dist/src/transports/factory.js +56 -9
- package/dist/src/transports/factory.js.map +1 -1
- package/dist/src/transports/queue-mock-adapter.d.ts +7 -2
- package/dist/src/transports/queue-mock-adapter.d.ts.map +1 -1
- package/dist/src/transports/queue-mock-adapter.js +18 -2
- package/dist/src/transports/queue-mock-adapter.js.map +1 -1
- package/dist/src/transports/queue-remote.d.ts +146 -0
- package/dist/src/transports/queue-remote.d.ts.map +1 -0
- package/dist/src/transports/queue-remote.js +398 -0
- package/dist/src/transports/queue-remote.js.map +1 -0
- package/dist/src/transports/queue.d.ts +159 -6
- package/dist/src/transports/queue.d.ts.map +1 -1
- package/dist/src/transports/queue.js +747 -37
- package/dist/src/transports/queue.js.map +1 -1
- package/dist/src/transports/task-failure.d.ts +31 -0
- package/dist/src/transports/task-failure.d.ts.map +1 -0
- package/dist/src/transports/task-failure.js +50 -0
- package/dist/src/transports/task-failure.js.map +1 -0
- package/dist/src/workflow-als.d.ts +1 -1
- package/dist/src/workflow-als.d.ts.map +1 -1
- package/dist/src/workflow-ctx.d.ts +10 -0
- package/dist/src/workflow-ctx.d.ts.map +1 -1
- package/dist/src/workflow-ctx.js +97 -15
- package/dist/src/workflow-ctx.js.map +1 -1
- package/dist/src/workflow-discovery.d.ts +32 -41
- package/dist/src/workflow-discovery.d.ts.map +1 -1
- package/dist/src/workflow-discovery.js +65 -88
- package/dist/src/workflow-discovery.js.map +1 -1
- package/dist/src/workflow-ref.d.ts +24 -0
- package/dist/src/workflow-ref.d.ts.map +1 -1
- package/dist/src/workflow-ref.js.map +1 -1
- package/dist/stubs/config/durable.stub +6 -0
- package/dist/stubs/config/durable_dashboard.stub +7 -3
- package/dist/stubs/database/migrations/create_durable_tables.stub +2 -1
- package/dist/stubs/database/migrations/create_durable_transport_messages.stub +43 -0
- package/dist/stubs/database/migrations/create_durable_transport_tables.stub +12 -9
- package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +1 -1
- package/dist/stubs/make/workflow/main.stub +1 -1
- package/package.json +6 -4
- package/skills/durable-cluster/SKILL.md +80 -11
- package/skills/durable-determinism/SKILL.md +11 -5
- package/skills/durable-reliability/SKILL.md +77 -37
- package/skills/durable-setup/SKILL.md +51 -22
- package/skills/durable-transports-stores/SKILL.md +128 -18
- package/skills/durable-workflows/SKILL.md +71 -18
- package/dist/assets/spa/assets/index-DdB9zxuX.js +0 -16
- package/dist/assets/spa/assets/index-DdB9zxuX.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/durable",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Durable cross-app workflows for AdonisJS — deterministic replay engine, AdonisJS binding, ace commands, dashboard, OpenTelemetry, Telescope, testing harness and Redis admission, all in one package. Part of the Agora ecosystem.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -125,8 +125,7 @@
|
|
|
125
125
|
],
|
|
126
126
|
"sideEffects": false,
|
|
127
127
|
"dependencies": {
|
|
128
|
-
"@adonis-agora/filter": "0.11.0"
|
|
129
|
-
"zod": "4.6.7"
|
|
128
|
+
"@adonis-agora/filter": "0.11.0"
|
|
130
129
|
},
|
|
131
130
|
"peerDependencies": {
|
|
132
131
|
"@adonisjs/assembler": "^8.4.0",
|
|
@@ -170,7 +169,7 @@
|
|
|
170
169
|
}
|
|
171
170
|
},
|
|
172
171
|
"devDependencies": {
|
|
173
|
-
"@adonis-agora/durable-dashboard": "0.8.
|
|
172
|
+
"@adonis-agora/durable-dashboard": "0.8.1",
|
|
174
173
|
"@adonisjs/assembler": "8.6.0",
|
|
175
174
|
"@adonisjs/core": "7.6.0",
|
|
176
175
|
"@adonisjs/lucid": "22.4.2",
|
|
@@ -183,11 +182,14 @@
|
|
|
183
182
|
"@tanstack/intent": "0.5.6",
|
|
184
183
|
"@types/better-sqlite3": "9.6.0",
|
|
185
184
|
"@types/node": "26.6.5",
|
|
185
|
+
"arktype": "2.2.9",
|
|
186
186
|
"better-sqlite3": "13.0.3",
|
|
187
187
|
"bullmq": "6.3.17",
|
|
188
188
|
"cron-parser": "5.10.2",
|
|
189
189
|
"ioredis": "6.0.0",
|
|
190
|
+
"pg": "8.23.1",
|
|
190
191
|
"typescript": "7.0.2",
|
|
192
|
+
"valibot": "1.5.0",
|
|
191
193
|
"vitest": "5.0.5",
|
|
192
194
|
"zod": "4.6.7"
|
|
193
195
|
},
|
|
@@ -8,13 +8,14 @@ description: >-
|
|
|
8
8
|
(compatible/degraded/incompatible) and capability routing (requires → blocked
|
|
9
9
|
runs), layered tenant auth (signTenantToken/hmacTenantVerifier/verifyTenant),
|
|
10
10
|
runGateway as the role-portable read/control surface, namespace vs partition,
|
|
11
|
-
|
|
11
|
+
tenant pods over bullmq, the @adonisjs/queue transport or the db transport, and Python
|
|
12
|
+
(durable-worker) / NestJS interop over the bullmq wire. Use when
|
|
12
13
|
splitting web from workers, isolating tenants, or adding polyglot workers.
|
|
13
14
|
license: MIT
|
|
14
15
|
metadata:
|
|
15
16
|
type: core
|
|
16
17
|
library: "@adonis-agora/durable"
|
|
17
|
-
library_version: "0.
|
|
18
|
+
library_version: "0.51.0"
|
|
18
19
|
framework: adonisjs
|
|
19
20
|
sources:
|
|
20
21
|
- 'DavideCarvalho/adonis-durable:docs/cluster/index.mdx'
|
|
@@ -44,11 +45,11 @@ execute changes.
|
|
|
44
45
|
Split fleet: a pure coordinator plus store-less tenant workers.
|
|
45
46
|
|
|
46
47
|
```ts title="config/durable.ts (control plane)"
|
|
47
|
-
import { defineConfig, stores, transports } from '@adonis-agora/durable'
|
|
48
|
+
import { defineConfig, hmacTenantVerifier, stores, transports } from '@adonis-agora/durable'
|
|
48
49
|
|
|
49
50
|
export default defineConfig({
|
|
50
51
|
role: 'control-plane',
|
|
51
|
-
transport: 'bullmq', //
|
|
52
|
+
transport: 'bullmq', // or 'queue' / 'db' for Adonis-only fleets
|
|
52
53
|
transports: { bullmq: transports.bullmq({ connection: { host: '127.0.0.1', port: 6379 } }) },
|
|
53
54
|
store: 'lucid',
|
|
54
55
|
stores: { lucid: stores.lucid({ connection: 'pg' }) },
|
|
@@ -79,6 +80,41 @@ Isolation is three-layered: type (`store` is `never` on tenant), container (no
|
|
|
79
80
|
store binding registered), object (`WorkerRuntime`/`ProxyRunGateway` have no
|
|
80
81
|
store field).
|
|
81
82
|
|
|
83
|
+
Tenant pods need a transport carrying the run-request protocol: `bullmq`,
|
|
84
|
+
`queue` or `db` (the provider feature-detects the methods). Both sides use the
|
|
85
|
+
same transport and backend. On `queue`, replies go to a per-pod
|
|
86
|
+
`<prefix>:run-replies:<instanceId>` queue and live-tail events are pushed to
|
|
87
|
+
each subscribed pod's `<prefix>:tenant-events:<instanceId>`; the pod announces
|
|
88
|
+
its subscriptions and (with `workerKeys: false`) its worker advertisement to the
|
|
89
|
+
control plane, which records them in its own key store — so the pod needs no
|
|
90
|
+
database:
|
|
91
|
+
|
|
92
|
+
```ts title="config/durable.ts (tenant pod, queue transport)"
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
role: 'tenant',
|
|
95
|
+
transport: 'queue',
|
|
96
|
+
transports: { queue: transports.queue({ connection: 'redis', workerKeys: false }) },
|
|
97
|
+
partition: 'acme-corp',
|
|
98
|
+
tenant: { token: process.env.DURABLE_TENANT_TOKEN },
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Queue-transport limits vs bullmq: replies/events are polled (not pushed), a new
|
|
103
|
+
subscription can miss events for about a second, each event costs one job per
|
|
104
|
+
subscribed pod, and live-tail needs a worker-key store on the control plane.
|
|
105
|
+
Source: `docs/transports/queue.mdx` (tenant pods over the queue transport).
|
|
106
|
+
|
|
107
|
+
On `db`, the pod points `transports.db()` at the control plane's database (it
|
|
108
|
+
still owns no durable store). Requests, starts, replies, decisions and events
|
|
109
|
+
are rows of `durable_transport_messages`; a reply or event is a row addressed to
|
|
110
|
+
one instance, and the pod writes its own `-inbox:` / `-tenant-subscriber:` keys
|
|
111
|
+
and worker advertisement to `durable_transport_worker_keys`. Remote workflow
|
|
112
|
+
turns are task rows of kind `workflow`, and each decision is addressed to the
|
|
113
|
+
engine replica that dispatched the turn (any replica could apply it: the engine
|
|
114
|
+
applies a decision by run id). Needs the
|
|
115
|
+
`create_durable_transport_messages` migration (or `autoCreate`). Source:
|
|
116
|
+
`docs/transports/db.mdx` (remote workflows and tenant pods).
|
|
117
|
+
|
|
82
118
|
## Core patterns
|
|
83
119
|
|
|
84
120
|
### Pattern 1 — role-portable code via `runGateway`
|
|
@@ -107,16 +143,22 @@ export default class RunsController {
|
|
|
107
143
|
Verbs: `getRun`, `listRuns`, `getCheckpoints`, `getRunChildren`,
|
|
108
144
|
`getRunDetail` / `getFanMembers` (optional; summarize and page wide fans),
|
|
109
145
|
`getSearchAttributes`, `start`, `signal`, `cancel`, `redispatchPending`,
|
|
110
|
-
`workerHealth`, `subscribe`, `topology()
|
|
146
|
+
`workerHealth`, `subscribe`, `topology()`, and the NestJS gateway's `retry`
|
|
147
|
+
(in-place requeue), `continue` (breakpoint), `retryWithInput`, `runFacets`,
|
|
148
|
+
`runValueFacets`, `waitingFor` (optional on the interface; both built-in
|
|
149
|
+
gateways implement them, tenant-scoped by the responder). Never branch on `role`.
|
|
111
150
|
|
|
112
151
|
```ts
|
|
113
152
|
// Each fan of more than 50 members — or declared wider than 50 — comes back as one FanSummary in `fans`.
|
|
114
153
|
const detail = await runGateway.getRunDetail?.(params.id, { summarizeFansOver: 50 })
|
|
115
154
|
```
|
|
116
155
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
Tenants and the control plane upgrade in either order. A verb the control plane
|
|
157
|
+
does not know comes back as `unsupported-request` (or, before 0.50, an
|
|
158
|
+
`unknown run-request verb` error) and the proxy falls back at once: run detail
|
|
159
|
+
is built from `getRun` + `getCheckpoints` and summarized on the tenant, and the
|
|
160
|
+
tenant console degrades `retry` to a re-dispatch; it remembers the old control
|
|
161
|
+
plane for a minute. Every request gets a reply, even when the gateway throws.
|
|
120
162
|
Source: `docs/cluster/roles-and-config.mdx`.
|
|
121
163
|
|
|
122
164
|
### Pattern 2 — capability-aware routing and blocked runs
|
|
@@ -147,10 +189,11 @@ imports zero store code:
|
|
|
147
189
|
|
|
148
190
|
```ts title="worker.ts"
|
|
149
191
|
import { RedisWorkerRegistry, WorkerRuntime } from '@adonis-agora/durable/worker'
|
|
150
|
-
import {
|
|
192
|
+
import { BullMQTransport, createBullMQDeps } from '@adonis-agora/durable'
|
|
151
193
|
import { Redis } from 'ioredis'
|
|
152
194
|
|
|
153
|
-
|
|
195
|
+
// No Adonis app here, so build the transport directly instead of through `transports.bullmq()`.
|
|
196
|
+
const transport = new BullMQTransport({ deps: await createBullMQDeps(process.env.REDIS_URL) })
|
|
154
197
|
const runtime = new WorkerRuntime({
|
|
155
198
|
transport,
|
|
156
199
|
partition: 'acme-corp',
|
|
@@ -294,6 +337,28 @@ the queues the deployed fleet consumes — so the local process steals productio
|
|
|
294
337
|
tasks; docs call this out under Tenancy pitfalls ("competes for, and steals, the
|
|
295
338
|
deployed fleet's tasks"). Source: `docs/concepts/tenancy.mdx`.
|
|
296
339
|
|
|
340
|
+
### HIGH a db-transport tenant pod on a database of its own
|
|
341
|
+
|
|
342
|
+
Wrong:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
// the pod's default Lucid connection is its own app database
|
|
346
|
+
defineConfig({ role: 'tenant', transport: 'db', transports: { db: transports.db() }, partition: 'acme' })
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Correct:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// a connection to the CONTROL PLANE's database (config/database.ts), used only for the transport tables
|
|
353
|
+
defineConfig({ role: 'tenant', transport: 'db', transports: { db: transports.db({ connection: 'durable' }) }, partition: 'acme' })
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Mechanism: on `db` the wire IS the transport tables, so a pod writing its
|
|
357
|
+
requests to another database is never answered — every `runGateway` call times
|
|
358
|
+
out after `requestTimeoutMs` and no step reaches it. Pods that cannot reach the
|
|
359
|
+
control plane's database need `queue` or `bullmq`. Source:
|
|
360
|
+
`docs/transports/db.mdx` (remote workflows and tenant pods).
|
|
361
|
+
|
|
297
362
|
### MEDIUM stamping runs into a namespace nothing polls
|
|
298
363
|
|
|
299
364
|
Wrong:
|
|
@@ -311,7 +376,11 @@ await engine.start('checkout', order, id, { namespace: 'eu-west' })
|
|
|
311
376
|
|
|
312
377
|
Mechanism: between namespaced engines there is no fallback owner; a namespace
|
|
313
378
|
with neither an engine of its own nor an operator above it is a dead end where
|
|
314
|
-
runs sit `pending` forever.
|
|
379
|
+
runs sit `pending` forever. Since 0.51 only a `control-plane` with no
|
|
380
|
+
`namespace` is the operator; a `standalone` engine with no `namespace` is the
|
|
381
|
+
`'default'` pool. Before 0.51 an unset namespace made ANY engine an operator,
|
|
382
|
+
so a standalone app sharing a store drove every pool's runs. Source:
|
|
383
|
+
`docs/concepts/tenancy.mdx` (pitfalls).
|
|
315
384
|
|
|
316
385
|
See also: `durable-setup` — the standalone execution model this scales out of.
|
|
317
386
|
See also: `durable-transports-stores` — bullmq/queue/db driver details,
|
|
@@ -6,14 +6,15 @@ description: >-
|
|
|
6
6
|
checkpointed ctx.now() and ctx.sideEffect() capture sources,
|
|
7
7
|
NonDeterminismError at replay time, workflow versions for breaking shape
|
|
8
8
|
changes (skew protection), ctx.patched for surgical in-place guards, and the
|
|
9
|
-
@adonis-agora/durable-eslint-plugin no-nondeterminism
|
|
10
|
-
|
|
9
|
+
@adonis-agora/durable-eslint-plugin rules (no-nondeterminism,
|
|
10
|
+
rethrow-control-flow-signals, no-io-in-workflow-body) and its recommended
|
|
11
|
+
preset. Use when authoring or changing workflow code that has
|
|
11
12
|
runs in flight, fixing NonDeterminismError, or wiring determinism linting.
|
|
12
13
|
license: MIT
|
|
13
14
|
metadata:
|
|
14
15
|
type: core
|
|
15
16
|
library: "@adonis-agora/durable"
|
|
16
|
-
library_version: "0.
|
|
17
|
+
library_version: "0.51.0"
|
|
17
18
|
framework: adonisjs
|
|
18
19
|
sources:
|
|
19
20
|
- 'DavideCarvalho/adonis-durable:docs/concepts/durability.mdx'
|
|
@@ -75,15 +76,20 @@ export default [
|
|
|
75
76
|
plugins: { '@adonis-agora/durable': durable },
|
|
76
77
|
rules: {
|
|
77
78
|
'@adonis-agora/durable/no-nondeterminism': 'error',
|
|
79
|
+
'@adonis-agora/durable/rethrow-control-flow-signals': 'error',
|
|
80
|
+
'@adonis-agora/durable/no-io-in-workflow-body': 'error',
|
|
78
81
|
},
|
|
79
82
|
},
|
|
80
83
|
]
|
|
81
84
|
|
|
82
|
-
// or simply:
|
|
85
|
+
// or simply (enables all three):
|
|
83
86
|
// export default [durable.configs.recommended]
|
|
84
87
|
```
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
`rethrow-control-flow-signals` flags a `catch` around an awaited ctx op that
|
|
90
|
+
does not rethrow `isWorkflowControlFlowSignal(error)`; `no-io-in-workflow-body`
|
|
91
|
+
flags a global `fetch()` and raw `engine.*` calls in the body. Each report names
|
|
92
|
+
its replacement ("use `ctx.now()`"). The plugin is the cheap
|
|
87
93
|
early layer; `NonDeterminismError` is the replay-time backstop for calls it
|
|
88
94
|
cannot see (transitive helpers). Source: `docs/tooling/linting.mdx`.
|
|
89
95
|
|
|
@@ -5,23 +5,25 @@ description: >-
|
|
|
5
5
|
backoff and jitter, FatalError and worker retryable:false, timeoutMs liveness
|
|
6
6
|
vs the durable suspend path, pickupTimeoutMs, log.heartbeat, saga
|
|
7
7
|
compensation (compensate callbacks/refs, UndoOf, compensationRetries,
|
|
8
|
-
cancel({compensate:true})), flow-control queues (registerQueue, concurrency,
|
|
8
|
+
cancel({compensate:true})), flow-control queues (config queues / registerQueue, concurrency,
|
|
9
9
|
rateLimit, fairnessKey) with Redis-backed global admission (admission-redis),
|
|
10
|
-
the dead-letter queue (maxRecoveryAttempts,
|
|
10
|
+
the dead-letter queue (maxRecoveryAttempts, deadLetterWorkflow, deadLetter()
|
|
11
|
+
method, onDead, retryWithInput), retention (maxAge/maxCount policies), the
|
|
11
12
|
stuck-run playbook (stranded signature, redispatchPending,
|
|
12
|
-
remoteRedispatchMs), ace ops commands (durable:work/runs/retry --stale), and
|
|
13
|
+
remoteRedispatchMs, remoteAdvanceSilenceMs for remote workflow turns), ace ops commands (durable:work/runs/retry --stale), and
|
|
13
14
|
the testing harness (createTestEngine, tick, failOnce, conformance kits).
|
|
14
15
|
license: MIT
|
|
15
16
|
metadata:
|
|
16
17
|
type: core
|
|
17
18
|
library: "@adonis-agora/durable"
|
|
18
|
-
library_version: "0.
|
|
19
|
+
library_version: "0.51.0"
|
|
19
20
|
framework: adonisjs
|
|
20
21
|
sources:
|
|
21
22
|
- 'DavideCarvalho/adonis-durable:docs/reliability/retries.mdx'
|
|
22
23
|
- 'DavideCarvalho/adonis-durable:docs/reliability/sagas.mdx'
|
|
23
24
|
- 'DavideCarvalho/adonis-durable:docs/reliability/dead-letter.mdx'
|
|
24
25
|
- 'DavideCarvalho/adonis-durable:docs/reliability/flow-control.mdx'
|
|
26
|
+
- 'DavideCarvalho/adonis-durable:docs/reliability/retention.mdx'
|
|
25
27
|
- 'DavideCarvalho/adonis-durable:docs/reliability/failure-modes.mdx'
|
|
26
28
|
- 'DavideCarvalho/adonis-durable:docs/cli.mdx'
|
|
27
29
|
- 'DavideCarvalho/adonis-durable:docs/testing.mdx'
|
|
@@ -40,7 +42,7 @@ Retries are declared per step; `FatalError` opts a deterministic business
|
|
|
40
42
|
failure out of the retry path:
|
|
41
43
|
|
|
42
44
|
```ts title="app/steps/charge.ts"
|
|
43
|
-
import { defineStep } from '@adonis-agora/durable'
|
|
45
|
+
import { FatalError, defineStep } from '@adonis-agora/durable'
|
|
44
46
|
|
|
45
47
|
export const chargeCard = defineStep(
|
|
46
48
|
'payments:charge-card',
|
|
@@ -48,7 +50,7 @@ export const chargeCard = defineStep(
|
|
|
48
50
|
const res = await stripe.charge(input)
|
|
49
51
|
if (res.declined) {
|
|
50
52
|
// deterministic verdict — don't make the engine retry it
|
|
51
|
-
throw
|
|
53
|
+
throw new FatalError('card declined', 'declined')
|
|
52
54
|
}
|
|
53
55
|
return { chargeId: res.id }
|
|
54
56
|
},
|
|
@@ -56,7 +58,9 @@ export const chargeCard = defineStep(
|
|
|
56
58
|
)
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
`FatalError` carries `retryable: false`, which the step result builders read; a
|
|
62
|
+
worker in another runtime gets the same effect by throwing any error with
|
|
63
|
+
`retryable: false` (and an optional `code`). A dispatched step WITHOUT `timeoutMs` takes the durable path: failure → persist
|
|
60
64
|
`wakeAt = now + backoff(attempt)` on the checkpoint → suspend → the timer poller
|
|
61
65
|
re-dispatches. Crash-safe, never held in memory.
|
|
62
66
|
|
|
@@ -90,16 +94,20 @@ envelope (`{ input, output }`). Engine-level `compensationRetries` (default 1)
|
|
|
90
94
|
bounds transient undo failures; failing compensations are skipped so they can't
|
|
91
95
|
mask the original error. Trigger deliberately with
|
|
92
96
|
`await engine.cancel(runId, { compensate: true })` or
|
|
93
|
-
`engine.cancelWhere(filter, { compensate: true })`.
|
|
94
|
-
|
|
97
|
+
`engine.cancelWhere(filter, { compensate: true })`. The cancel is durable (a
|
|
98
|
+
`cancel:<runId>` marker) and honored on every exit of the turn running when it
|
|
99
|
+
lands: return, throw, continue-as-new or suspend all compensate and settle
|
|
100
|
+
`cancelled`. Every undo surfaces as a visible `compensate:<step>` event.
|
|
101
|
+
Source: `docs/reliability/sagas.mdx`.
|
|
95
102
|
|
|
96
103
|
### Pattern 2 — flow-control queues + fleet-wide admission
|
|
97
104
|
|
|
98
105
|
Cap concurrency/rate durably — a blocked call re-suspends with a persisted
|
|
99
106
|
`retryAt`, driven by the timer poller (nothing waits in memory):
|
|
100
107
|
|
|
101
|
-
```ts title="
|
|
102
|
-
|
|
108
|
+
```ts title="config/durable.ts"
|
|
109
|
+
queues: [{ name: 'emails', concurrency: 10, rateLimit: { limit: 1000, periodMs: 3_600_000 } }],
|
|
110
|
+
// or at runtime: engine.registerQueue({ name: 'emails', … }) — same shape, replaces a same-named one
|
|
103
111
|
```
|
|
104
112
|
|
|
105
113
|
```ts
|
|
@@ -130,7 +138,12 @@ Source: `docs/reliability/flow-control.mdx`.
|
|
|
130
138
|
|
|
131
139
|
Crash-recovery reclaims orphaned runs within ~`leaseMs`; a run that crashes the
|
|
132
140
|
process every time would loop forever. Cap it and route the terminal `dead`
|
|
133
|
-
status into a handler workflow
|
|
141
|
+
status into a handler workflow — the engine starts it as `dlq:<runId>`
|
|
142
|
+
(idempotent) with a `DeadLetter` input `{ deadRunId, workflow, input, error }`
|
|
143
|
+
in the dead run's namespace. A run
|
|
144
|
+
whose remote step is still in flight is parked `suspended`, never counted, and a
|
|
145
|
+
retry (`requeue` / `durable:retry`) of a `dead`/`failed` run resets
|
|
146
|
+
`recoveryAttempts` to 0:
|
|
134
147
|
|
|
135
148
|
```ts title="config/durable.ts"
|
|
136
149
|
export default defineConfig({
|
|
@@ -139,20 +152,40 @@ export default defineConfig({
|
|
|
139
152
|
})
|
|
140
153
|
```
|
|
141
154
|
|
|
142
|
-
```ts title="
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
})
|
|
155
|
+
```ts title="app/workflows/pipeline_workflow.ts"
|
|
156
|
+
export default class PipelineWorkflow extends BaseWorkflow {
|
|
157
|
+
static workflow = { name: 'pipeline', version: '1' }
|
|
158
|
+
async run(ctx: WorkflowCtx, input: PipelineInput) { /* … */ }
|
|
159
|
+
// registered as `pipeline.dlq`; shares this instance's injected services
|
|
160
|
+
async deadLetter(ctx: WorkflowCtx, dl: DeadLetter<PipelineInput>) { /* alert, ticket */ }
|
|
161
|
+
}
|
|
150
162
|
```
|
|
151
163
|
|
|
164
|
+
Precedence: inline `deadLetter()` > per-workflow `deadLetterWorkflow`
|
|
165
|
+
(`static workflow` / `register` / `registerRemote` option) > config
|
|
166
|
+
`deadLetterWorkflow` (fleet default). Declaring both a method and the option
|
|
167
|
+
throws. A `dlq:` handler run that dies is not routed again; a failed start
|
|
168
|
+
(handler not registered) is `console.warn`-ed. `engine.onDead(run => …)` still
|
|
169
|
+
fires first and is the manual escape hatch (keep the `dlq:<runId>` id).
|
|
170
|
+
|
|
152
171
|
A dead run stays inspectable and retriable (`durable:retry <runId>`, dashboard
|
|
153
172
|
retry, or `engine.retryWithInput(runId, fixedInput)` for bad data).
|
|
154
173
|
Source: `docs/reliability/dead-letter.mdx`.
|
|
155
174
|
|
|
175
|
+
Retention bounds terminal history (swept by `durable:work`, once a minute,
|
|
176
|
+
`onEvict` archives first). Scoped policies take `maxAge`, `maxCount` (keep the N
|
|
177
|
+
newest by start time within the scope; `0` deletes all matching), or both (most
|
|
178
|
+
restrictive wins); a policy with neither fails boot:
|
|
179
|
+
|
|
180
|
+
```ts title="config/durable.ts"
|
|
181
|
+
retention: {
|
|
182
|
+
completed: '30d',
|
|
183
|
+
policies: [{ statuses: ['completed', 'failed'], maxCount: 200, scope: { namespace: 'acme' } }],
|
|
184
|
+
},
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Source: `docs/reliability/retention.mdx`.
|
|
188
|
+
|
|
156
189
|
### Pattern 4 — the stuck-run playbook
|
|
157
190
|
|
|
158
191
|
A lost remote dispatch does **not** self-heal by default: the run cycles
|
|
@@ -166,22 +199,14 @@ node ace durable:retry checkout:1234
|
|
|
166
199
|
```
|
|
167
200
|
|
|
168
201
|
```ts title="app/controllers/ops_controller.ts"
|
|
169
|
-
import {
|
|
170
|
-
import {
|
|
171
|
-
|
|
172
|
-
WorkflowEngine,
|
|
173
|
-
attachLiveness,
|
|
174
|
-
filterStale,
|
|
175
|
-
listRuns,
|
|
176
|
-
} from '@adonis-agora/durable'
|
|
202
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
203
|
+
import { DEFAULT_STALE_MS, attachLiveness, filterStale, listRuns } from '@adonis-agora/durable'
|
|
204
|
+
import engine from '@adonis-agora/durable/services/main'
|
|
177
205
|
|
|
178
|
-
@inject()
|
|
179
206
|
export default class OpsController {
|
|
180
|
-
constructor(private engine: WorkflowEngine) {}
|
|
181
|
-
|
|
182
207
|
async strandedRuns({ response }: HttpContext) {
|
|
183
|
-
const runs = await listRuns(
|
|
184
|
-
const stale = filterStale(await attachLiveness(
|
|
208
|
+
const runs = await listRuns(engine, { statuses: ['running', 'suspended'], limit: 200 })
|
|
209
|
+
const stale = filterStale(await attachLiveness(engine, runs), DEFAULT_STALE_MS)
|
|
185
210
|
return response.ok({ stranded: stale.map(({ run }) => run.id) })
|
|
186
211
|
}
|
|
187
212
|
}
|
|
@@ -198,6 +223,20 @@ Three nets, in preference order:
|
|
|
198
223
|
3. `engine.redispatchPending(runId)` — the operator escape hatch, safe on a
|
|
199
224
|
healthy run (only touches already-pending checkpoints).
|
|
200
225
|
|
|
226
|
+
Remote (polyglot) workflow TURNS have their own net: `remoteAdvanceSilenceMs`
|
|
227
|
+
(config; `RemoteWorkflowExecutor`'s `timeoutMs` per workflow, shorter wins). The
|
|
228
|
+
engine records the turn's random `taskId` on the run (`awaitingDecisionTaskId`),
|
|
229
|
+
releases the lease, then enqueues; whichever replica consumes the decision
|
|
230
|
+
applies it by run id (stale/duplicate decisions dropped), so a point-to-point
|
|
231
|
+
`<P>-decisions` queue across replicas is safe. That is 0.51+: before 0.51 a
|
|
232
|
+
replica that had not dispatched the turn consumed the decision, found no waiter
|
|
233
|
+
and dropped it, leaving the run leased forever — upgrade any multi-replica fleet
|
|
234
|
+
running remote (Python/NestJS) workflows. A turn with no decision and no
|
|
235
|
+
run-scoped heartbeat (`{ runId, seq: 0, group }`, sent every 5s by the Python
|
|
236
|
+
and TS workers) for the window is re-dispatched by the timer poller — never
|
|
237
|
+
failed. Unset = wait for the decision forever. Source:
|
|
238
|
+
`docs/reliability/multi-instance.mdx` (remote workflow turns).
|
|
239
|
+
|
|
201
240
|
Source: `docs/reliability/failure-modes.mdx`, `docs/cli.mdx`.
|
|
202
241
|
|
|
203
242
|
### Pattern 5 — test workflows without infrastructure
|
|
@@ -282,7 +321,7 @@ Correct:
|
|
|
282
321
|
|
|
283
322
|
```ts
|
|
284
323
|
const inFlight = counts['run.started'] - (counts['run.completed'] + counts['run.failed'])
|
|
285
|
-
// or read the store: listRuns({
|
|
324
|
+
// or read the store: listRuns(engine, { statuses: ['running', 'suspended'] })
|
|
286
325
|
```
|
|
287
326
|
|
|
288
327
|
Mechanism: `run.suspended` fires on EVERY park — each sleep, signal wait, child
|
|
@@ -296,7 +335,7 @@ Source: `docs/observability/events-and-interceptors.mdx`.
|
|
|
296
335
|
Wrong:
|
|
297
336
|
|
|
298
337
|
```ts
|
|
299
|
-
const stalled = (await engine.workerHealth()).some((g) => g.stalled) // always false
|
|
338
|
+
const stalled = (await engine.workerHealth()).some((g) => g.stalled) // always false where no group health is reported (eventEmitter, queue with workerKeys: false)
|
|
300
339
|
```
|
|
301
340
|
|
|
302
341
|
Correct:
|
|
@@ -308,9 +347,10 @@ const stale = filterStale(
|
|
|
308
347
|
)
|
|
309
348
|
```
|
|
310
349
|
|
|
311
|
-
Mechanism:
|
|
350
|
+
Mechanism: only `bullmq`, `queue` (with a worker-key store) and `db` report group
|
|
351
|
+
health — on `eventEmitter`, or `queue` with `workerKeys: false`,
|
|
312
352
|
`workerHealth()` returns `[]`, not an error, so "no stalled groups reported" says
|
|
313
|
-
nothing. Use the stranded-signature query instead. Source:
|
|
353
|
+
nothing. On `queue`, `depth` counts pending jobs only; on `db`, pending + claimed rows. Use the stranded-signature query instead. Source:
|
|
314
354
|
`docs/reliability/failure-modes.mdx`.
|
|
315
355
|
|
|
316
356
|
### MEDIUM writing non-idempotent saga compensations
|
|
@@ -6,7 +6,7 @@ description: >-
|
|
|
6
6
|
(memory, event-emitter, queue, db, bullmq; lucid store), the WorkflowEngine
|
|
7
7
|
container singleton from @adonis-agora/durable/services/main,
|
|
8
8
|
Workflow.dispatch vs .start, where runs execute (in-process dispatcher,
|
|
9
|
-
|
|
9
|
+
drive: false, durable:work), consumers, leaseMs, shutdown drain. Use when setting up
|
|
10
10
|
durable workflows for the first time, moving from dev defaults to production
|
|
11
11
|
infrastructure, or diagnosing "runs vanish on restart" / "workflows compete
|
|
12
12
|
with HTTP traffic".
|
|
@@ -14,7 +14,7 @@ license: MIT
|
|
|
14
14
|
metadata:
|
|
15
15
|
type: core
|
|
16
16
|
library: "@adonis-agora/durable"
|
|
17
|
-
library_version: "0.
|
|
17
|
+
library_version: "0.51.0"
|
|
18
18
|
framework: adonisjs
|
|
19
19
|
sources:
|
|
20
20
|
- 'DavideCarvalho/adonis-durable:docs/getting-started.mdx'
|
|
@@ -34,15 +34,20 @@ default is for development only.
|
|
|
34
34
|
|
|
35
35
|
## Setup
|
|
36
36
|
|
|
37
|
-
Install via ace
|
|
38
|
-
|
|
37
|
+
Install via ace. The `configure` hook registers the durable and dashboard
|
|
38
|
+
providers, the Ace commands and the `app/workflows` / `app/steps` codegen hooks
|
|
39
|
+
in `adonisrc.ts`, adds the `#workflows/*` / `#steps/*` subpath imports to
|
|
40
|
+
`package.json`, and publishes `config/durable.ts`, `config/durable_dashboard.ts`
|
|
41
|
+
and the Lucid migrations:
|
|
39
42
|
|
|
40
43
|
```bash
|
|
41
44
|
node ace add @adonis-agora/durable
|
|
42
45
|
```
|
|
43
46
|
|
|
44
47
|
The provider binds a singleton `WorkflowEngine` built from the config. Import it
|
|
45
|
-
anywhere with `import engine from '@adonis-agora/durable/services/main'
|
|
48
|
+
anywhere with `import engine from '@adonis-agora/durable/services/main'` — never
|
|
49
|
+
`app.container.make(WorkflowEngine)` in app code. On a store-less `tenant` pod
|
|
50
|
+
the engine is `undefined`; use the same module's `runGateway` export instead.
|
|
46
51
|
|
|
47
52
|
```ts title="config/durable.ts"
|
|
48
53
|
import { defineConfig, stores, transports } from '@adonis-agora/durable'
|
|
@@ -120,49 +125,69 @@ export default defineConfig({
|
|
|
120
125
|
})
|
|
121
126
|
```
|
|
122
127
|
|
|
123
|
-
Run `node ace durable:work` alongside (or
|
|
124
|
-
|
|
128
|
+
Run `node ace durable:work` alongside (or set `worker: { embedded: true }` to
|
|
129
|
+
run the same loop inside the web process) so recovery, timers and schedules are
|
|
130
|
+
driven even if you later split processes.
|
|
125
131
|
Source: `docs/transports/index.mdx`.
|
|
126
132
|
|
|
127
133
|
### Pattern 2 — split web from workers
|
|
128
134
|
|
|
129
135
|
`engine.start()` never blocks the caller, but by default the body executes on a
|
|
130
136
|
microtask **on the calling instance**. On an API/dashboard pod that means heavy
|
|
131
|
-
runs compete with HTTP traffic and the DB pool.
|
|
132
|
-
the API pod only creates runs, and let `durable:work`
|
|
137
|
+
runs compete with HTTP traffic and the DB pool. Set `drive: false` (with
|
|
138
|
+
`consumers: 'never'`) so the API pod only creates runs, and let `durable:work`
|
|
139
|
+
execute them:
|
|
133
140
|
|
|
134
141
|
```ts title="config/durable.ts (API/dashboard pod)"
|
|
135
|
-
import {
|
|
142
|
+
import { defineConfig, stores, transports } from '@adonis-agora/durable'
|
|
136
143
|
|
|
137
144
|
export default defineConfig({
|
|
138
145
|
transport: 'queue',
|
|
139
146
|
transports: { queue: transports.queue({ connection: 'redis' }) },
|
|
140
147
|
store: 'lucid',
|
|
141
148
|
stores: { lucid: stores.lucid() },
|
|
142
|
-
|
|
149
|
+
drive: false, // create runs; never execute bodies here
|
|
150
|
+
consumers: 'never', // claim no broker jobs either
|
|
143
151
|
})
|
|
144
152
|
```
|
|
145
153
|
|
|
154
|
+
`durable:work` drives through its poll whatever `drive` says, so one config file
|
|
155
|
+
can serve both pods (runs a worker pod starts itself, e.g. children, are picked
|
|
156
|
+
up on its next tick rather than inline). A resume handled on the API pod (an
|
|
157
|
+
`engine.signal` or webhook) still executes there under the run's lease. An
|
|
158
|
+
explicit `runDispatcher` wins over `drive`; `drive: false` plus
|
|
159
|
+
`worker: { embedded: true }` throws at boot. Every store-backed process drains
|
|
160
|
+
in-flight runs on shutdown (`worker.drainTimeoutMs`, else `drainTimeoutMs`,
|
|
161
|
+
default 10s) before the transport closes; no `app.terminating` hook
|
|
162
|
+
needed.
|
|
163
|
+
|
|
146
164
|
```bash title="a separate worker process"
|
|
147
|
-
node ace durable:work --interval=500 --
|
|
165
|
+
node ace durable:work --interval=500 --drain-timeout=15000
|
|
148
166
|
```
|
|
149
167
|
|
|
150
168
|
Each `durable:work` tick runs `engine.runPending()` → `recoverIncomplete()` →
|
|
151
169
|
`resumeDueTimers()` → `sweepTimeouts()` → fires due schedules, then drains on
|
|
152
|
-
`SIGINT`/`SIGTERM`.
|
|
170
|
+
`SIGINT`/`SIGTERM`. `--drain-timeout` bounds the whole shutdown: the wait for
|
|
171
|
+
the tick in flight is deducted from the drain. The recovery fallbacks are
|
|
172
|
+
`config/durable.ts` fields: `reconcileMs` (lost-wake re-drive, default 300000,
|
|
173
|
+
`0` off) and `blockedPollMs` (blocked-run re-check, default 5000). Source: `docs/concepts/durability.mdx`,
|
|
153
174
|
`docs/reliability/failure-modes.mdx`, `docs/cli.mdx`.
|
|
154
175
|
|
|
155
176
|
### Pattern 3 — dispatch vs start vs waitForRun
|
|
156
177
|
|
|
157
178
|
One rule: `.dispatch(input)` = fire-and-forget (`{ runId }`); `.start(input)` =
|
|
158
|
-
block until the run
|
|
159
|
-
`engine.waitForRun(runId)` resolves a previously started run
|
|
179
|
+
block until the run is **terminal** (it waits through sleeps/signals) and get its
|
|
180
|
+
output; `engine.waitForRun(runId)` resolves a previously started run once it
|
|
181
|
+
**settles** (terminal or suspended — except a `suspended` run still awaiting a
|
|
182
|
+
remote workflow turn's decision, which keeps waiting until the decision lands) —
|
|
183
|
+
pass `{ terminal: true }` to wait for the end.
|
|
160
184
|
|
|
161
185
|
```ts
|
|
162
186
|
const { runId } = await OrderWorkflow.dispatch(order) // hot path — respond now
|
|
163
|
-
const
|
|
187
|
+
const output = await OrderWorkflow.start(order) // scripts / short runs only
|
|
164
188
|
await engine.start('order', order, runId)
|
|
165
|
-
const settled = await engine.waitForRun(runId) // resolves on settle
|
|
189
|
+
const settled = await engine.waitForRun(runId) // RunResult, resolves on settle
|
|
190
|
+
const done = await engine.waitForRun(runId, { terminal: true, timeoutMs: 60_000 })
|
|
166
191
|
```
|
|
167
192
|
|
|
168
193
|
`.start` blocks the calling context — avoid it on a hot HTTP path for
|
|
@@ -249,12 +274,13 @@ export default defineConfig({ transport: 'queue', transports: { /* … */ } })
|
|
|
249
274
|
Correct:
|
|
250
275
|
|
|
251
276
|
```ts
|
|
252
|
-
import {
|
|
277
|
+
import { defineConfig } from '@adonis-agora/durable'
|
|
253
278
|
|
|
254
279
|
export default defineConfig({
|
|
255
280
|
transport: 'queue',
|
|
256
281
|
transports: { /* … */ },
|
|
257
|
-
|
|
282
|
+
drive: false,
|
|
283
|
+
consumers: 'never',
|
|
258
284
|
})
|
|
259
285
|
```
|
|
260
286
|
|
|
@@ -284,9 +310,12 @@ export default defineConfig({
|
|
|
284
310
|
```
|
|
285
311
|
|
|
286
312
|
—or don't round-trip inline: start the run, exit, let the worker fleet finish
|
|
287
|
-
it. Mechanism:
|
|
288
|
-
loops (they would claim jobs and die
|
|
289
|
-
in that process unless
|
|
313
|
+
it. Mechanism: with `consumers: 'auto'` (the default) console/repl processes
|
|
314
|
+
deliberately do not start broker consumer loops (they would claim jobs and die
|
|
315
|
+
holding them), so nothing receives results in that process unless
|
|
316
|
+
`consumers: 'always'`. Before 0.50 the bullmq and db transports ignored this and
|
|
317
|
+
a one-off `node ace` command consumed jobs; upgrade if you see that.
|
|
318
|
+
`consumers: 'never'` makes a web fleet a pure producer.
|
|
290
319
|
Source: `docs/transports/queue.mdx` ("consumers").
|
|
291
320
|
|
|
292
321
|
See also: `durable-transports-stores` — picking drivers, namespaces, migrations.
|