@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
|
@@ -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/transports/index.mdx'
|
|
@@ -59,6 +59,11 @@ export default defineConfig({
|
|
|
59
59
|
|
|
60
60
|
Switching transports is a one-line change; workflow code never changes.
|
|
61
61
|
|
|
62
|
+
`transports.eventEmitter({ onError })` reports a failing result/heartbeat
|
|
63
|
+
handler (default `console.error`) and redelivers a failed result from memory
|
|
64
|
+
with backoff until `close()`. Before 0.50 such a failure was an unhandled
|
|
65
|
+
rejection — fatal under Node's default — and the result was lost.
|
|
66
|
+
|
|
62
67
|
## Core patterns
|
|
63
68
|
|
|
64
69
|
### Pattern 1 — cross-process steps without new infrastructure (`db`)
|
|
@@ -75,10 +80,51 @@ const transport = new DbTransport({ db }) // { partition } isolates this pool
|
|
|
75
80
|
transport.handle('pipeline:extract', async (input) => extract(input))
|
|
76
81
|
```
|
|
77
82
|
|
|
83
|
+
A worker runs ONE task row at a time unless `concurrency` is raised
|
|
84
|
+
(`transports.db({ concurrency: 8, resultConcurrency: 8 })`, or `'adaptive'` with
|
|
85
|
+
the same controller and fields as `queue`/`bullmq`); one poll loop serves every
|
|
86
|
+
handler name and claims only as many rows as it has free slots. Every held
|
|
87
|
+
claim is renewed every `leaseMs / 3`, and a row is recorded (task delete +
|
|
88
|
+
result insert, one transaction) only while `claimed_by` is still the worker's
|
|
89
|
+
token — a worker whose claim was taken over drops its result and reports it to
|
|
90
|
+
`onError`. (Before 0.50 the loop claimed 20 rows per poll, renewed only the
|
|
91
|
+
running one, and double-ran the rest of the batch when a step outlived
|
|
92
|
+
`leaseMs` — upgrade if `db` steps run twice.) Idle task/heartbeat/control loops back off up to `idleBackoffMax`
|
|
93
|
+
(default 4) poll intervals; results never back off.
|
|
94
|
+
|
|
95
|
+
Workers beat `durable[-<ns>]-worker-heartbeat:<token>:<instance>` into
|
|
96
|
+
`durable_transport_worker_keys` with their `WorkerStatus`, and `DbTransport`
|
|
97
|
+
implements `groupHealth`: `depth` = pending + claimed task rows of the group.
|
|
98
|
+
Each takeover of a lapsed claim bumps the row's `reclaims` column; past
|
|
99
|
+
`maxStalledCount` (default 3) the row is deleted and a retryable failed
|
|
100
|
+
`StepResult` (`remote step worker failed: claim lapsed more than N times`) is
|
|
101
|
+
published. `close()` (after `closeTimeoutMs`, default 5s) clears every claim it
|
|
102
|
+
still holds, uncounted, so another worker takes the rows at once and a deploy
|
|
103
|
+
never pushes a step toward the poison bound; only a crash (no close) counts.
|
|
104
|
+
Dispatch `priority` is stored on the row (`0` when absent = normal) and rows
|
|
105
|
+
are claimed highest priority first, then oldest. `autoCreate` /
|
|
106
|
+
`createDurableTransportTables` add `reclaims` and `priority` to an existing
|
|
107
|
+
table; with `autoCreate: false` add them yourself (integer, not null, default
|
|
108
|
+
0), or the poison bound / priority ordering stay off.
|
|
109
|
+
|
|
110
|
+
`db` also carries remote workflow turns (task rows of kind `workflow`, claimed
|
|
111
|
+
with the token's steps; each decision is a `durable_transport_messages` row
|
|
112
|
+
addressed to the engine replica that dispatched the turn, though any replica
|
|
113
|
+
could apply it) and store-less tenant
|
|
114
|
+
pods that reach the same database: start-run/run-request/step-event rows go to
|
|
115
|
+
any consumer, a reply is a row addressed to the requester, a tenant event is one
|
|
116
|
+
row per live subscriber (pods write their own `-inbox:` / `-tenant-subscriber:`
|
|
117
|
+
keys to `durable_transport_worker_keys`). A janitor on every instance that
|
|
118
|
+
addresses rows deletes those older than `inboxTtlMs` whose recipient's inbox key
|
|
119
|
+
is gone. Ships as the `create_durable_transport_messages` migration (also adds
|
|
120
|
+
the tasks `kind` column) or via `autoCreate`; without it the protocol is off and
|
|
121
|
+
said once through `onError`. Tune with `inboxTtlMs`, `tenantSubscribersCacheMs`,
|
|
122
|
+
`inboxSweepIntervalMs`. See `durable-cluster`.
|
|
123
|
+
|
|
78
124
|
Trade-off: throughput is bounded by polling + row contention — right for
|
|
79
125
|
workflow-scale modest rates, not high-fanout firehoses. Delivery is
|
|
80
|
-
at-least-once (
|
|
81
|
-
be idempotent on `step_id`. Source: `docs/transports/db.mdx`.
|
|
126
|
+
at-least-once (a worker that dies after its handler ran re-runs elsewhere), so
|
|
127
|
+
handlers should be idempotent on `step_id`. Source: `docs/transports/db.mdx`.
|
|
82
128
|
|
|
83
129
|
### Pattern 2 — real brokers (`queue`, `bullmq`) and stalled-claim tuning
|
|
84
130
|
|
|
@@ -91,9 +137,9 @@ export default defineConfig({
|
|
|
91
137
|
transports: {
|
|
92
138
|
queue: transports.queue({
|
|
93
139
|
connection: 'redis',
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
stalledThresholdMs:
|
|
140
|
+
// Claims are renewed while a step runs, so this bounds worker SILENCE
|
|
141
|
+
// (crash, or a blocked event loop), not step length. Default 2min:
|
|
142
|
+
stalledThresholdMs: 5 * 60 * 1000,
|
|
97
143
|
maxStalledCount: 3, // bounds a poison job
|
|
98
144
|
onError: (err) => logger.error({ err }, 'durable queue transport'),
|
|
99
145
|
}),
|
|
@@ -102,11 +148,37 @@ export default defineConfig({
|
|
|
102
148
|
```
|
|
103
149
|
|
|
104
150
|
One instance consumes engine-side results; run one or more `node ace
|
|
105
|
-
durable:work` workers serving the step names.
|
|
151
|
+
durable:work` workers serving the step names. A worker runs ONE job of a step
|
|
152
|
+
name at a time unless `concurrency` is raised (`transports.queue({ connection,
|
|
153
|
+
concurrency: 8, resultConcurrency: 8 })`); the slots share one poll loop.
|
|
154
|
+
`concurrency: 'adaptive'` (or `{ mode: 'adaptive', min, max, start, tickMs,
|
|
155
|
+
ramCeilingPct, cpuCeilingPct }`) runs the same controller as `bullmq` (below);
|
|
156
|
+
each step name's loop reads the live limit every poll. Idle
|
|
157
|
+
task/heartbeat/control loops back off up to `idleBackoffMax × pollIntervalMs`
|
|
158
|
+
(default 4 × 200ms; `1` disables).
|
|
159
|
+
|
|
160
|
+
A `queue` worker beats `<prefix>-worker-heartbeat:<token>:<instance>` into the
|
|
161
|
+
worker-key store (10s, live 35s) with its `WorkerStatus`, and the transport
|
|
162
|
+
implements `groupHealth` when a store is configured (the default with a database;
|
|
163
|
+
absent with `workerKeys: false`). Its `depth` is the PENDING count (`sizeOf`) only —
|
|
164
|
+
the adapter contract can't count jobs in flight or delayed retries. A task that
|
|
165
|
+
stalls past `maxStalledCount` is failed AND bridged into a retryable failed
|
|
166
|
+
`StepResult` (`remote step worker failed: stalled more than N times`) on
|
|
167
|
+
`@adonisjs/queue` 0.7; on 0.6 the adapter deletes it internally and nothing is
|
|
168
|
+
published, so rely on `timeoutMs` / `remoteRedispatchMs` there. Source:
|
|
106
169
|
`docs/transports/queue.mdx`.
|
|
107
170
|
|
|
171
|
+
`queue` also carries remote workflow turns (`<prefix>:decisions`,
|
|
172
|
+
`<prefix>:step-events`) and store-less tenant pods: start-run/run-request are
|
|
173
|
+
plain jobs to the control plane, replies go to a per-pod
|
|
174
|
+
`<prefix>:run-replies:<instanceId>` queue, and tenant events are pushed to each
|
|
175
|
+
subscribed pod's `<prefix>:tenant-events:<instanceId>` (subscriptions announced
|
|
176
|
+
on `<prefix>:inbox-registrations`, recorded in the control plane's key store;
|
|
177
|
+
tune with `inboxTtlMs`, `tenantSubscribersCacheMs`, `inboxSweepIntervalMs`).
|
|
178
|
+
See `durable-cluster`.
|
|
179
|
+
|
|
108
180
|
For wide fans (`ctx.all` over thousands of steps), raise how many results an
|
|
109
|
-
instance settles at once on `bullmq
|
|
181
|
+
instance settles at once on `bullmq`, `queue` or `db` (`resultConcurrency`),
|
|
110
182
|
let its workers size their step concurrency, and size the engine's due-timer
|
|
111
183
|
batch:
|
|
112
184
|
|
|
@@ -115,7 +187,7 @@ export default defineConfig({
|
|
|
115
187
|
timerBatchSize: 1000, // due runs per timer pass, most overdue first (default 500)
|
|
116
188
|
transports: {
|
|
117
189
|
bullmq: transports.bullmq({
|
|
118
|
-
connection: { host: '127.0.0.1', port: 6379 },
|
|
190
|
+
connection: { host: '127.0.0.1', port: 6379 }, // or 'redis://…' / 'rediss://…' (0.50+)
|
|
119
191
|
resultConcurrency: 16, // default 1
|
|
120
192
|
// A number pins each task queue's concurrency (default 1). 'adaptive' moves it
|
|
121
193
|
// every tickMs: +1 while steps saturate the limit at steady latency, down by the
|
|
@@ -133,6 +205,17 @@ work and progress); a non-finite or out-of-range field falls back to its default
|
|
|
133
205
|
throughput covers the trailing minute, decaying while the worker is idle and reading 0 once a minute passes with no completed step; with a fixed limit the heartbeat still reports `cpuPct`, sampled at most once every 2s. In adaptive mode the decisions, step-events, start-run and run-request consumers run
|
|
134
206
|
at 1 (results run at `resultConcurrency`). Sources: `docs/transports/index.mdx`, `docs/cluster/interop.mdx`.
|
|
135
207
|
|
|
208
|
+
Two `bullmq` safety nets: `pingIntervalMs` (default 30s; `0`/`false` off) PINGs
|
|
209
|
+
every pub/sub subscriber (heartbeats, run replies, tenant events) and reconnects
|
|
210
|
+
one a VPN/NAT/idle timeout dropped silently, so autoResubscribe restores it; each
|
|
211
|
+
subscriber also gets an `error` listener (reported once per burst to `onError`).
|
|
212
|
+
`stepTimeoutMs` (off by default) fails a step handler still running at the
|
|
213
|
+
ceiling as RETRYABLE, so a wedged `await` cannot hold its job forever (BullMQ
|
|
214
|
+
renews the lock on a timer, so the job never stalls). A `redis://` /
|
|
215
|
+
`rediss://` connection string is parsed into ioredis options since 0.50; before
|
|
216
|
+
0.50 it was silently ignored and every client connected to `127.0.0.1:6379`.
|
|
217
|
+
Source: `docs/transports/bullmq.mdx`.
|
|
218
|
+
|
|
136
219
|
### Pattern 3 — multi-replica fan-out: the control plane
|
|
137
220
|
|
|
138
221
|
The task transport is point-to-point; lifecycle events and cancellation need a
|
|
@@ -214,30 +297,56 @@ without it the engine loads the run's checkpoints per result), implement
|
|
|
214
297
|
(without it a blind save can put a settled step back to `pending`), and return
|
|
215
298
|
`listDueTimers` most overdue first, with or without a `limit`. Run
|
|
216
299
|
`runStateStoreContract` from `@adonis-agora/durable/testing/conformance`, which
|
|
217
|
-
covers all three and fails a store missing either method.
|
|
300
|
+
covers all three and fails a store missing either method. The contract also
|
|
301
|
+
requires `saveCheckpointsIfPending` (batched result settling), `deleteRuns`
|
|
302
|
+
(batched retention/purge deletes), `runFacets`, `pruneBufferedEvents`
|
|
303
|
+
(`eventBufferTtl`) and the `checkpointStats` + `queryCheckpoints` pair (the run
|
|
304
|
+
detail from aggregates, with an ETag the console polls with `If-None-Match`).
|
|
305
|
+
|
|
306
|
+
The Lucid schema check runs at every boot but is gated by a fingerprint row in
|
|
307
|
+
`durable_schema_meta`: a current schema costs two statements. A stale or missing
|
|
308
|
+
marker runs the full check under `pg_advisory_xact_lock` / MySQL `GET_LOCK`, so
|
|
309
|
+
pods booting together don't race `CREATE TABLE`. Bump `DURABLE_SCHEMA_REVISION`
|
|
310
|
+
with any DDL change; a hand change behind the marker needs `{ force: true }`. On
|
|
311
|
+
MySQL the JSON columns are `longtext` (an old `text` column is widened in place;
|
|
312
|
+
a table rebuild, so run the migration in a maintenance window for big tables),
|
|
313
|
+
and `stores.lucid({ collation })` aligns the tables to the app's collation. A
|
|
314
|
+
JSON column that does not parse reads back as `undefined` and is reported once
|
|
315
|
+
(`onCorruptJsonColumn`) instead of failing every read that touches the row.
|
|
316
|
+
|
|
317
|
+
To run singleton workflows a custom store also needs `tryAdmitSingleton` (one transaction that
|
|
318
|
+
locks before it reads, built on the exported `singletonAdmissionTags` /
|
|
319
|
+
`singletonAdmissionDenialPatch` including the pre-0.51 holder count, and a terminal `updateRun`/`updateRunIf` that
|
|
320
|
+
drops both singleton markers (`durable:singleton:admitted` and `durable:singleton:tracked`, via `withoutSingletonMarkers`)); without it singletons fall back
|
|
321
|
+
to the old non-atomic scan and the engine warns once naming `StateStore.tryAdmitSingleton`.
|
|
218
322
|
|
|
219
323
|
Source: `docs/stores/lucid.mdx`, `docs/stores/index.mdx`.
|
|
220
324
|
|
|
221
325
|
## Common mistakes
|
|
222
326
|
|
|
223
|
-
### HIGH
|
|
327
|
+
### HIGH blocking the event loop longer than `stalledThresholdMs`
|
|
224
328
|
|
|
225
329
|
Wrong:
|
|
226
330
|
|
|
227
331
|
```ts
|
|
228
|
-
|
|
332
|
+
// a step body that renders a report synchronously for ~3 minutes, default 2min threshold
|
|
333
|
+
transports.queue({ connection: 'redis' })
|
|
229
334
|
```
|
|
230
335
|
|
|
231
336
|
Correct:
|
|
232
337
|
|
|
233
338
|
```ts
|
|
234
|
-
transports.queue({ connection: 'redis', stalledThresholdMs:
|
|
339
|
+
transports.queue({ connection: 'redis', stalledThresholdMs: 5 * 60 * 1000 })
|
|
340
|
+
// or keep the heavy work async / off the main thread
|
|
235
341
|
```
|
|
236
342
|
|
|
237
|
-
Mechanism:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
343
|
+
Mechanism: the transport renews every in-flight claim each half threshold
|
|
344
|
+
(`adapter.renewJobs`), so a long but async step keeps its claim forever. A
|
|
345
|
+
synchronous body starves the renewal timer; once the claim goes unrenewed past
|
|
346
|
+
the threshold the sweep re-delivers it and the step runs twice — reported to
|
|
347
|
+
`onError` as `lost N of M claim(s)`. Raising the threshold to cover step LENGTH
|
|
348
|
+
is no longer needed: before 0.50 the `queue` transport never renewed claims
|
|
349
|
+
(30min default threshold, long steps re-delivered); upgrade instead.
|
|
241
350
|
Source: `docs/transports/queue.mdx` ("Stalled-job reclaim").
|
|
242
351
|
|
|
243
352
|
### HIGH hand-copying the DDL into the migration
|
|
@@ -309,7 +418,8 @@ transports.db({ partition: 'tenant-a' })
|
|
|
309
418
|
Mechanism: routing is by handler name — an instance serves whatever names it
|
|
310
419
|
registers, so there is no group to declare. Isolation comes from `partition`
|
|
311
420
|
(suffixes routing tokens `<name>@<partition>`) plus `namespace` (scopes which
|
|
312
|
-
runs this engine polls
|
|
421
|
+
runs this engine polls; absent = `'default'` on standalone, every pool on a
|
|
422
|
+
control plane). Source: `docs/transports/db.mdx` options table,
|
|
313
423
|
`docs/concepts/tenancy.mdx`.
|
|
314
424
|
|
|
315
425
|
### MEDIUM wrapping a store in CodecStateStore and expecting heartbeats
|
|
@@ -17,7 +17,7 @@ license: MIT
|
|
|
17
17
|
metadata:
|
|
18
18
|
type: core
|
|
19
19
|
library: "@adonis-agora/durable"
|
|
20
|
-
library_version: "0.
|
|
20
|
+
library_version: "0.51.0"
|
|
21
21
|
framework: adonisjs
|
|
22
22
|
sources:
|
|
23
23
|
- 'DavideCarvalho/adonis-durable:docs/authoring/app-workflows.mdx'
|
|
@@ -37,10 +37,17 @@ and registers every exported class — you never call `engine.register` by hand
|
|
|
37
37
|
(it remains the low-level escape hatch). The convention mirrors `@adonisjs/queue`
|
|
38
38
|
jobs under `app/jobs`; scaffold with `node ace make:workflow order`.
|
|
39
39
|
|
|
40
|
+
A modular app (one folder per bounded context) lists several directories:
|
|
41
|
+
`workflowsPath` and `stepsPath` in `config/durable.ts` take an array, and so
|
|
42
|
+
does the `source` option of the `workflowsHook` / `stepsHook` codegen hooks.
|
|
43
|
+
Names must be unique across those directories. To register one more directory
|
|
44
|
+
from a preload, call `registerWorkflowsFromDir(engine, dir)` and
|
|
45
|
+
`registerStepsFromDir(engine, dir)`. The engine serves steps through its
|
|
46
|
+
transport (`engine.handle`).
|
|
47
|
+
|
|
40
48
|
## Setup
|
|
41
49
|
|
|
42
50
|
```ts title="app/workflows/checkout_workflow.ts"
|
|
43
|
-
import { inject } from '@adonisjs/core'
|
|
44
51
|
import { BaseWorkflow } from '@adonis-agora/durable'
|
|
45
52
|
import type { WorkflowCtx } from '@adonis-agora/durable'
|
|
46
53
|
|
|
@@ -62,7 +69,8 @@ export default class CheckoutWorkflow extends BaseWorkflow {
|
|
|
62
69
|
```
|
|
63
70
|
|
|
64
71
|
Run it from anywhere: `CheckoutWorkflow.dispatch(input, { runId })` enqueues;
|
|
65
|
-
`CheckoutWorkflow.start(input)`
|
|
72
|
+
`CheckoutWorkflow.start(input)` blocks until the run is terminal and returns its
|
|
73
|
+
output. Both statics are
|
|
66
74
|
context-aware — inside a running body they become a linked child /
|
|
67
75
|
fire-and-forget child instead of a top-level run.
|
|
68
76
|
|
|
@@ -75,7 +83,7 @@ Steps are discovered from `app/steps` and served by name automatically. A
|
|
|
75
83
|
|
|
76
84
|
```ts title="app/steps/payment_steps.ts"
|
|
77
85
|
import { Step } from '@adonis-agora/durable'
|
|
78
|
-
import { z } from 'zod'
|
|
86
|
+
import { z } from 'zod' // any Standard Schema library: zod, valibot, arktype, …
|
|
79
87
|
|
|
80
88
|
export default class PaymentSteps {
|
|
81
89
|
@Step({
|
|
@@ -120,6 +128,26 @@ constructor as wiring; keep per-run state in input and step outputs, never on
|
|
|
120
128
|
`this`. Injected services are for **composing**, not doing work: reach them
|
|
121
129
|
through step refs so the work is checkpointed.
|
|
122
130
|
|
|
131
|
+
Schemas are **any Standard Schema** (zod 3.24+/4, valibot, arktype, effect
|
|
132
|
+
schema, …) — the package depends on none of them. They fit every schema slot:
|
|
133
|
+
`@Step({ input, output })` / `defineStep` (with an unannotated handler the types
|
|
134
|
+
are inferred from the schema), `static workflow = { inputSchema }` (validated at
|
|
135
|
+
`start`, nothing created on failure; the schema's output value is the run's
|
|
136
|
+
input), and `static workflow = { searchAttributes }` (every
|
|
137
|
+
`ctx.upsertSearchAttributes` is validated against the merged attributes; type
|
|
138
|
+
the ctx as `WorkflowCtx<InferSearchAttributes<typeof schema>>` and read back with
|
|
139
|
+
`attributesOf(WorkflowClass, run)`). A failure is a `SchemaValidationError`
|
|
140
|
+
(`FatalError`, code `schema_validation`, issues in `details.issues`) — never
|
|
141
|
+
retried.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import * as v from 'valibot'
|
|
145
|
+
|
|
146
|
+
export const charge = defineStep('billing:charge', async (input) => input.amount * 100, {
|
|
147
|
+
input: v.object({ amount: v.number() }), // input: { amount: number }, inferred
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
123
151
|
### Pattern 2 — pausing durably: sleeps, signals, events
|
|
124
152
|
|
|
125
153
|
All three suspend the run with zero compute and survive restarts.
|
|
@@ -151,15 +179,12 @@ events with `{ onEvent: ['payment.settled'] }`; coalesce chatty sources with
|
|
|
151
179
|
`{ mode: 'batch', maxSize: 50, windowMs: 60_000 }` (receives `{ events: [...] }`).
|
|
152
180
|
|
|
153
181
|
```ts title="app/controllers/approvals_controller.ts"
|
|
154
|
-
import {
|
|
155
|
-
import
|
|
182
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
183
|
+
import engine from '@adonis-agora/durable/services/main'
|
|
156
184
|
|
|
157
|
-
@inject()
|
|
158
185
|
export default class ApprovalsController {
|
|
159
|
-
constructor(private engine: WorkflowEngine) {}
|
|
160
|
-
|
|
161
186
|
async approve({ params, request, response }: HttpContext) {
|
|
162
|
-
await
|
|
187
|
+
await engine.signal(`approve:${params.id}`, request.body())
|
|
163
188
|
return response.noContent()
|
|
164
189
|
}
|
|
165
190
|
}
|
|
@@ -181,9 +206,9 @@ use `ctx.allSettled(Workflow, inputs, { concurrency })` instead: the same fan,
|
|
|
181
206
|
resolving with `{ status: 'fulfilled', value } | { status: 'rejected', id, error, code?, details? }`
|
|
182
207
|
per input and never throwing for a child. Child ids derive from the call position and
|
|
183
208
|
are replay-stable; never pass a random one. `{ concurrency: n }` caps how many
|
|
184
|
-
children run at once (waves of `floor(n / 2)`, two at a time)
|
|
185
|
-
id), so changing it for an in-flight
|
|
186
|
-
`inputs`.
|
|
209
|
+
children run at once (waves of `floor(n / 2)`, two at a time). The wave is part of
|
|
210
|
+
each child id (`<runId>.all.<firstSeq>.w<k>.<i>`), so changing it for an in-flight
|
|
211
|
+
run is non-deterministic, like changing `inputs`.
|
|
187
212
|
|
|
188
213
|
For the same step over many inputs, pass the step reference (`defineStep` ref
|
|
189
214
|
or `@Step` method) to `ctx.all`. It
|
|
@@ -203,8 +228,10 @@ Prefer it to `Promise.all` past a handful of steps (every result of a
|
|
|
203
228
|
child-workflow `ctx.all` when each item is just a step. A `queue` or a
|
|
204
229
|
`timeoutMs` is rejected on a step fan.
|
|
205
230
|
|
|
206
|
-
Serialize runs per subject with a singleton — runs sharing a key queue FIFO
|
|
207
|
-
|
|
231
|
+
Serialize runs per subject with a singleton — runs sharing a key queue FIFO.
|
|
232
|
+
Admission is an atomic store claim (`StateStore.tryAdmitSingleton`, built into the
|
|
233
|
+
Lucid and in-memory stores), and a holder keeps its slot until it settles
|
|
234
|
+
terminally, so replicas never over-admit and a later start never displaces it:
|
|
208
235
|
|
|
209
236
|
```ts
|
|
210
237
|
export default class SyncShopWorkflow extends BaseWorkflow {
|
|
@@ -354,7 +381,7 @@ each replay breaks that dedup and re-fires the child. Source:
|
|
|
354
381
|
Wrong:
|
|
355
382
|
|
|
356
383
|
```ts
|
|
357
|
-
const result = await CheckoutWorkflow.start(order) // request held open until
|
|
384
|
+
const result = await CheckoutWorkflow.start(order) // request held open until the run is terminal
|
|
358
385
|
```
|
|
359
386
|
|
|
360
387
|
Correct:
|
|
@@ -364,8 +391,9 @@ const { runId } = await CheckoutWorkflow.dispatch(order, { runId: `checkout:${or
|
|
|
364
391
|
return response.accepted({ runId })
|
|
365
392
|
```
|
|
366
393
|
|
|
367
|
-
Mechanism: `.start` resolves only when the run reaches a terminal state
|
|
368
|
-
|
|
394
|
+
Mechanism: `.start` resolves only when the run reaches a terminal state — it
|
|
395
|
+
keeps waiting through every sleep/signal suspension, hours later for a long
|
|
396
|
+
workflow. Source: `docs/getting-started.mdx`
|
|
369
397
|
(warning callout).
|
|
370
398
|
|
|
371
399
|
### MEDIUM awaiting a webhook with no timeout
|
|
@@ -419,5 +447,30 @@ caller suspended on `ctx.callEntity`; model expected rejections as returned
|
|
|
419
447
|
discriminated results and reserve throws for genuine programmer errors.
|
|
420
448
|
Source: `docs/authoring/entities.mdx` (warning callout).
|
|
421
449
|
|
|
450
|
+
### MEDIUM the same workflow or step name in two discovery directories
|
|
451
|
+
|
|
452
|
+
Wrong:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
// config/durable.ts — both contexts declare a workflow named 'charge' (version 1)
|
|
456
|
+
export default defineConfig({
|
|
457
|
+
workflowsPath: ['app/orders/flows', 'app/billing/flows'],
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Correct:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// app/billing/flows/charge_workflow.ts
|
|
465
|
+
static workflow = { name: 'billing.charge', version: '1' }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Mechanism: names route runs and steps, so with several `workflowsPath` /
|
|
469
|
+
`stepsPath` directories boot throws `DuplicateDiscoveryError` naming both files
|
|
470
|
+
when two directories define the same workflow name + version or step name with
|
|
471
|
+
different classes. Prefix names by context. The hooks' `source` list must name
|
|
472
|
+
the same directories, otherwise the barrel and the runtime scan disagree.
|
|
473
|
+
Source: `docs/authoring/app-workflows.mdx` ("Several directories").
|
|
474
|
+
|
|
422
475
|
See also: `durable-determinism` — why the body must stay deterministic and how
|
|
423
476
|
to change it safely once live.
|