@adonis-agora/durable 0.10.0 → 0.12.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 +24 -0
- package/dist/assets/dashboard.html +116 -3
- package/dist/commands/durable_worker.d.ts +25 -0
- package/dist/commands/durable_worker.d.ts.map +1 -0
- package/dist/commands/durable_worker.js +113 -0
- package/dist/commands/durable_worker.js.map +1 -0
- package/dist/commands/main.d.ts +6 -5
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/commands/main.js +11 -4
- package/dist/commands/main.js.map +1 -1
- package/dist/providers/dashboard_provider.d.ts +37 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +98 -9
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/providers/durable_provider.d.ts +24 -16
- package/dist/providers/durable_provider.d.ts.map +1 -1
- package/dist/providers/durable_provider.js +205 -21
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/config_types.d.ts +92 -0
- package/dist/src/config_types.d.ts.map +1 -0
- package/dist/src/config_types.js +2 -0
- package/dist/src/config_types.js.map +1 -0
- package/dist/src/dashboard/compat-view.d.ts +13 -0
- package/dist/src/dashboard/compat-view.d.ts.map +1 -0
- package/dist/src/dashboard/compat-view.js +32 -0
- package/dist/src/dashboard/compat-view.js.map +1 -0
- package/dist/src/dashboard/compat.d.ts +76 -0
- package/dist/src/dashboard/compat.d.ts.map +1 -0
- package/dist/src/dashboard/compat.js +130 -0
- package/dist/src/dashboard/compat.js.map +1 -0
- package/dist/src/dashboard/diagnostics-recorder.d.ts +59 -0
- package/dist/src/dashboard/diagnostics-recorder.d.ts.map +1 -0
- package/dist/src/dashboard/diagnostics-recorder.js +75 -0
- package/dist/src/dashboard/diagnostics-recorder.js.map +1 -0
- package/dist/src/dashboard/gateway-adapter.d.ts +37 -0
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -0
- package/dist/src/dashboard/gateway-adapter.js +49 -0
- package/dist/src/dashboard/gateway-adapter.js.map +1 -0
- package/dist/src/dashboard/handlers.d.ts +31 -8
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +2 -1
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +10 -3
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +8 -2
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/define_config.d.ts +40 -5
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +4 -3
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/dispatch-routing.d.ts +105 -0
- package/dist/src/dispatch-routing.d.ts.map +1 -0
- package/dist/src/dispatch-routing.js +114 -0
- package/dist/src/dispatch-routing.js.map +1 -0
- package/dist/src/engine.d.ts +65 -2
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +131 -3
- package/dist/src/engine.js.map +1 -1
- package/dist/src/handshake/descriptor.d.ts +128 -0
- package/dist/src/handshake/descriptor.d.ts.map +1 -0
- package/dist/src/handshake/descriptor.js +133 -0
- package/dist/src/handshake/descriptor.js.map +1 -0
- package/dist/src/handshake/negotiate.d.ts +83 -0
- package/dist/src/handshake/negotiate.d.ts.map +1 -0
- package/dist/src/handshake/negotiate.js +108 -0
- package/dist/src/handshake/negotiate.js.map +1 -0
- package/dist/src/handshake/routing.d.ts +51 -0
- package/dist/src/handshake/routing.d.ts.map +1 -0
- package/dist/src/handshake/routing.js +57 -0
- package/dist/src/handshake/routing.js.map +1 -0
- package/dist/src/index.d.ts +14 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +12 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +168 -2
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/remote-workflow-executor.d.ts +25 -1
- package/dist/src/remote-workflow-executor.d.ts.map +1 -1
- package/dist/src/remote-workflow-executor.js +41 -0
- package/dist/src/remote-workflow-executor.js.map +1 -1
- package/dist/src/role_bindings.d.ts +85 -0
- package/dist/src/role_bindings.d.ts.map +1 -0
- package/dist/src/role_bindings.js +73 -0
- package/dist/src/role_bindings.js.map +1 -0
- package/dist/src/run-gateway/interface.d.ts +89 -0
- package/dist/src/run-gateway/interface.d.ts.map +1 -0
- package/dist/src/run-gateway/interface.js +2 -0
- package/dist/src/run-gateway/interface.js.map +1 -0
- package/dist/src/run-gateway/proxy-run-gateway.d.ts +84 -0
- package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -0
- package/dist/src/run-gateway/proxy-run-gateway.js +142 -0
- package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -0
- package/dist/src/run-gateway/run-request-responder.d.ts +67 -0
- package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -0
- package/dist/src/run-gateway/run-request-responder.js +228 -0
- package/dist/src/run-gateway/run-request-responder.js.map +1 -0
- package/dist/src/run-gateway/store-run-gateway.d.ts +90 -0
- package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -0
- package/dist/src/run-gateway/store-run-gateway.js +80 -0
- package/dist/src/run-gateway/store-run-gateway.js.map +1 -0
- package/dist/src/run-gateway/tenant-auth.d.ts +19 -0
- package/dist/src/run-gateway/tenant-auth.d.ts.map +1 -0
- package/dist/src/run-gateway/tenant-auth.js +71 -0
- package/dist/src/run-gateway/tenant-auth.js.map +1 -0
- package/dist/src/services/main.d.ts +16 -3
- package/dist/src/services/main.d.ts.map +1 -1
- package/dist/src/services/main.js +23 -4
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/step-name-symbol.d.ts +3 -0
- package/dist/src/step-name-symbol.d.ts.map +1 -1
- package/dist/src/step-name-symbol.js.map +1 -1
- package/dist/src/step-ref.d.ts +4 -0
- package/dist/src/step-ref.d.ts.map +1 -1
- package/dist/src/step-ref.js +2 -0
- package/dist/src/step-ref.js.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts +130 -0
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -0
- package/dist/src/transports/bullmq/bullmq-transport.js +565 -0
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -0
- package/dist/src/transports/bullmq/deps.d.ts +59 -0
- package/dist/src/transports/bullmq/deps.d.ts.map +1 -0
- package/dist/src/transports/bullmq/deps.js +41 -0
- package/dist/src/transports/bullmq/deps.js.map +1 -0
- package/dist/src/transports/bullmq/index.d.ts +5 -0
- package/dist/src/transports/bullmq/index.d.ts.map +1 -0
- package/dist/src/transports/bullmq/index.js +5 -0
- package/dist/src/transports/bullmq/index.js.map +1 -0
- package/dist/src/transports/bullmq/naming.d.ts +71 -0
- package/dist/src/transports/bullmq/naming.d.ts.map +1 -0
- package/dist/src/transports/bullmq/naming.js +101 -0
- package/dist/src/transports/bullmq/naming.js.map +1 -0
- package/dist/src/transports/bullmq/serialization.d.ts +56 -0
- package/dist/src/transports/bullmq/serialization.d.ts.map +1 -0
- package/dist/src/transports/bullmq/serialization.js +101 -0
- package/dist/src/transports/bullmq/serialization.js.map +1 -0
- package/dist/src/transports/factory.d.ts +31 -0
- package/dist/src/transports/factory.d.ts.map +1 -1
- package/dist/src/transports/factory.js +21 -0
- package/dist/src/transports/factory.js.map +1 -1
- package/dist/src/worker-runtime/index.d.ts +22 -0
- package/dist/src/worker-runtime/index.d.ts.map +1 -0
- package/dist/src/worker-runtime/index.js +27 -0
- package/dist/src/worker-runtime/index.js.map +1 -0
- package/dist/src/worker-runtime/naming.d.ts +24 -0
- package/dist/src/worker-runtime/naming.d.ts.map +1 -0
- package/dist/src/worker-runtime/naming.js +28 -0
- package/dist/src/worker-runtime/naming.js.map +1 -0
- package/dist/src/worker-runtime/registry.d.ts +81 -0
- package/dist/src/worker-runtime/registry.d.ts.map +1 -0
- package/dist/src/worker-runtime/registry.js +44 -0
- package/dist/src/worker-runtime/registry.js.map +1 -0
- package/dist/src/worker-runtime/worker-runtime.d.ts +155 -0
- package/dist/src/worker-runtime/worker-runtime.d.ts.map +1 -0
- package/dist/src/worker-runtime/worker-runtime.js +267 -0
- package/dist/src/worker-runtime/worker-runtime.js.map +1 -0
- package/dist/src/workflow-ctx.d.ts.map +1 -1
- package/dist/src/workflow-ctx.js +3 -0
- package/dist/src/workflow-ctx.js.map +1 -1
- package/dist/src/workflow-turn.d.ts +167 -0
- package/dist/src/workflow-turn.d.ts.map +1 -0
- package/dist/src/workflow-turn.js +380 -0
- package/dist/src/workflow-turn.js.map +1 -0
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @adonis-agora/durable
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#15](https://github.com/DavideCarvalho/adonis-durable/pull/15) [`ae3fbee`](https://github.com/DavideCarvalho/adonis-durable/commit/ae3fbee7e00a32ff9d2463d616aeea8a1a5ac566) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - O provider agora **provisiona o schema do store no boot** por padrão — o durable passa a gerenciar as próprias tabelas, como o resto do ecossistema (agent/authz/telescope), em vez de exigir uma migration.
|
|
8
|
+
|
|
9
|
+
Novo `autoSchema?: boolean` no config (default `true`): quando ligado, `DurableProvider` chama `store.ensureSchema()` ao resolver o store (idempotente, `CREATE TABLE IF NOT EXISTS`; o store lucid resolve o db pelo alias `'lucid.db'`, disponível no boot). O store in-memory não tem schema, então é no-op.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// dev/prod: sem migration, a lib cria as tabelas
|
|
13
|
+
export default defineConfig({ store: 'lucid', stores: { lucid: stores.lucid({ connection: 'main' }) } })
|
|
14
|
+
|
|
15
|
+
// opt-out: gerencie via migration com createDurableTables(db, connection)
|
|
16
|
+
export default defineConfig({ autoSchema: false, store: 'lucid', stores: { lucid: stores.lucid(...) } })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Mudança de comportamento:** apps existentes que criavam as tabelas via migration passam a também provisioná-las no boot (idempotente — as tabelas já existentes são um no-op). Para manter o comportamento anterior (só migration, sem DDL no boot), setar `autoSchema: false`.
|
|
20
|
+
|
|
21
|
+
## 0.11.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- Store-less cluster + cross-ecosystem interop: separate API from engine and run store-less "thin" pods that only talk to the control plane. New role-discriminated config (`standalone` / `control-plane` / `tenant`, `store?: never` on tenant → compile-time isolation), an aviary byte-compatible BullMQ transport (a Python aviary worker can share the same control plane, proven live both directions), the P4 RunGateway (Store/Proxy/Responder) request/reply protocol with layered tenant auth, a store-less WorkerRuntime running steps + workflow turns + parallel `gather` (subpath `@adonis-agora/durable/worker`, no Lucid), a worker-descriptor handshake with capability/protocol negotiation + capability-aware dispatch (park `blocked` instead of hanging), and a dashboard fleet-health panel. Adds `bullmq` as a dependency.
|
|
26
|
+
|
|
3
27
|
## 0.10.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
|
@@ -177,12 +177,26 @@
|
|
|
177
177
|
font-size: 12.5px;
|
|
178
178
|
}
|
|
179
179
|
.health .h-card.stalled { border-color: var(--red); }
|
|
180
|
+
.nav { display: flex; gap: 16px; }
|
|
181
|
+
.nav a { color: var(--muted); font-size: 13px; }
|
|
182
|
+
.nav a:hover, .nav a.active { color: var(--text); }
|
|
183
|
+
/* Fleet-health / protocol-compat badges — mirror src/dashboard/compat-view.ts outcomeClass(). */
|
|
184
|
+
.badge.c-ok { color: var(--green); background: rgba(63,185,80,0.12); border-color: rgba(63,185,80,0.3); }
|
|
185
|
+
.badge.c-degraded { color: var(--amber); background: rgba(210,153,34,0.12); border-color: rgba(210,153,34,0.3); }
|
|
186
|
+
.badge.c-bad { color: var(--red); background: rgba(248,81,73,0.12); border-color: rgba(248,81,73,0.3); }
|
|
187
|
+
.panel.flagged-panel { border-color: var(--red); }
|
|
188
|
+
tr.flagged { background: rgba(248,81,73,0.06); }
|
|
189
|
+
.flag-reason { color: var(--red); }
|
|
180
190
|
</style>
|
|
181
191
|
</head>
|
|
182
192
|
<body>
|
|
183
193
|
<header>
|
|
184
194
|
<h1><span class="dot">●</span> Durable</h1>
|
|
185
195
|
<span class="muted mono" id="crumb">runs</span>
|
|
196
|
+
<nav class="nav">
|
|
197
|
+
<a href="#" id="nav-runs">Runs</a>
|
|
198
|
+
<a href="#/health" id="nav-health">Fleet health</a>
|
|
199
|
+
</nav>
|
|
186
200
|
<span class="spacer"></span>
|
|
187
201
|
<span class="muted" id="health-strip"></span>
|
|
188
202
|
</header>
|
|
@@ -414,6 +428,98 @@
|
|
|
414
428
|
if (childrenPanel) app.appendChild(childrenPanel);
|
|
415
429
|
}
|
|
416
430
|
|
|
431
|
+
// Mirrors src/dashboard/compat-view.ts — keep the two in lockstep (unit-tested there).
|
|
432
|
+
const OUTCOME = {
|
|
433
|
+
compatible: { cls: 'c-ok', label: 'compatible' },
|
|
434
|
+
degraded: { cls: 'c-degraded', label: 'degraded' },
|
|
435
|
+
incompatible: { cls: 'c-bad', label: 'incompatible' },
|
|
436
|
+
};
|
|
437
|
+
const protoRange = (r) =>
|
|
438
|
+
!r ? '—' : (r[0] === r[1] ? 'v' + r[0] : 'v' + r[0] + '–' + r[1]);
|
|
439
|
+
|
|
440
|
+
async function renderCompat() {
|
|
441
|
+
document.getElementById('crumb').textContent = 'fleet health';
|
|
442
|
+
const app = document.getElementById('app');
|
|
443
|
+
let data;
|
|
444
|
+
try {
|
|
445
|
+
data = await api('/compat');
|
|
446
|
+
} catch (e) {
|
|
447
|
+
app.innerHTML = '';
|
|
448
|
+
app.appendChild(el('div', { class: 'empty' }, 'Failed to load fleet health: ' + e.message));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const cp = data.controlPlane;
|
|
453
|
+
const cpPanel = el('div', { class: 'panel' }, [
|
|
454
|
+
el('h2', {}, 'Control plane'),
|
|
455
|
+
el('dl', { class: 'kv' }, [
|
|
456
|
+
el('dt', {}, 'Instance'), el('dd', {}, el('code', {}, cp.instanceId)),
|
|
457
|
+
el('dt', {}, 'Protocol'), el('dd', {}, protoRange(cp.protocolRange)),
|
|
458
|
+
el('dt', {}, 'Capabilities'),
|
|
459
|
+
el('dd', { class: 'muted' }, (cp.capabilities || []).join(', ') || '—'),
|
|
460
|
+
]),
|
|
461
|
+
]);
|
|
462
|
+
|
|
463
|
+
const groupPanels = data.groups.map((g) => {
|
|
464
|
+
const rows = g.pods.map((p) => {
|
|
465
|
+
const o = OUTCOME[p.outcome] || { cls: '', label: p.outcome };
|
|
466
|
+
return el('tr', p.incompatible ? { class: 'flagged' } : {}, [
|
|
467
|
+
el('td', {}, el('code', {}, p.instanceId)),
|
|
468
|
+
el('td', { class: 'muted' }, p.runtime),
|
|
469
|
+
el('td', {}, protoRange(p.protocolRange)),
|
|
470
|
+
el('td', {}, el('span', { class: 'badge ' + o.cls }, o.label)),
|
|
471
|
+
el('td', { class: p.incompatible ? 'flag-reason' : 'muted' }, p.reason || '—'),
|
|
472
|
+
]);
|
|
473
|
+
});
|
|
474
|
+
return el('div', { class: 'panel' + (g.incompatible ? ' flagged-panel' : '') }, [
|
|
475
|
+
el('h2', {}, (g.incompatible ? '⚠ ' : '') + g.token),
|
|
476
|
+
el('table', {}, [
|
|
477
|
+
el('thead', {}, el('tr', {}, [
|
|
478
|
+
el('th', {}, 'Pod'), el('th', {}, 'Runtime'), el('th', {}, 'Protocol'),
|
|
479
|
+
el('th', {}, 'Negotiated'), el('th', {}, 'Reason'),
|
|
480
|
+
])),
|
|
481
|
+
el('tbody', {}, rows),
|
|
482
|
+
]),
|
|
483
|
+
]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const blockedRows = data.blocked.map((b) =>
|
|
487
|
+
el('tr', { onclick: () => navigate(b.id) }, [
|
|
488
|
+
el('td', {}, el('code', {}, b.id)),
|
|
489
|
+
el('td', {}, b.workflow || '—'),
|
|
490
|
+
el('td', { class: 'flag-reason' }, b.reason),
|
|
491
|
+
el('td', { class: 'muted' }, (b.requires && b.requires.length) ? b.requires.join(', ') : '—'),
|
|
492
|
+
el('td', { class: 'muted mono' }, fmt(b.updatedAt)),
|
|
493
|
+
]));
|
|
494
|
+
const blockedPanel = el('div', { class: 'panel' + (data.blocked.length ? ' flagged-panel' : '') }, [
|
|
495
|
+
el('h2', {}, (data.blocked.length ? '⚠ ' : '') + 'Blocked runs (' + data.blocked.length + ')'),
|
|
496
|
+
data.blocked.length
|
|
497
|
+
? el('table', {}, [
|
|
498
|
+
el('thead', {}, el('tr', {}, [
|
|
499
|
+
el('th', {}, 'Run id'), el('th', {}, 'Workflow'), el('th', {}, 'Reason'),
|
|
500
|
+
el('th', {}, 'Requires'), el('th', {}, 'Updated'),
|
|
501
|
+
])),
|
|
502
|
+
el('tbody', {}, blockedRows),
|
|
503
|
+
])
|
|
504
|
+
: el('div', { class: 'muted' }, 'No blocked runs — every run has a compatible worker.'),
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
app.innerHTML = '';
|
|
508
|
+
app.appendChild(el('a', {
|
|
509
|
+
class: 'back', href: '#',
|
|
510
|
+
onclick: (e) => { e.preventDefault(); navigate(''); },
|
|
511
|
+
}, '← all runs'));
|
|
512
|
+
app.appendChild(cpPanel);
|
|
513
|
+
for (const panel of (data.groups.length ? groupPanels
|
|
514
|
+
: [el('div', { class: 'panel' }, [
|
|
515
|
+
el('h2', {}, 'Workers'),
|
|
516
|
+
el('div', { class: 'muted' }, 'No live worker descriptors advertised.'),
|
|
517
|
+
])])) {
|
|
518
|
+
app.appendChild(panel);
|
|
519
|
+
}
|
|
520
|
+
app.appendChild(blockedPanel);
|
|
521
|
+
}
|
|
522
|
+
|
|
417
523
|
async function act(action, id) {
|
|
418
524
|
try {
|
|
419
525
|
await api('/runs/' + encodeURIComponent(id) + '/' + action, {
|
|
@@ -433,9 +539,16 @@
|
|
|
433
539
|
|
|
434
540
|
function route() {
|
|
435
541
|
const hash = location.hash.replace(/^#/, '');
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
542
|
+
const onHealth = hash === '/health';
|
|
543
|
+
document.getElementById('nav-health').className = onHealth ? 'active' : '';
|
|
544
|
+
document.getElementById('nav-runs').className = onHealth ? '' : 'active';
|
|
545
|
+
if (onHealth) {
|
|
546
|
+
renderCompat();
|
|
547
|
+
} else {
|
|
548
|
+
const m = /^\/runs\/(.+)$/.exec(hash);
|
|
549
|
+
if (m) renderDetail(decodeURIComponent(m[1]));
|
|
550
|
+
else renderList();
|
|
551
|
+
}
|
|
439
552
|
refreshHealth();
|
|
440
553
|
}
|
|
441
554
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* `node ace durable:worker` — the **store-less** task-consumer loop for a `role: 'tenant'` worker pod
|
|
5
|
+
* (design §3). Distinct from `durable:work` (the store-backed dispatch/recovery/timers loop, which
|
|
6
|
+
* this command deliberately does NOT clobber): a store-less worker owns no durable state — it consumes
|
|
7
|
+
* `${P}-tasks-<token>` tasks (executed through the shared `runStepHandler`, results published) and
|
|
8
|
+
* advertises its {@link import('../src/handshake/descriptor.js').WorkerDescriptor} + heartbeat. Stays
|
|
9
|
+
* alive until SIGINT/SIGTERM, then drains.
|
|
10
|
+
*
|
|
11
|
+
* On a `role: 'tenant'` pod it resolves the **container-bound {@link WorkerRuntime}** the provider wired
|
|
12
|
+
* (design §5): that runtime shares the tenant transport with the `ProxyRunGateway` and carries a
|
|
13
|
+
* `RedisWorkerRegistry` built from the transport's Redis connection, so descriptor + heartbeat are
|
|
14
|
+
* actually published on the shared Redis. Only when no bound runtime exists (a non-tenant role, or a
|
|
15
|
+
* misconfigured pod) does it fall back to building a self-contained runtime from `config/durable.ts`
|
|
16
|
+
* with a {@link NoopWorkerRegistry} (advertising is then a no-op).
|
|
17
|
+
*/
|
|
18
|
+
export default class DurableWorker extends BaseCommand {
|
|
19
|
+
#private;
|
|
20
|
+
static commandName: string;
|
|
21
|
+
static description: string;
|
|
22
|
+
static options: CommandOptions;
|
|
23
|
+
run(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=durable_worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable_worker.d.ts","sourceRoot":"","sources":["../../commands/durable_worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAU/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,WAAW;;IACpD,OAAgB,WAAW,SAAoB;IAC/C,OAAgB,WAAW,SACsE;IACjG,OAAgB,OAAO,EAAE,cAAc,CAAwC;IAEhE,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA+GpC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import { DURABLE_WORKER_RUNTIME } from '../src/role_bindings.js';
|
|
3
|
+
import { InMemoryTransport } from '../src/testing/in-memory-transport.js';
|
|
4
|
+
import { NoopWorkerRegistry } from '../src/worker-runtime/index.js';
|
|
5
|
+
import { registerStepsFromDir } from '../src/worker-runtime/index.js';
|
|
6
|
+
import { WorkerRuntime } from '../src/worker-runtime/index.js';
|
|
7
|
+
import { discoverWorkflows } from '../src/workflow-discovery.js';
|
|
8
|
+
/**
|
|
9
|
+
* `node ace durable:worker` — the **store-less** task-consumer loop for a `role: 'tenant'` worker pod
|
|
10
|
+
* (design §3). Distinct from `durable:work` (the store-backed dispatch/recovery/timers loop, which
|
|
11
|
+
* this command deliberately does NOT clobber): a store-less worker owns no durable state — it consumes
|
|
12
|
+
* `${P}-tasks-<token>` tasks (executed through the shared `runStepHandler`, results published) and
|
|
13
|
+
* advertises its {@link import('../src/handshake/descriptor.js').WorkerDescriptor} + heartbeat. Stays
|
|
14
|
+
* alive until SIGINT/SIGTERM, then drains.
|
|
15
|
+
*
|
|
16
|
+
* On a `role: 'tenant'` pod it resolves the **container-bound {@link WorkerRuntime}** the provider wired
|
|
17
|
+
* (design §5): that runtime shares the tenant transport with the `ProxyRunGateway` and carries a
|
|
18
|
+
* `RedisWorkerRegistry` built from the transport's Redis connection, so descriptor + heartbeat are
|
|
19
|
+
* actually published on the shared Redis. Only when no bound runtime exists (a non-tenant role, or a
|
|
20
|
+
* misconfigured pod) does it fall back to building a self-contained runtime from `config/durable.ts`
|
|
21
|
+
* with a {@link NoopWorkerRegistry} (advertising is then a no-op).
|
|
22
|
+
*/
|
|
23
|
+
export default class DurableWorker extends BaseCommand {
|
|
24
|
+
static commandName = 'durable:worker';
|
|
25
|
+
static description = 'Run the store-less durable worker (consume tasks, execute step bodies, advertise descriptor)';
|
|
26
|
+
static options = { startApp: true, staysAlive: true };
|
|
27
|
+
async run() {
|
|
28
|
+
const config = this.app.config.get('durable', {});
|
|
29
|
+
if (config.role !== 'tenant') {
|
|
30
|
+
this.logger.warning(`durable:worker is the store-less worker for role 'tenant'; config.role is '${config.role ?? 'standalone'}'. Continuing, but a store-backed role should use durable:work.`);
|
|
31
|
+
}
|
|
32
|
+
const { runtime, partition } = await this.#resolveRuntime(config);
|
|
33
|
+
// Register `app/steps` (served by the transport) + advertise `app/workflows` names.
|
|
34
|
+
if (config.stepsPath !== false) {
|
|
35
|
+
const stepsDir = this.app.makePath(config.stepsPath ?? 'app/steps');
|
|
36
|
+
const steps = await registerStepsFromDir(runtime, stepsDir);
|
|
37
|
+
this.logger.info(`registered ${steps.length} step handler(s) from ${config.stepsPath ?? 'app/steps'}`);
|
|
38
|
+
}
|
|
39
|
+
if (config.workflowsPath !== false) {
|
|
40
|
+
const workflowsDir = this.app.makePath(config.workflowsPath ?? 'app/workflows');
|
|
41
|
+
const workflows = await discoverWorkflows(workflowsDir);
|
|
42
|
+
runtime.registerWorkflowNames(workflows.map((w) => w.meta.name));
|
|
43
|
+
this.logger.info(`advertising ${workflows.length} workflow name(s) from ${config.workflowsPath ?? 'app/workflows'}`);
|
|
44
|
+
}
|
|
45
|
+
let stop;
|
|
46
|
+
const stopSignal = new Promise((resolve) => {
|
|
47
|
+
stop = resolve;
|
|
48
|
+
});
|
|
49
|
+
const onSignal = () => {
|
|
50
|
+
this.logger.info('shutdown signal received — stopping store-less worker…');
|
|
51
|
+
stop();
|
|
52
|
+
};
|
|
53
|
+
process.once('SIGINT', onSignal);
|
|
54
|
+
process.once('SIGTERM', onSignal);
|
|
55
|
+
await runtime.start();
|
|
56
|
+
this.logger.info(`durable:worker started (partition ${partition})`);
|
|
57
|
+
await stopSignal;
|
|
58
|
+
await runtime.stop();
|
|
59
|
+
process.off('SIGINT', onSignal);
|
|
60
|
+
process.off('SIGTERM', onSignal);
|
|
61
|
+
await this.terminate();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Prefer the provider-bound {@link WorkerRuntime} (the `role: 'tenant'` branch of `durable_provider`):
|
|
65
|
+
* it shares the tenant transport with the `ProxyRunGateway` and advertises through a real
|
|
66
|
+
* `RedisWorkerRegistry`. Fall back to a self-built runtime (a {@link NoopWorkerRegistry}) only when the
|
|
67
|
+
* container has no bound runtime — a non-tenant role, or a pod running this command by mistake.
|
|
68
|
+
*/
|
|
69
|
+
async #resolveRuntime(config) {
|
|
70
|
+
if (this.app.container.hasBinding(DURABLE_WORKER_RUNTIME)) {
|
|
71
|
+
const runtime = await this.app.container.make(DURABLE_WORKER_RUNTIME);
|
|
72
|
+
const partition = config.partition ?? config.namespace ?? 'default';
|
|
73
|
+
return { runtime, partition };
|
|
74
|
+
}
|
|
75
|
+
return this.#buildFallbackRuntime(config);
|
|
76
|
+
}
|
|
77
|
+
/** Build a self-contained runtime from `config/durable.ts` (no bound runtime available). Advertising is
|
|
78
|
+
* a no-op ({@link NoopWorkerRegistry}) — the descriptor is still built + observable, just unpublished. */
|
|
79
|
+
async #buildFallbackRuntime(config) {
|
|
80
|
+
const transport = await this.#resolveTransport(config);
|
|
81
|
+
if (typeof transport.handle !== 'function') {
|
|
82
|
+
throw new Error(`@agora/durable: the selected transport ("${config.transport ?? 'memory'}") cannot serve handlers (no handle()), so a store-less worker has nothing to consume.`);
|
|
83
|
+
}
|
|
84
|
+
transport.useNamespace?.(config.namespace ?? 'default');
|
|
85
|
+
const partition = config.partition ?? config.namespace ?? 'default';
|
|
86
|
+
const runtime = new WorkerRuntime({
|
|
87
|
+
transport: transport,
|
|
88
|
+
partition,
|
|
89
|
+
...(config.namespace !== undefined ? { namespace: config.namespace } : {}),
|
|
90
|
+
...(config.instanceId !== undefined ? { instanceId: config.instanceId } : {}),
|
|
91
|
+
...(config.capabilities !== undefined
|
|
92
|
+
? { capabilities: config.capabilities }
|
|
93
|
+
: {}),
|
|
94
|
+
registry: new NoopWorkerRegistry(),
|
|
95
|
+
logger: { info: (m) => this.logger.info(m), error: (m) => this.logger.error(m) },
|
|
96
|
+
onError: (err) => this.logger.error(`worker-runtime advertisement error: ${String(err)}`),
|
|
97
|
+
});
|
|
98
|
+
return { runtime, partition };
|
|
99
|
+
}
|
|
100
|
+
/** Build the transport from `config/durable.ts` — the same resolution the provider uses, so the
|
|
101
|
+
* worker lands on the SAME queues as the control-plane. In-memory default when no transport named. */
|
|
102
|
+
async #resolveTransport(config) {
|
|
103
|
+
const name = config.transport;
|
|
104
|
+
if (!name)
|
|
105
|
+
return new InMemoryTransport();
|
|
106
|
+
const factory = config.transports?.[name];
|
|
107
|
+
if (!factory) {
|
|
108
|
+
throw new Error(`@agora/durable: config.transport is "${name}", but config.transports.${name} is not defined`);
|
|
109
|
+
}
|
|
110
|
+
return factory({ app: this.app });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=durable_worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable_worker.js","sourceRoot":"","sources":["../../commands/durable_worker.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/main.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ListLoader } from '@adonisjs/core/ace';
|
|
2
|
+
import DurableWorker from './durable_worker.js';
|
|
2
3
|
import MakeWorkflow from './make_workflow.js';
|
|
3
4
|
import DurableRetry from './retry.js';
|
|
4
5
|
import DurableRuns from './runs.js';
|
|
@@ -6,12 +7,12 @@ import DurableWork from './work.js';
|
|
|
6
7
|
/**
|
|
7
8
|
* The commands barrel for `@adonis-agora/durable`. Registered in an app's `adonisrc` via
|
|
8
9
|
* `rcFile.addCommand('@adonis-agora/durable/commands')` (done by this package's `configure`). The ace
|
|
9
|
-
* kernel imports this module and treats it as a commands loader: a {@link ListLoader} over the
|
|
10
|
-
*
|
|
11
|
-
* metadata and constructors.
|
|
10
|
+
* kernel imports this module and treats it as a commands loader: a {@link ListLoader} over the durable
|
|
11
|
+
* commands (`durable:work`, `durable:worker`, `durable:runs`, `durable:retry`, `make:workflow`)
|
|
12
|
+
* provides their metadata and constructors.
|
|
12
13
|
*/
|
|
13
|
-
declare const loader: ListLoader<typeof MakeWorkflow | typeof DurableRetry | typeof DurableRuns | typeof DurableWork>;
|
|
14
|
+
declare const loader: ListLoader<typeof DurableWorker | typeof MakeWorkflow | typeof DurableRetry | typeof DurableRuns | typeof DurableWork>;
|
|
14
15
|
export declare const getMetaData: () => Promise<import("@adonisjs/core/types/ace").CommandMetaData[]>;
|
|
15
|
-
export declare const getCommand: (metaData: import("@adonisjs/core/types/ace").CommandMetaData) => Promise<typeof MakeWorkflow | typeof DurableRetry | typeof DurableRuns | typeof DurableWork | null>;
|
|
16
|
+
export declare const getCommand: (metaData: import("@adonisjs/core/types/ace").CommandMetaData) => Promise<typeof DurableWorker | typeof MakeWorkflow | typeof DurableRetry | typeof DurableRuns | typeof DurableWork | null>;
|
|
16
17
|
export default loader;
|
|
17
18
|
//# sourceMappingURL=main.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../commands/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAC9C,OAAO,YAAY,MAAM,YAAY,CAAC;AACtC,OAAO,WAAW,MAAM,WAAW,CAAC;AACpC,OAAO,WAAW,MAAM,WAAW,CAAC;AAEpC;;;;;;GAMG;AACH,QAAA,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../commands/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAC9C,OAAO,YAAY,MAAM,YAAY,CAAC;AACtC,OAAO,WAAW,MAAM,WAAW,CAAC;AACpC,OAAO,WAAW,MAAM,WAAW,CAAC;AAEpC;;;;;;GAMG;AACH,QAAA,MAAM,MAAM,wHAMV,CAAC;AAEH,eAAO,MAAM,WAAW,qEAAkC,CAAC;AAC3D,eAAO,MAAM,UAAU,8LAAiC,CAAC;AAEzD,eAAe,MAAM,CAAC"}
|
package/dist/commands/main.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ListLoader } from '@adonisjs/core/ace';
|
|
2
|
+
import DurableWorker from './durable_worker.js';
|
|
2
3
|
import MakeWorkflow from './make_workflow.js';
|
|
3
4
|
import DurableRetry from './retry.js';
|
|
4
5
|
import DurableRuns from './runs.js';
|
|
@@ -6,11 +7,17 @@ import DurableWork from './work.js';
|
|
|
6
7
|
/**
|
|
7
8
|
* The commands barrel for `@adonis-agora/durable`. Registered in an app's `adonisrc` via
|
|
8
9
|
* `rcFile.addCommand('@adonis-agora/durable/commands')` (done by this package's `configure`). The ace
|
|
9
|
-
* kernel imports this module and treats it as a commands loader: a {@link ListLoader} over the
|
|
10
|
-
*
|
|
11
|
-
* metadata and constructors.
|
|
10
|
+
* kernel imports this module and treats it as a commands loader: a {@link ListLoader} over the durable
|
|
11
|
+
* commands (`durable:work`, `durable:worker`, `durable:runs`, `durable:retry`, `make:workflow`)
|
|
12
|
+
* provides their metadata and constructors.
|
|
12
13
|
*/
|
|
13
|
-
const loader = new ListLoader([
|
|
14
|
+
const loader = new ListLoader([
|
|
15
|
+
DurableWork,
|
|
16
|
+
DurableWorker,
|
|
17
|
+
DurableRuns,
|
|
18
|
+
DurableRetry,
|
|
19
|
+
MakeWorkflow,
|
|
20
|
+
]);
|
|
14
21
|
export const getMetaData = loader.getMetaData.bind(loader);
|
|
15
22
|
export const getCommand = loader.getCommand.bind(loader);
|
|
16
23
|
export default loader;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../commands/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAC9C,OAAO,YAAY,MAAM,YAAY,CAAC;AACtC,OAAO,WAAW,MAAM,WAAW,CAAC;AACpC,OAAO,WAAW,MAAM,WAAW,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../commands/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAC9C,OAAO,YAAY,MAAM,YAAY,CAAC;AACtC,OAAO,WAAW,MAAM,WAAW,CAAC;AACpC,OAAO,WAAW,MAAM,WAAW,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC;IAC5B,WAAW;IACX,aAAa;IACb,WAAW;IACX,YAAY;IACZ,YAAY;CACb,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAEzD,eAAe,MAAM,CAAC"}
|
|
@@ -19,8 +19,43 @@ export default class DashboardProvider {
|
|
|
19
19
|
constructor(app: ApplicationService);
|
|
20
20
|
/** Warn once so a throwing `login` hook doesn't spam the logs on every failed attempt. */
|
|
21
21
|
private warnedOnHookThrow;
|
|
22
|
+
/**
|
|
23
|
+
* Captures the engine's `capability.unavailable` / `protocol.incompatible` diagnostics events so the
|
|
24
|
+
* `/compat` health panel can render each blocked run's structured delta + the live-fleet compat view
|
|
25
|
+
* (design §7.6, §10). Store-role only — attached in {@link boot}; a `tenant` pod owns no engine.
|
|
26
|
+
*/
|
|
27
|
+
private readonly diagnostics;
|
|
28
|
+
/** Detach handle for the diagnostics subscription, released on {@link shutdown}. */
|
|
29
|
+
private detachDiagnostics;
|
|
30
|
+
/** The active durable role (`standalone` when no `durable` config is present — today's default). */
|
|
31
|
+
private durableRole;
|
|
22
32
|
boot(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe the {@link diagnostics} recorder to the store-backed engine's lifecycle events so the
|
|
35
|
+
* `/compat` panel is fed live (design §10). Store-role only: a `tenant` pod owns no engine (structural
|
|
36
|
+
* isolation), so its panel shows blocked runs reason-only. Defensive: if the engine can't be resolved
|
|
37
|
+
* (the dashboard mounted without `@adonis-agora/durable`'s provider), the recorder simply stays empty
|
|
38
|
+
* rather than crashing boot.
|
|
39
|
+
*/
|
|
40
|
+
private attachDiagnostics;
|
|
23
41
|
private registerRoutes;
|
|
42
|
+
/**
|
|
43
|
+
* Assemble the {@link CompatSource} the `/compat` handler reads (design §10). The fleet view is the LIVE
|
|
44
|
+
* green fleet enumerated off the transport (every advertising worker — compatible OR incompatible, even
|
|
45
|
+
* one that never triggered a blocked dispatch) merged with the descriptors the {@link diagnostics}
|
|
46
|
+
* recorder reconstructed from past blocks; the live descriptors win per instance (fresher). Captured
|
|
47
|
+
* diagnostics + blocked runs stay as they were.
|
|
48
|
+
*
|
|
49
|
+
* The live enumeration is snapshotted ONCE per request (an `async` fetch off the transport), then handed
|
|
50
|
+
* to the handler as a synchronous `fleet()` closure — so {@link CompatSource} keeps its cheap sync shape.
|
|
51
|
+
* A `tenant` pod (or a transport without the fleet capability) has no {@link DURABLE_TRANSPORT} binding →
|
|
52
|
+
* live fleet is empty → the panel degrades to the diagnostics-only view exactly as before.
|
|
53
|
+
*/
|
|
54
|
+
private compatSource;
|
|
55
|
+
/** Resolve the store role's task transport (published under {@link DURABLE_TRANSPORT}) for LIVE fleet
|
|
56
|
+
* enumeration, or `null` when unavailable — a `tenant` pod never binds it, and a mis-mounted dashboard
|
|
57
|
+
* degrades cleanly to the diagnostics-only view rather than crashing the panel. */
|
|
58
|
+
private fleetTransport;
|
|
24
59
|
/**
|
|
25
60
|
* Mount the built-in `dashboardAuth` endpoints under `basePath`. All three are public (no guard):
|
|
26
61
|
* they create/destroy the session the {@link enforce} guard checks for.
|
|
@@ -55,5 +90,7 @@ export default class DashboardProvider {
|
|
|
55
90
|
* `Secure` on https. `Path=/` so it reaches both the UI and API mounts regardless of prefix.
|
|
56
91
|
*/
|
|
57
92
|
private writeSessionCookie;
|
|
93
|
+
/** Detach the diagnostics subscription so the recorder stops listening on a clean shutdown/redeploy. */
|
|
94
|
+
shutdown(): Promise<void>;
|
|
58
95
|
}
|
|
59
96
|
//# sourceMappingURL=dashboard_provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard_provider.d.ts","sourceRoot":"","sources":["../../providers/dashboard_provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAqB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"dashboard_provider.d.ts","sourceRoot":"","sources":["../../providers/dashboard_provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAqB,MAAM,sBAAsB,CAAC;AAuClF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;IACxB,SAAS,CAAC,GAAG,EAAE,kBAAkB;gBAAvB,GAAG,EAAE,kBAAkB;IAE7C,0FAA0F;IAC1F,OAAO,CAAC,iBAAiB,CAAS;IAElC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE,oFAAoF;IACpF,OAAO,CAAC,iBAAiB,CAA6B;IAEtD,oGAAoG;IACpG,OAAO,CAAC,WAAW;IAIb,IAAI;IA8BV;;;;;;OAMG;YACW,iBAAiB;IAU/B,OAAO,CAAC,cAAc;IA8EtB;;;;;;;;;;;OAWG;YACW,YAAY;IAW1B;;wFAEoF;YACtE,cAAc;IAQ5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,kBAAkB;IAiD1B;;;;;;;;;;OAUG;YACW,OAAO;IA0BrB,iFAAiF;IACjF,OAAO,CAAC,iBAAiB;IAKzB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAW1B,wGAAwG;IAClG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAIhC"}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { SESSION_COOKIE_NAME, performLogin, readSession, } from '../src/dashboard/auth.js';
|
|
2
|
+
import { compat, enumerateLiveFleet, mergeFleets, } from '../src/dashboard/compat.js';
|
|
2
3
|
import { resolveConfig, } from '../src/dashboard/define_config.js';
|
|
4
|
+
import { BlockedDiagnosticsRecorder } from '../src/dashboard/diagnostics-recorder.js';
|
|
5
|
+
import { dashboardEngineForRole } from '../src/dashboard/gateway-adapter.js';
|
|
3
6
|
import { cancelRun, getRun, health, listRuns, redispatchPendingRun, retryRun, } from '../src/dashboard/handlers.js';
|
|
4
7
|
import { renderDashboard } from '../src/dashboard/html.js';
|
|
5
8
|
import { renderLoginPage } from '../src/dashboard/login_page.js';
|
|
6
9
|
import { WorkflowEngine } from '../src/index.js';
|
|
10
|
+
import { DURABLE_TRANSPORT } from '../src/role_bindings.js';
|
|
7
11
|
/**
|
|
8
12
|
* Mounts the durable dashboard into an AdonisJS app: a JSON API over the
|
|
9
13
|
* {@link WorkflowEngine}'s read surface, and a single self-contained HTML page
|
|
@@ -26,10 +30,23 @@ export default class DashboardProvider {
|
|
|
26
30
|
}
|
|
27
31
|
/** Warn once so a throwing `login` hook doesn't spam the logs on every failed attempt. */
|
|
28
32
|
warnedOnHookThrow = false;
|
|
33
|
+
/**
|
|
34
|
+
* Captures the engine's `capability.unavailable` / `protocol.incompatible` diagnostics events so the
|
|
35
|
+
* `/compat` health panel can render each blocked run's structured delta + the live-fleet compat view
|
|
36
|
+
* (design §7.6, §10). Store-role only — attached in {@link boot}; a `tenant` pod owns no engine.
|
|
37
|
+
*/
|
|
38
|
+
diagnostics = new BlockedDiagnosticsRecorder();
|
|
39
|
+
/** Detach handle for the diagnostics subscription, released on {@link shutdown}. */
|
|
40
|
+
detachDiagnostics = null;
|
|
41
|
+
/** The active durable role (`standalone` when no `durable` config is present — today's default). */
|
|
42
|
+
durableRole() {
|
|
43
|
+
return this.app.config.get('durable', {}).role ?? 'standalone';
|
|
44
|
+
}
|
|
29
45
|
async boot() {
|
|
30
46
|
const config = resolveConfig(this.app.config.get('durable_dashboard', {}));
|
|
31
47
|
if (!config.enabled)
|
|
32
48
|
return;
|
|
49
|
+
const role = this.durableRole();
|
|
33
50
|
// Route registration can't happen synchronously in `boot()`: at this point in the AdonisJS
|
|
34
51
|
// lifecycle the HTTP server/router binding may not be resolvable yet (bindings from other
|
|
35
52
|
// providers' `boot()` methods can still be pending), and — critically — the *documented*
|
|
@@ -47,18 +64,35 @@ export default class DashboardProvider {
|
|
|
47
64
|
// `@adonisjs/core`'s own `AppServiceProvider` uses internally.
|
|
48
65
|
await this.app.booted(async () => {
|
|
49
66
|
const router = await this.app.container.make('router');
|
|
50
|
-
this.registerRoutes(router, config);
|
|
67
|
+
this.registerRoutes(router, config, role);
|
|
68
|
+
await this.attachDiagnostics(role);
|
|
51
69
|
});
|
|
52
70
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe the {@link diagnostics} recorder to the store-backed engine's lifecycle events so the
|
|
73
|
+
* `/compat` panel is fed live (design §10). Store-role only: a `tenant` pod owns no engine (structural
|
|
74
|
+
* isolation), so its panel shows blocked runs reason-only. Defensive: if the engine can't be resolved
|
|
75
|
+
* (the dashboard mounted without `@adonis-agora/durable`'s provider), the recorder simply stays empty
|
|
76
|
+
* rather than crashing boot.
|
|
77
|
+
*/
|
|
78
|
+
async attachDiagnostics(role) {
|
|
79
|
+
if (role === 'tenant')
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
59
82
|
const engine = await this.app.container.make(WorkflowEngine);
|
|
60
|
-
|
|
61
|
-
}
|
|
83
|
+
this.detachDiagnostics = this.diagnostics.attach(engine);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// No store-backed engine available — the compat panel degrades to reason-only blocked runs.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
registerRoutes(router, config, role) {
|
|
90
|
+
const apiBase = `${config.path}/api`;
|
|
91
|
+
// Resolve the dashboard's read/control port lazily per request, branched by role (design §5/§8):
|
|
92
|
+
// a store role resolves the engine (built by @adonis-agora/durable's provider); a store-less `tenant`
|
|
93
|
+
// pod resolves the DURABLE_RUN_GATEWAY token (a ProxyRunGateway) and adapts it — NO engine, NO store.
|
|
94
|
+
const resolveEngine = () => dashboardEngineForRole(role, this.app.container, WorkflowEngine);
|
|
95
|
+
const deps = async () => ({ engine: await resolveEngine() });
|
|
62
96
|
// Built-in `dashboardAuth` login screen (opt-in). Registered ONLY when configured, so omitting
|
|
63
97
|
// `dashboardAuth` leaves route registration byte-for-byte as it was. These endpoints are public
|
|
64
98
|
// (behind NEITHER guard): they MINT the session the guard checks for, and the login page must
|
|
@@ -102,6 +136,56 @@ export default class DashboardProvider {
|
|
|
102
136
|
router
|
|
103
137
|
.get(`${apiBase}/health`, json((d) => health(d)))
|
|
104
138
|
.as('durable_dashboard.health');
|
|
139
|
+
// Fleet health / protocol-compatibility panel (design §7.6, §10): per queue/group/pod protocol +
|
|
140
|
+
// negotiated level + red-flag reason on incompatibility, plus blocked runs with their structured delta.
|
|
141
|
+
router
|
|
142
|
+
.get(`${apiBase}/compat`, async (ctx) => {
|
|
143
|
+
if (!(await this.enforce(config, ctx, 'api')))
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
const result = await compat(await this.compatSource(await resolveEngine()));
|
|
147
|
+
return ctx.response.status(result.status).json(result.body);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return ctx.response
|
|
151
|
+
.status(500)
|
|
152
|
+
.json({ error: error instanceof Error ? error.message : 'internal error' });
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.as('durable_dashboard.compat');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Assemble the {@link CompatSource} the `/compat` handler reads (design §10). The fleet view is the LIVE
|
|
159
|
+
* green fleet enumerated off the transport (every advertising worker — compatible OR incompatible, even
|
|
160
|
+
* one that never triggered a blocked dispatch) merged with the descriptors the {@link diagnostics}
|
|
161
|
+
* recorder reconstructed from past blocks; the live descriptors win per instance (fresher). Captured
|
|
162
|
+
* diagnostics + blocked runs stay as they were.
|
|
163
|
+
*
|
|
164
|
+
* The live enumeration is snapshotted ONCE per request (an `async` fetch off the transport), then handed
|
|
165
|
+
* to the handler as a synchronous `fleet()` closure — so {@link CompatSource} keeps its cheap sync shape.
|
|
166
|
+
* A `tenant` pod (or a transport without the fleet capability) has no {@link DURABLE_TRANSPORT} binding →
|
|
167
|
+
* live fleet is empty → the panel degrades to the diagnostics-only view exactly as before.
|
|
168
|
+
*/
|
|
169
|
+
async compatSource(engine) {
|
|
170
|
+
const live = await enumerateLiveFleet(await this.fleetTransport());
|
|
171
|
+
const fleet = mergeFleets(this.diagnostics.fleet(), live);
|
|
172
|
+
return {
|
|
173
|
+
controlPlaneDescriptor: () => this.diagnostics.controlPlaneDescriptor(),
|
|
174
|
+
fleet: () => fleet,
|
|
175
|
+
blockedRuns: () => engine.listRuns({ statuses: ['blocked'] }),
|
|
176
|
+
diagnosticsFor: (runId) => this.diagnostics.diagnosticsFor(runId),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Resolve the store role's task transport (published under {@link DURABLE_TRANSPORT}) for LIVE fleet
|
|
180
|
+
* enumeration, or `null` when unavailable — a `tenant` pod never binds it, and a mis-mounted dashboard
|
|
181
|
+
* degrades cleanly to the diagnostics-only view rather than crashing the panel. */
|
|
182
|
+
async fleetTransport() {
|
|
183
|
+
try {
|
|
184
|
+
return (await this.app.container.make(DURABLE_TRANSPORT));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
105
189
|
}
|
|
106
190
|
/**
|
|
107
191
|
* Mount the built-in `dashboardAuth` endpoints under `basePath`. All three are public (no guard):
|
|
@@ -207,6 +291,11 @@ export default class DashboardProvider {
|
|
|
207
291
|
encode: false,
|
|
208
292
|
});
|
|
209
293
|
}
|
|
294
|
+
/** Detach the diagnostics subscription so the recorder stops listening on a clean shutdown/redeploy. */
|
|
295
|
+
async shutdown() {
|
|
296
|
+
this.detachDiagnostics?.();
|
|
297
|
+
this.detachDiagnostics = null;
|
|
298
|
+
}
|
|
210
299
|
}
|
|
211
300
|
/** Adapt an AdonisJS `HttpContext` to the framework-light {@link ApiRequest}. */
|
|
212
301
|
function toApiRequest(ctx) {
|