@adonis-agora/durable 0.56.2 → 0.56.3
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 +22 -0
- package/dist/assets/spa/assets/{index-9kO8tWjg.js → index-BULnCUuC.js} +4 -4
- package/dist/assets/spa/assets/index-BULnCUuC.js.map +1 -0
- package/dist/assets/spa/index.html +1 -1
- package/dist/src/admission-redis/redis-admission-backend.d.ts.map +1 -1
- package/dist/src/admission-redis/redis-admission-backend.js +6 -3
- package/dist/src/admission-redis/redis-admission-backend.js.map +1 -1
- package/dist/src/dashboard/compat.d.ts.map +1 -1
- package/dist/src/dashboard/compat.js +16 -7
- package/dist/src/dashboard/compat.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +5 -1
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +1 -1
- package/dist/src/dashboard/index.js +1 -1
- package/dist/src/engine.d.ts +19 -4
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +99 -18
- package/dist/src/engine.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/interfaces.d.ts +3 -0
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/run-detail-summary.d.ts.map +1 -1
- package/dist/src/run-detail-summary.js +23 -14
- package/dist/src/run-detail-summary.js.map +1 -1
- package/dist/src/singleton-gate.d.ts +1 -1
- package/dist/src/singleton-gate.d.ts.map +1 -1
- package/dist/src/singleton-gate.js +7 -6
- package/dist/src/singleton-gate.js.map +1 -1
- package/dist/src/stores/lucid-schema.d.ts +1 -1
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +26 -1
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +45 -8
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/telescope/data-providers.d.ts.map +1 -1
- package/dist/src/telescope/data-providers.js +27 -3
- package/dist/src/telescope/data-providers.js.map +1 -1
- package/package.json +3 -3
- package/dist/assets/spa/assets/index-9kO8tWjg.js.map +0 -1
package/dist/src/engine.js
CHANGED
|
@@ -122,6 +122,9 @@ const STEP_FAN_CHUNK = 200;
|
|
|
122
122
|
* waiting for every checkpoint of a wide fan to commit.
|
|
123
123
|
*/
|
|
124
124
|
const STEP_FAN_SEND_ROUND = 2000;
|
|
125
|
+
/** Most step events one control-plane message carries (see {@link WorkflowEngine.broadcastEvents}). */
|
|
126
|
+
const STEP_EVENTS_PER_BROADCAST = 500;
|
|
127
|
+
const isStepEvent = (event) => event.type.startsWith('step.');
|
|
125
128
|
/** The default {@link WorkflowEngineDeps.timerBatchSize}. */
|
|
126
129
|
const DEFAULT_TIMER_BATCH = 500;
|
|
127
130
|
/** {@link WorkflowEngine.sweepBufferedSignals}: rows read per page. */
|
|
@@ -161,6 +164,24 @@ const LEASE_RECHECK_SLACK_MS = 100;
|
|
|
161
164
|
* turn; the calls already in flight are not re-dispatched (their pending checkpoints dedupe them).
|
|
162
165
|
*/
|
|
163
166
|
const REMOTE_TURN_REDRIVE_MS = 1_000;
|
|
167
|
+
/**
|
|
168
|
+
* The most entries a per-step in-memory throttle/fallback map keeps (heartbeat writes, lease
|
|
169
|
+
* re-arms, admitted queues). Each entry is a few dozen bytes; the cap is there so a long-lived pod
|
|
170
|
+
* that hears the whole fleet's beats cannot grow without bound, while staying well above the number
|
|
171
|
+
* of steps a fleet keeps in flight, so the throttles keep working at scale.
|
|
172
|
+
*/
|
|
173
|
+
const THROTTLE_MAP_CAP = 16_384;
|
|
174
|
+
/** `map.set(key, value)` keeping insertion order as recency, evicting the oldest past {@link THROTTLE_MAP_CAP}. */
|
|
175
|
+
function setBounded(map, key, value) {
|
|
176
|
+
map.delete(key);
|
|
177
|
+
map.set(key, value);
|
|
178
|
+
while (map.size > THROTTLE_MAP_CAP) {
|
|
179
|
+
const oldest = map.keys().next();
|
|
180
|
+
if (oldest.done)
|
|
181
|
+
break;
|
|
182
|
+
map.delete(oldest.value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
164
185
|
/** Fisher–Yates, in place. */
|
|
165
186
|
function shuffle(items) {
|
|
166
187
|
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
@@ -327,7 +348,8 @@ export class WorkflowEngine {
|
|
|
327
348
|
admission;
|
|
328
349
|
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
329
350
|
queueWaiters = new Map();
|
|
330
|
-
/** Which queue a dispatched step took a slot from, by stepId — so the result can release it.
|
|
351
|
+
/** Which queue a dispatched step took a slot from, by stepId — so the result can release it.
|
|
352
|
+
* Bounded (see {@link setBounded}): the checkpoint's `queue` is the durable source. */
|
|
331
353
|
stepQueue = new Map();
|
|
332
354
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
333
355
|
inflight = new Map();
|
|
@@ -621,6 +643,11 @@ export class WorkflowEngine {
|
|
|
621
643
|
// `at` may be a string after JSON transit (Redis) — normalize back to a Date.
|
|
622
644
|
this.deliver(markRemoteEvent({ ...msg.event, at: new Date(msg.event.at) }));
|
|
623
645
|
}
|
|
646
|
+
else if (msg.kind === 'events') {
|
|
647
|
+
for (const event of msg.events) {
|
|
648
|
+
this.deliver(markRemoteEvent({ ...event, at: new Date(event.at) }));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
624
651
|
else if (msg.kind === 'cancel') {
|
|
625
652
|
this.notifyCancelled(msg.runId);
|
|
626
653
|
}
|
|
@@ -1156,8 +1183,11 @@ export class WorkflowEngine {
|
|
|
1156
1183
|
* One exemption: a beat CARRYING progress punches through when the last persisted one had none —
|
|
1157
1184
|
* the automatic pickup beat lands first (no payload) and must not shadow the handler's first real
|
|
1158
1185
|
* progress for a whole window. Optional store capability: no `recordStepHeartbeat` → no-op.
|
|
1159
|
-
* Failures are swallowed (a lost liveness update never harms the run). The throttle map
|
|
1160
|
-
*
|
|
1186
|
+
* Failures are swallowed (a lost liveness update never harms the run). The throttle map stays in
|
|
1187
|
+
* write order: entries whose window has passed are dropped from its front (they no longer throttle
|
|
1188
|
+
* anything), and past {@link THROTTLE_MAP_CAP} the oldest goes — a rare extra write, never a leak.
|
|
1189
|
+
* It used to be cleared wholesale past 1024 entries, which a fleet with more steps beating than that
|
|
1190
|
+
* (every engine pod hears every beat) hit every few seconds, turning the throttle off.
|
|
1161
1191
|
*/
|
|
1162
1192
|
persistHeartbeat(beat) {
|
|
1163
1193
|
const id = beat.stepId;
|
|
@@ -1170,9 +1200,15 @@ export class WorkflowEngine {
|
|
|
1170
1200
|
now - last.at < this.heartbeatPersistThrottleMs &&
|
|
1171
1201
|
!upgradesToProgress)
|
|
1172
1202
|
return;
|
|
1173
|
-
|
|
1174
|
-
|
|
1203
|
+
// Re-insert at the back so the map stays ordered by write time, then drop the expired front.
|
|
1204
|
+
this.heartbeatPersists.delete(id);
|
|
1175
1205
|
this.heartbeatPersists.set(id, { at: now, hadProgress: beat.progress !== undefined });
|
|
1206
|
+
for (const [oldId, entry] of this.heartbeatPersists) {
|
|
1207
|
+
if (now - entry.at < this.heartbeatPersistThrottleMs &&
|
|
1208
|
+
this.heartbeatPersists.size <= THROTTLE_MAP_CAP)
|
|
1209
|
+
break;
|
|
1210
|
+
this.heartbeatPersists.delete(oldId);
|
|
1211
|
+
}
|
|
1176
1212
|
this.heartbeatWrites.set(id, {
|
|
1177
1213
|
runId: beat.runId,
|
|
1178
1214
|
seq: beat.seq,
|
|
@@ -1335,12 +1371,51 @@ export class WorkflowEngine {
|
|
|
1335
1371
|
emit(event) {
|
|
1336
1372
|
const full = { ...event, at: new Date() };
|
|
1337
1373
|
this.deliver(full);
|
|
1338
|
-
if (this.controlPlane)
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1374
|
+
if (!this.controlPlane)
|
|
1375
|
+
return;
|
|
1376
|
+
this.eventsToBroadcast.push(full);
|
|
1377
|
+
if (!this.broadcastingEvents)
|
|
1378
|
+
void this.broadcastEvents();
|
|
1379
|
+
}
|
|
1380
|
+
/** Events waiting for the control-plane publish in flight, in emit order (see {@link broadcastEvents}). */
|
|
1381
|
+
eventsToBroadcast = [];
|
|
1382
|
+
broadcastingEvents = false;
|
|
1383
|
+
/**
|
|
1384
|
+
* Publish the queued events in the order they were emitted. A run's own events go out one per
|
|
1385
|
+
* message, as every instance expects them. Step events (a wide fan emits two per step) that queued
|
|
1386
|
+
* up while a publish was in flight go out together in one `events` message, so a fan of 40,000
|
|
1387
|
+
* steps costs the control plane a few hundred messages to every instance instead of 80,000.
|
|
1388
|
+
*/
|
|
1389
|
+
async broadcastEvents() {
|
|
1390
|
+
this.broadcastingEvents = true;
|
|
1391
|
+
try {
|
|
1392
|
+
while (this.eventsToBroadcast.length > 0 && this.controlPlane) {
|
|
1393
|
+
const first = this.eventsToBroadcast[0];
|
|
1394
|
+
let msg;
|
|
1395
|
+
if (isStepEvent(first)) {
|
|
1396
|
+
let n = 1;
|
|
1397
|
+
while (n < STEP_EVENTS_PER_BROADCAST &&
|
|
1398
|
+
n < this.eventsToBroadcast.length &&
|
|
1399
|
+
isStepEvent(this.eventsToBroadcast[n])) {
|
|
1400
|
+
n += 1;
|
|
1401
|
+
}
|
|
1402
|
+
const events = this.eventsToBroadcast.splice(0, n);
|
|
1403
|
+
msg =
|
|
1404
|
+
events.length === 1
|
|
1405
|
+
? { kind: 'event', event: first, from: this.instanceId }
|
|
1406
|
+
: { kind: 'events', events, from: this.instanceId };
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
this.eventsToBroadcast.shift();
|
|
1410
|
+
msg = { kind: 'event', event: first, from: this.instanceId };
|
|
1411
|
+
}
|
|
1412
|
+
await this.controlPlane.publishControl(msg).catch(() => {
|
|
1413
|
+
// control-plane delivery is best-effort observability; never break execution
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
finally {
|
|
1418
|
+
this.broadcastingEvents = false;
|
|
1344
1419
|
}
|
|
1345
1420
|
}
|
|
1346
1421
|
/** Deliver an event to local subscribers only (no re-broadcast) — used for both locally-produced
|
|
@@ -1726,7 +1801,12 @@ export class WorkflowEngine {
|
|
|
1726
1801
|
// (b) an old pending REMOTE step whose worker heartbeat is silent — dispatched and lost.
|
|
1727
1802
|
// A long `ctx.sleep` (wakeAt in the future) and a beating long step are NOT stalled.
|
|
1728
1803
|
let stalePending = null;
|
|
1729
|
-
|
|
1804
|
+
// Only pending checkpoints can be stale: read just those where the store can filter, not the
|
|
1805
|
+
// run's whole history (a wide fan's thousands of settled rows, payloads included).
|
|
1806
|
+
const pendingCheckpoints = this.store.queryCheckpoints
|
|
1807
|
+
? await this.store.queryCheckpoints(run.id, { statuses: ['pending'] })
|
|
1808
|
+
: await this.store.listCheckpoints(run.id);
|
|
1809
|
+
for (const cp of pendingCheckpoints) {
|
|
1730
1810
|
if (cp.kind !== 'remote' || cp.status !== 'pending')
|
|
1731
1811
|
continue;
|
|
1732
1812
|
const ageMs = now - cp.enqueuedAt.getTime();
|
|
@@ -5066,7 +5146,8 @@ export class WorkflowEngine {
|
|
|
5066
5146
|
* (with a 30-minute window: two writes an hour), independent of beat frequency. The in-memory mark
|
|
5067
5147
|
* is a pure optimisation — cold on a fresh instance, or on the one that didn't dispatch the step —
|
|
5068
5148
|
* and the durable half-spent check re-derives the same answer from the checkpoint, so nothing
|
|
5069
|
-
* depends on it surviving.
|
|
5149
|
+
* depends on it surviving. Bounded at {@link THROTTLE_MAP_CAP} entries, oldest first: a rare extra
|
|
5150
|
+
* read, never a leak.
|
|
5070
5151
|
*
|
|
5071
5152
|
* NEVER REJECTS. This runs on the transport's SERIAL heartbeat handler, so a store hiccup thrown out
|
|
5072
5153
|
* of here would take the whole fleet's beat loop with it — and every lease would then lapse at once,
|
|
@@ -5104,15 +5185,13 @@ export class WorkflowEngine {
|
|
|
5104
5185
|
const halfWindow = Math.ceil(this.remoteRedispatchMs / 2);
|
|
5105
5186
|
// More than half the window left: nothing to buy, and nothing to look at until it is half spent.
|
|
5106
5187
|
if (cp.wakeAt != null && cp.wakeAt > now + halfWindow) {
|
|
5107
|
-
this.stepLeaseRearms
|
|
5188
|
+
setBounded(this.stepLeaseRearms, id, cp.wakeAt - halfWindow);
|
|
5108
5189
|
return;
|
|
5109
5190
|
}
|
|
5110
5191
|
const renewed = now + this.remoteRedispatchMs;
|
|
5111
5192
|
const previous = cp.wakeAt;
|
|
5112
5193
|
await this.store.saveCheckpoint({ ...cp, wakeAt: renewed });
|
|
5113
|
-
|
|
5114
|
-
this.stepLeaseRearms.clear();
|
|
5115
|
-
this.stepLeaseRearms.set(id, renewed - halfWindow);
|
|
5194
|
+
setBounded(this.stepLeaseRearms, id, renewed - halfWindow);
|
|
5116
5195
|
// If the run is parked EXACTLY on this step's old lease, carry its wake forward too — otherwise a
|
|
5117
5196
|
// beating worker costs one wasted turn per renewal window. Only on an exact match: any other
|
|
5118
5197
|
// `wakeAt` belongs to some other timer (a sleep, a sibling's shorter lease) and moving it would
|
|
@@ -6265,7 +6344,9 @@ export class WorkflowEngine {
|
|
|
6265
6344
|
throw new WorkflowSuspended(decision.retryAt);
|
|
6266
6345
|
}
|
|
6267
6346
|
this.queueWaiters.get(queue)?.delete(runId);
|
|
6268
|
-
|
|
6347
|
+
// Bounded: the result (and so the delete) may land on another pod, which releases the slot
|
|
6348
|
+
// from the checkpoint's durable `queue` — this map is only the legacy fallback.
|
|
6349
|
+
setBounded(this.stepQueue, id, queue);
|
|
6269
6350
|
admittedQueue = queue;
|
|
6270
6351
|
}
|
|
6271
6352
|
// Optional runtime schema: validate only when the def carries one (a bare `@Step()` / string-name
|