@adonis-agora/durable 0.48.0 → 0.49.1
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 +43 -0
- package/dist/assets/spa/assets/index-6nmSTfT7.css +1 -0
- package/dist/assets/spa/assets/index-DdB9zxuX.js +16 -0
- package/dist/assets/spa/assets/index-DdB9zxuX.js.map +1 -0
- package/dist/assets/spa/index.html +2 -2
- package/dist/providers/dashboard_provider.d.ts +3 -1
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +7 -2
- 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 +1 -0
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/codec-state-store.d.ts +2 -0
- package/dist/src/codec-state-store.d.ts.map +1 -1
- package/dist/src/codec-state-store.js +6 -0
- package/dist/src/codec-state-store.js.map +1 -1
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
- package/dist/src/dashboard/gateway-adapter.js +10 -0
- package/dist/src/dashboard/gateway-adapter.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts +22 -1
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +63 -10
- 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/dashboard/openapi.d.ts.map +1 -1
- package/dist/src/dashboard/openapi.js +26 -2
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/define_config.d.ts +6 -0
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/engine.d.ts +44 -3
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +226 -28
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +12 -1
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +8 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/fan-join.d.ts +32 -3
- package/dist/src/fan-join.d.ts.map +1 -1
- package/dist/src/fan-join.js +58 -4
- package/dist/src/fan-join.js.map +1 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +55 -9
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/run-detail-summary.d.ts +98 -0
- package/dist/src/run-detail-summary.d.ts.map +1 -0
- package/dist/src/run-detail-summary.js +150 -0
- package/dist/src/run-detail-summary.js.map +1 -0
- package/dist/src/run-gateway/interface.d.ts +11 -0
- package/dist/src/run-gateway/interface.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.d.ts +3 -0
- package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.js +8 -0
- package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/run-request-responder.d.ts +4 -3
- package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
- package/dist/src/run-gateway/run-request-responder.js +23 -3
- package/dist/src/run-gateway/run-request-responder.js.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.d.ts +3 -0
- package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.js +7 -0
- package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +1 -0
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +16 -3
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/testing/in-memory-state-store.d.ts +1 -0
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +14 -6
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +59 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/transports/bullmq/adaptive-concurrency.d.ts +166 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.d.ts.map +1 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.js +352 -0
- package/dist/src/transports/bullmq/adaptive-concurrency.js.map +1 -0
- package/dist/src/transports/bullmq/bullmq-transport.d.ts +18 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +50 -49
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/dist/src/transports/bullmq/deps.d.ts +11 -1
- package/dist/src/transports/bullmq/deps.d.ts.map +1 -1
- package/dist/src/transports/bullmq/deps.js +12 -1
- package/dist/src/transports/bullmq/deps.js.map +1 -1
- package/dist/src/transports/bullmq/index.d.ts +1 -0
- package/dist/src/transports/bullmq/index.d.ts.map +1 -1
- package/dist/src/transports/bullmq/index.js +1 -0
- package/dist/src/transports/bullmq/index.js.map +1 -1
- package/dist/src/transports/factory.d.ts +21 -2
- package/dist/src/transports/factory.d.ts.map +1 -1
- package/dist/src/transports/factory.js +5 -1
- package/dist/src/transports/factory.js.map +1 -1
- package/dist/src/workflow-ctx.d.ts +13 -1
- package/dist/src/workflow-ctx.d.ts.map +1 -1
- package/dist/src/workflow-ctx.js +25 -1
- package/dist/src/workflow-ctx.js.map +1 -1
- package/package.json +3 -3
- package/skills/durable-cluster/SKILL.md +10 -0
- package/skills/durable-transports-stores/SKILL.md +38 -0
- package/skills/durable-workflows/SKILL.md +19 -1
- package/dist/assets/spa/assets/index-DtGMBUDm.css +0 -1
- package/dist/assets/spa/assets/index-_VISH9Xg.js +0 -16
- package/dist/assets/spa/assets/index-_VISH9Xg.js.map +0 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { cpus, totalmem } from 'node:os';
|
|
3
|
+
const DEFAULT_MIN = 1;
|
|
4
|
+
const DEFAULT_MAX = 32;
|
|
5
|
+
const DEFAULT_RAM_CEILING_PCT = 85;
|
|
6
|
+
const DEFAULT_TICK_MS = 2000;
|
|
7
|
+
function clamp(value, min, max) {
|
|
8
|
+
return Math.min(max, Math.max(min, value));
|
|
9
|
+
}
|
|
10
|
+
/** `value` floored when it is a finite number of at least 1, else `fallback`. */
|
|
11
|
+
function wholeAtLeastOne(value, fallback) {
|
|
12
|
+
return value !== undefined && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
|
|
13
|
+
}
|
|
14
|
+
/** `value` when it is a finite number above 0, else `fallback`. */
|
|
15
|
+
function finitePositive(value, fallback) {
|
|
16
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalise a {@link ConcurrencyOption} into a {@link ResolvedConcurrency}: a bare number (or
|
|
20
|
+
* `undefined` → 1) becomes `{ mode:'fixed', fixed }`; `'adaptive'` / `{ mode:'adaptive', ... }`
|
|
21
|
+
* become `{ mode:'adaptive', adaptive }` with every default from the spec applied and `start`
|
|
22
|
+
* clamped into `[min,max]`.
|
|
23
|
+
*
|
|
24
|
+
* A non-finite or out-of-range number falls back to its default (a bad `cpuCeilingPct` turns the
|
|
25
|
+
* CPU ceiling off): BullMQ throws on a `NaN` or sub-1 concurrency, a `NaN` ceiling never brakes, and
|
|
26
|
+
* a `NaN` `tickMs` makes the control loop fire every millisecond.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveConcurrency(opt) {
|
|
29
|
+
if (opt === undefined)
|
|
30
|
+
return { mode: 'fixed', fixed: 1 };
|
|
31
|
+
if (typeof opt === 'number')
|
|
32
|
+
return { mode: 'fixed', fixed: wholeAtLeastOne(opt, 1) };
|
|
33
|
+
const overrides = opt === 'adaptive' ? {} : opt;
|
|
34
|
+
const min = wholeAtLeastOne(overrides.min, DEFAULT_MIN);
|
|
35
|
+
const max = Math.max(min, wholeAtLeastOne(overrides.max, DEFAULT_MAX));
|
|
36
|
+
const start = clamp(wholeAtLeastOne(overrides.start, min), min, max);
|
|
37
|
+
const cpuCeilingPct = finitePositive(overrides.cpuCeilingPct, undefined);
|
|
38
|
+
const adaptive = {
|
|
39
|
+
min,
|
|
40
|
+
max,
|
|
41
|
+
start,
|
|
42
|
+
ramCeilingPct: finitePositive(overrides.ramCeilingPct, DEFAULT_RAM_CEILING_PCT),
|
|
43
|
+
tickMs: wholeAtLeastOne(overrides.tickMs, DEFAULT_TICK_MS),
|
|
44
|
+
...(cpuCeilingPct !== undefined ? { cpuCeilingPct } : {}),
|
|
45
|
+
};
|
|
46
|
+
return { mode: 'adaptive', adaptive };
|
|
47
|
+
}
|
|
48
|
+
// The cgroup "unlimited" sentinel: a limit at/above this is no real cap, so fall through to host total.
|
|
49
|
+
const CGROUP_UNLIMITED = 2 ** 62;
|
|
50
|
+
/**
|
|
51
|
+
* The process memory ceiling in bytes, read once (cgroup limits don't move at runtime): cgroup v2
|
|
52
|
+
* `memory.max`, then cgroup v1 `memory.limit_in_bytes`, then the host total. A `max` / absurdly-large
|
|
53
|
+
* ("unlimited") cgroup value falls through to {@link totalmem}. No new deps — just `node:fs`/`node:os`.
|
|
54
|
+
*/
|
|
55
|
+
export function readMemoryLimitBytes() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim();
|
|
58
|
+
if (raw && raw !== 'max') {
|
|
59
|
+
const n = Number(raw);
|
|
60
|
+
if (Number.isFinite(n) && n > 0 && n < CGROUP_UNLIMITED)
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* not cgroup v2 (or unreadable) — try v1 next */
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const raw = readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8').trim();
|
|
69
|
+
const n = Number(raw);
|
|
70
|
+
if (Number.isFinite(n) && n > 0 && n < CGROUP_UNLIMITED)
|
|
71
|
+
return n;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* not cgroup v1 (or unreadable) — fall through to host total */
|
|
75
|
+
}
|
|
76
|
+
return totalmem();
|
|
77
|
+
}
|
|
78
|
+
// How many recent completions the controller reasons over (latency percentiles + throughput + errors).
|
|
79
|
+
const WINDOW_SIZE = 100;
|
|
80
|
+
// EWMA smoothing for the "no-queuing" baseline (rttLong): low weight so a single fast sample doesn't
|
|
81
|
+
// reset the baseline, but it still tracks a genuine floor shift.
|
|
82
|
+
const RTT_LONG_EWMA_ALPHA = 0.2;
|
|
83
|
+
// Consecutive ticks with in-flight work but zero completions before we call the worker stalled.
|
|
84
|
+
const STALL_TICKS = 2;
|
|
85
|
+
// Throughput is a rate over the trailing minute: a window whose newest completion is older than this
|
|
86
|
+
// reports 0, so an idle worker never advertises its last burst as live.
|
|
87
|
+
const THROUGHPUT_WINDOW_MS = 60_000;
|
|
88
|
+
// The shortest span a rate is computed over, so a worker's first completion doesn't extrapolate a
|
|
89
|
+
// sub-millisecond gap into a huge per-minute figure.
|
|
90
|
+
const THROUGHPUT_MIN_SPAN_MS = 1_000;
|
|
91
|
+
function percentile(sortedAsc, p) {
|
|
92
|
+
if (sortedAsc.length === 0)
|
|
93
|
+
return 0;
|
|
94
|
+
const idx = clamp(Math.ceil((p / 100) * sortedAsc.length) - 1, 0, sortedAsc.length - 1);
|
|
95
|
+
return sortedAsc[idx] ?? 0;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A single gradient-limit (AIMD) concurrency controller, shared by every per-routing-token task
|
|
99
|
+
* worker of one {@link import('./bullmq-transport.js').BullMQTransport}. It tracks in-flight work and a rolling window of
|
|
100
|
+
* completions, and — in `adaptive` mode — re-evaluates the worker's concurrency every `tickMs` from a
|
|
101
|
+
* latency gradient, an error/stall backpressure signal, a hard RAM brake and an optional CPU ceiling.
|
|
102
|
+
*
|
|
103
|
+
* In `fixed` mode no control loop runs and the limit never moves, but the controller still tracks
|
|
104
|
+
* `inFlight`/RSS/throughput/p95 (and samples CPU when a snapshot is taken) so the observability half
|
|
105
|
+
* (the heartbeat status payload) works without adaptive. Either way, {@link snapshot} returns the
|
|
106
|
+
* shared {@link WorkerStatus} contract the heartbeat writer stamps.
|
|
107
|
+
*/
|
|
108
|
+
export class AdaptiveController {
|
|
109
|
+
resolved;
|
|
110
|
+
apply;
|
|
111
|
+
now;
|
|
112
|
+
readRss;
|
|
113
|
+
memoryLimitBytes;
|
|
114
|
+
tickMs;
|
|
115
|
+
readCpuUsage;
|
|
116
|
+
cpuCores;
|
|
117
|
+
currentLimit;
|
|
118
|
+
inFlightCount = 0;
|
|
119
|
+
window = [];
|
|
120
|
+
rttLongMs;
|
|
121
|
+
cpuPct;
|
|
122
|
+
lastAdjust;
|
|
123
|
+
completionsThisTick = 0;
|
|
124
|
+
stallTicks = 0;
|
|
125
|
+
lastCpuUsage;
|
|
126
|
+
lastCpuSampleAt = 0;
|
|
127
|
+
timer;
|
|
128
|
+
constructor(options = {}) {
|
|
129
|
+
this.resolved = resolveConcurrency(options.concurrency);
|
|
130
|
+
this.apply = options.apply ?? (() => { });
|
|
131
|
+
this.now = options.now ?? Date.now;
|
|
132
|
+
this.readRss = options.readRss ?? (() => process.memoryUsage().rss);
|
|
133
|
+
this.memoryLimitBytes = (options.readMemoryLimit ?? readMemoryLimitBytes)();
|
|
134
|
+
this.tickMs = this.resolved.adaptive?.tickMs ?? DEFAULT_TICK_MS;
|
|
135
|
+
this.readCpuUsage = options.readCpuUsage ?? (() => process.cpuUsage());
|
|
136
|
+
this.cpuCores = wholeAtLeastOne((options.readCpuCount ?? (() => cpus().length))(), 1);
|
|
137
|
+
this.currentLimit =
|
|
138
|
+
this.resolved.mode === 'adaptive'
|
|
139
|
+
? (this.resolved.adaptive?.start ?? DEFAULT_MIN)
|
|
140
|
+
: (this.resolved.fixed ?? 1);
|
|
141
|
+
}
|
|
142
|
+
/** The concurrency to hand the BullMQ Worker on construction (fixed N, or the adaptive `start`). */
|
|
143
|
+
get initialLimit() {
|
|
144
|
+
return this.currentLimit;
|
|
145
|
+
}
|
|
146
|
+
/** The live limit in effect right now. */
|
|
147
|
+
get limit() {
|
|
148
|
+
return this.currentLimit;
|
|
149
|
+
}
|
|
150
|
+
/** `true` when the controller will move the limit on its own (adaptive mode). */
|
|
151
|
+
get isAdaptive() {
|
|
152
|
+
return this.resolved.mode === 'adaptive';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Start the control loop (adaptive mode only; fixed mode just takes a CPU baseline). The timer is
|
|
156
|
+
* `unref`'d so it never keeps the process alive; idempotent.
|
|
157
|
+
*/
|
|
158
|
+
start() {
|
|
159
|
+
if (this.timer)
|
|
160
|
+
return;
|
|
161
|
+
if (!this.lastCpuUsage)
|
|
162
|
+
this.resetCpuBaseline();
|
|
163
|
+
if (!this.isAdaptive)
|
|
164
|
+
return;
|
|
165
|
+
this.timer = setInterval(() => this.tick(), this.tickMs);
|
|
166
|
+
this.timer.unref?.();
|
|
167
|
+
}
|
|
168
|
+
/** Stop the control loop (call on worker close). Idempotent. */
|
|
169
|
+
stop() {
|
|
170
|
+
if (this.timer) {
|
|
171
|
+
clearInterval(this.timer);
|
|
172
|
+
this.timer = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** A task started — bump in-flight. Call from the job processor before awaiting the body. */
|
|
176
|
+
onStart() {
|
|
177
|
+
this.inFlightCount += 1;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A task settled. ALWAYS drops in-flight and counts toward stall liveness (`completionsThisTick`),
|
|
181
|
+
* so the saturation gate, the heartbeat `inFlight`, and the "alive-but-busy" signal see every task.
|
|
182
|
+
*
|
|
183
|
+
* `kind` gates the MEASUREMENT window: only `'step'` completions push a duration into the rolling
|
|
184
|
+
* window that feeds the latency gradient / throughput / p95 / error rate. A workflow TURN
|
|
185
|
+
* (`kind: 'workflow'`) is NOT measured — a 5ms suspend sitting next to a 2s step would corrupt the
|
|
186
|
+
* gradient — but it still counts as in-flight + forward progress. This is the single concurrency
|
|
187
|
+
* pool measuring steps only; the transport tags the kind when it routes a job by shape.
|
|
188
|
+
*/
|
|
189
|
+
onSettle(durationMs, ok, kind) {
|
|
190
|
+
this.inFlightCount = Math.max(0, this.inFlightCount - 1);
|
|
191
|
+
// Forward progress for stall detection counts BOTH kinds — a worker churning workflow turns is
|
|
192
|
+
// not stalled even when no step settles this tick.
|
|
193
|
+
this.completionsThisTick += 1;
|
|
194
|
+
if (kind !== 'step')
|
|
195
|
+
return;
|
|
196
|
+
this.window.push({ durationMs, ok, at: this.now() });
|
|
197
|
+
if (this.window.length > WINDOW_SIZE)
|
|
198
|
+
this.window.shift();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* One control-loop iteration: sample CPU, refresh the latency baseline, then decide the next limit
|
|
202
|
+
* and apply it. A no-op in fixed mode. Exposed (not private) so a test can drive ticks
|
|
203
|
+
* deterministically.
|
|
204
|
+
*/
|
|
205
|
+
tick() {
|
|
206
|
+
if (this.resolved.mode !== 'adaptive' || !this.resolved.adaptive)
|
|
207
|
+
return;
|
|
208
|
+
this.sampleCpu(this.tickMs);
|
|
209
|
+
this.refreshRttLong();
|
|
210
|
+
// Stall = in-flight work but no completion this tick, sustained over STALL_TICKS.
|
|
211
|
+
if (this.inFlightCount > 0 && this.completionsThisTick === 0)
|
|
212
|
+
this.stallTicks += 1;
|
|
213
|
+
else
|
|
214
|
+
this.stallTicks = 0;
|
|
215
|
+
const stalled = this.stallTicks >= STALL_TICKS;
|
|
216
|
+
this.completionsThisTick = 0;
|
|
217
|
+
this.decide(this.resolved.adaptive, stalled);
|
|
218
|
+
}
|
|
219
|
+
resetCpuBaseline() {
|
|
220
|
+
this.lastCpuUsage = this.readCpuUsage();
|
|
221
|
+
this.lastCpuSampleAt = this.now();
|
|
222
|
+
}
|
|
223
|
+
/** Set `cpuPct` from the CPU time spent since the previous sample, over `elapsedMs` of wall time. */
|
|
224
|
+
sampleCpu(elapsedMs) {
|
|
225
|
+
const previous = this.lastCpuUsage;
|
|
226
|
+
this.resetCpuBaseline();
|
|
227
|
+
if (!previous || !this.lastCpuUsage || elapsedMs <= 0)
|
|
228
|
+
return;
|
|
229
|
+
// CpuUsage is microseconds of CPU; elapsedMs of wall time is elapsedMs*1000 micros per core.
|
|
230
|
+
const micros = this.lastCpuUsage.user - previous.user + (this.lastCpuUsage.system - previous.system);
|
|
231
|
+
const pct = (micros / (elapsedMs * 1000 * this.cpuCores)) * 100;
|
|
232
|
+
if (Number.isFinite(pct))
|
|
233
|
+
this.cpuPct = Math.max(0, pct);
|
|
234
|
+
}
|
|
235
|
+
/** Fixed mode has no control loop, so a snapshot samples CPU itself, at most once per tick period
|
|
236
|
+
* (several heartbeat keys snapshot back to back, and a near-zero span gives a meaningless rate). */
|
|
237
|
+
sampleCpuForSnapshot() {
|
|
238
|
+
if (this.isAdaptive)
|
|
239
|
+
return;
|
|
240
|
+
if (!this.lastCpuUsage) {
|
|
241
|
+
this.resetCpuBaseline();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const elapsedMs = this.now() - this.lastCpuSampleAt;
|
|
245
|
+
if (elapsedMs >= this.tickMs)
|
|
246
|
+
this.sampleCpu(elapsedMs);
|
|
247
|
+
}
|
|
248
|
+
refreshRttLong() {
|
|
249
|
+
if (this.window.length === 0)
|
|
250
|
+
return;
|
|
251
|
+
let minDuration = this.window[0]?.durationMs ?? 0;
|
|
252
|
+
for (const c of this.window)
|
|
253
|
+
if (c.durationMs < minDuration)
|
|
254
|
+
minDuration = c.durationMs;
|
|
255
|
+
this.rttLongMs =
|
|
256
|
+
this.rttLongMs === undefined
|
|
257
|
+
? minDuration
|
|
258
|
+
: RTT_LONG_EWMA_ALPHA * minDuration + (1 - RTT_LONG_EWMA_ALPHA) * this.rttLongMs;
|
|
259
|
+
}
|
|
260
|
+
rssPct() {
|
|
261
|
+
if (this.memoryLimitBytes <= 0)
|
|
262
|
+
return 0;
|
|
263
|
+
return (100 * this.readRss()) / this.memoryLimitBytes;
|
|
264
|
+
}
|
|
265
|
+
decide(cfg, stalled) {
|
|
266
|
+
const current = this.currentLimit;
|
|
267
|
+
const durations = this.window.map((c) => c.durationMs).sort((a, b) => a - b);
|
|
268
|
+
const p50 = percentile(durations, 50);
|
|
269
|
+
const errors = this.window.reduce((n, c) => n + (c.ok ? 0 : 1), 0);
|
|
270
|
+
const errorRate = this.window.length === 0 ? 0 : errors / this.window.length;
|
|
271
|
+
const gradient = this.rttLongMs !== undefined && p50 > 0 ? Math.min(1, this.rttLongMs / p50) : 1;
|
|
272
|
+
const rssPct = this.rssPct();
|
|
273
|
+
const saturated = this.inFlightCount >= current * 0.8;
|
|
274
|
+
let next = current;
|
|
275
|
+
let reason;
|
|
276
|
+
if (rssPct >= cfg.ramCeilingPct) {
|
|
277
|
+
next = Math.max(cfg.min, Math.floor(current * 0.8));
|
|
278
|
+
reason = 'ram_ceiling';
|
|
279
|
+
}
|
|
280
|
+
else if (cfg.cpuCeilingPct !== undefined && (this.cpuPct ?? 0) >= cfg.cpuCeilingPct) {
|
|
281
|
+
// CPU at/over the ceiling: never grow; shed one to relieve it.
|
|
282
|
+
next = Math.max(cfg.min, current - 1);
|
|
283
|
+
reason = 'cpu_ceiling';
|
|
284
|
+
}
|
|
285
|
+
else if (errorRate > 0.2 || stalled) {
|
|
286
|
+
next = Math.max(cfg.min, current - 1);
|
|
287
|
+
reason = 'backpressure';
|
|
288
|
+
}
|
|
289
|
+
else if (gradient < 0.7) {
|
|
290
|
+
next = Math.max(cfg.min, Math.floor(current * gradient));
|
|
291
|
+
reason = 'shrink';
|
|
292
|
+
}
|
|
293
|
+
else if (gradient >= 0.9 && saturated) {
|
|
294
|
+
next = Math.min(cfg.max, current + 1);
|
|
295
|
+
reason = 'grow';
|
|
296
|
+
}
|
|
297
|
+
next = clamp(next, cfg.min, cfg.max);
|
|
298
|
+
if (next === current || reason === undefined)
|
|
299
|
+
return;
|
|
300
|
+
this.currentLimit = next;
|
|
301
|
+
this.lastAdjust = { at: this.now(), from: current, to: next, reason };
|
|
302
|
+
this.apply(next);
|
|
303
|
+
}
|
|
304
|
+
/** The shared {@link WorkerStatus} the heartbeat writer stamps. Best-effort fields are omitted when
|
|
305
|
+
* unmeasured (no completions yet → no p95/throughput; no CPU sample yet → no `cpuPct`). */
|
|
306
|
+
snapshot() {
|
|
307
|
+
this.sampleCpuForSnapshot();
|
|
308
|
+
const durations = this.window.map((c) => c.durationMs).sort((a, b) => a - b);
|
|
309
|
+
const rssBytes = this.readRss();
|
|
310
|
+
const rssLimitBytes = this.memoryLimitBytes;
|
|
311
|
+
const status = {
|
|
312
|
+
runtime: 'node',
|
|
313
|
+
concurrency: {
|
|
314
|
+
mode: this.resolved.mode,
|
|
315
|
+
limit: this.currentLimit,
|
|
316
|
+
...(this.resolved.mode === 'adaptive' && this.resolved.adaptive
|
|
317
|
+
? { min: this.resolved.adaptive.min, max: this.resolved.adaptive.max }
|
|
318
|
+
: {}),
|
|
319
|
+
},
|
|
320
|
+
inFlight: this.inFlightCount,
|
|
321
|
+
rssBytes,
|
|
322
|
+
rssLimitBytes,
|
|
323
|
+
rssPct: rssLimitBytes > 0 ? (100 * rssBytes) / rssLimitBytes : 0,
|
|
324
|
+
};
|
|
325
|
+
if (this.cpuPct !== undefined)
|
|
326
|
+
status.cpuPct = this.cpuPct;
|
|
327
|
+
if (this.window.length > 0) {
|
|
328
|
+
status.throughputPerMin = this.throughputPerMin();
|
|
329
|
+
status.p95Ms = percentile(durations, 95);
|
|
330
|
+
}
|
|
331
|
+
if (this.lastAdjust)
|
|
332
|
+
status.lastAdjust = this.lastAdjust;
|
|
333
|
+
return status;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Completions per minute over the trailing minute: the window's completions from the last minute,
|
|
337
|
+
* over the span from the oldest of them (or a full minute, when the window reaches further back) to
|
|
338
|
+
* now, floored at a second. The rate decays while the worker is idle and reads 0 once a minute passes with no completion.
|
|
339
|
+
*/
|
|
340
|
+
throughputPerMin() {
|
|
341
|
+
const first = this.window[0];
|
|
342
|
+
if (!first)
|
|
343
|
+
return 0;
|
|
344
|
+
const now = this.now();
|
|
345
|
+
const recent = this.window.filter((c) => now - c.at <= THROUGHPUT_WINDOW_MS).length;
|
|
346
|
+
if (recent === 0)
|
|
347
|
+
return 0;
|
|
348
|
+
const spanMs = clamp(now - first.at, THROUGHPUT_MIN_SPAN_MS, THROUGHPUT_WINDOW_MS);
|
|
349
|
+
return (recent / spanMs) * 60_000;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=adaptive-concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adaptive-concurrency.js","sourceRoot":"","sources":["../../../../src/transports/bullmq/adaptive-concurrency.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,8 @@ import type { WorkerDescriptor } from '../../handshake/descriptor.js';
|
|
|
2
2
|
import type { GroupHealth, Heartbeat, RemoteTask, RunReply, RunRequest, StartRunMessage, StepResult, TenantEvent, Transport, WorkflowDecision, WorkflowStepEvent, WorkflowTask } from '../../interfaces.js';
|
|
3
3
|
import { type StepHandler } from '../../protocol.js';
|
|
4
4
|
import { type WorkflowTurnHandler } from '../../workflow-turn.js';
|
|
5
|
-
import
|
|
5
|
+
import { type ConcurrencyOption } from './adaptive-concurrency.js';
|
|
6
|
+
import { type BullMQDeps, type RedisLike } from './deps.js';
|
|
6
7
|
export interface BullMQTransportOptions {
|
|
7
8
|
/**
|
|
8
9
|
* Ceiling (ms) `close()` gives the graceful shutdown before it stops waiting and drops the Redis
|
|
@@ -34,6 +35,22 @@ export interface BullMQTransportOptions {
|
|
|
34
35
|
instanceId?: string | undefined;
|
|
35
36
|
/** Where a background failure (a failed heartbeat refresh, a result-publish error) is reported. */
|
|
36
37
|
onError?: ((err: unknown) => void) | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* How many step results this instance settles at once from the results queue. With the default
|
|
40
|
+
* of 1, a wide fan's results queue up behind one checkpoint write at a time on each pod. The engine
|
|
41
|
+
* still resumes one run's turns in order on an instance, so a run never gets two concurrent turns
|
|
42
|
+
* from it. A non-finite or sub-1 value falls back to the default. Default 1.
|
|
43
|
+
*/
|
|
44
|
+
resultConcurrency?: number | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* How many tasks each per-routing-token task worker runs at once. A number pins it (a non-finite or
|
|
47
|
+
* sub-1 value falls back to 1). `'adaptive'` or `{ mode: 'adaptive', min, max, start, tickMs,
|
|
48
|
+
* ramCeilingPct, cpuCeilingPct }` lets an {@link AdaptiveController} move the limit at runtime from
|
|
49
|
+
* step latency, error rate and stalls, braking on RAM (and CPU, when a ceiling is set). The live
|
|
50
|
+
* limit, in-flight count and last adjustment ride the worker heartbeat in both modes. Unset leaves
|
|
51
|
+
* each task worker at the deps' default concurrency.
|
|
52
|
+
*/
|
|
53
|
+
concurrency?: ConcurrencyOption | undefined;
|
|
37
54
|
}
|
|
38
55
|
/**
|
|
39
56
|
* A queue-backed {@link Transport} over BullMQ/Redis, BYTE-COMPATIBLE with the aviary
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bullmq-transport.d.ts","sourceRoot":"","sources":["../../../../src/transports/bullmq/bullmq-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACV,eAAe,EACf,UAAU,EACV,WAAW,EACX,SAAS,EAGT,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"bullmq-transport.d.ts","sourceRoot":"","sources":["../../../../src/transports/bullmq/bullmq-transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { runStepHandler } from '../../protocol.js';
|
|
2
2
|
import { swallowSubscribe } from '../../safe-subscribe.js';
|
|
3
3
|
import { isWorkflowTask } from '../../workflow-turn.js';
|
|
4
|
+
import { AdaptiveController } from './adaptive-concurrency.js';
|
|
5
|
+
import { workerConcurrency, } from './deps.js';
|
|
4
6
|
import { decisionsName, effectivePrefix, heartbeatChannel, resultsName, routingToken, runReplyChannel, runRequestName, startRunName, stepEventsName, tasksName, tenantEventsChannel, workerDescriptorTokenPrefix, workerHeartbeatKey, workerHeartbeatKeyPrefix, } from './naming.js';
|
|
5
7
|
import { buildInstanceId, heartbeatKeyValue, jobOptions, parseHeartbeatValue, resultJobOptions, taskJobOptions, WORKER_HEARTBEAT_INTERVAL_MS, WORKER_HEARTBEAT_TTL_SECONDS, } from './serialization.js';
|
|
6
8
|
/**
|
|
@@ -30,6 +32,11 @@ export class BullMQTransport {
|
|
|
30
32
|
#hasOnError;
|
|
31
33
|
#closing = false;
|
|
32
34
|
#closeTimeoutMs;
|
|
35
|
+
#resultConcurrency;
|
|
36
|
+
#concurrency;
|
|
37
|
+
// Shared by every task worker: tracks in-flight work and the step-completion window, snapshots the
|
|
38
|
+
// heartbeat status and, in adaptive mode, sets every task worker's `concurrency` on each move.
|
|
39
|
+
#controller;
|
|
33
40
|
#handlers = new Map();
|
|
34
41
|
// Worker-side workflow turn consumers, keyed by workflow name — a `workflow`-shaped job routes here.
|
|
35
42
|
#workflowTurns = new Map();
|
|
@@ -46,11 +53,6 @@ export class BullMQTransport {
|
|
|
46
53
|
#workerRedis;
|
|
47
54
|
#workerHeartbeatTimer;
|
|
48
55
|
#heartbeatTokens = new Set();
|
|
49
|
-
// Live execution telemetry carried on the heartbeat (the console's worker cards): jobs currently
|
|
50
|
-
// in a handler, and a bounded window of recent completions for throughput / p95.
|
|
51
|
-
#inFlightJobs = 0;
|
|
52
|
-
#recentCompletions = [];
|
|
53
|
-
#lastCpu;
|
|
54
56
|
// P4 store-less protocol: start-run / run-request queues + consumers, run-reply / tenant-events pub/sub.
|
|
55
57
|
#startRunWorker;
|
|
56
58
|
#runRequestWorker;
|
|
@@ -72,6 +74,21 @@ export class BullMQTransport {
|
|
|
72
74
|
this.#onError = options.onError ?? (() => { });
|
|
73
75
|
this.#hasOnError = options.onError !== undefined;
|
|
74
76
|
this.#closeTimeoutMs = options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
77
|
+
this.#resultConcurrency = workerConcurrency(options.resultConcurrency);
|
|
78
|
+
this.#concurrency = options.concurrency;
|
|
79
|
+
}
|
|
80
|
+
/** The shared concurrency controller, created on first use (a task worker, a job or a heartbeat). */
|
|
81
|
+
#concurrencyController() {
|
|
82
|
+
if (!this.#controller) {
|
|
83
|
+
this.#controller = new AdaptiveController({
|
|
84
|
+
...(this.#concurrency !== undefined ? { concurrency: this.#concurrency } : {}),
|
|
85
|
+
apply: (limit) => {
|
|
86
|
+
for (const worker of this.#taskWorkers.values())
|
|
87
|
+
worker.concurrency = limit;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return this.#controller;
|
|
75
92
|
}
|
|
76
93
|
/** Stable id stamped on this instance's worker-liveness keys. */
|
|
77
94
|
get instanceId() {
|
|
@@ -158,7 +175,10 @@ export class BullMQTransport {
|
|
|
158
175
|
#ensureTaskWorker(token) {
|
|
159
176
|
if (this.#taskWorkers.has(token))
|
|
160
177
|
return;
|
|
161
|
-
const
|
|
178
|
+
const controller = this.#concurrencyController();
|
|
179
|
+
// A new worker starts at the controller's live limit, so a token registered after an adaptive
|
|
180
|
+
// move joins at the current limit rather than at `start`.
|
|
181
|
+
const worker = this.#deps.makeWorker(tasksName(this.#effectivePrefix(), token), (job) => this.#runJob(job.data), this.#concurrency !== undefined ? { concurrency: controller.limit } : undefined);
|
|
162
182
|
// Terminal-failure bridge: a task job reaching a TERMINAL `failed` state is an INFRASTRUCTURE
|
|
163
183
|
// failure (worker crash / stalled-count exhaustion), never a handler business error —
|
|
164
184
|
// `runStepHandler` catches every handler throw and publishes a failed StepResult so the job
|
|
@@ -174,23 +194,31 @@ export class BullMQTransport {
|
|
|
174
194
|
void this.#bridgeTaskFailure(identity, job?.failedReason ?? error?.message ?? 'unknown');
|
|
175
195
|
});
|
|
176
196
|
this.#taskWorkers.set(token, worker);
|
|
197
|
+
controller.start();
|
|
177
198
|
this.#startWorkerHeartbeat(token);
|
|
178
199
|
}
|
|
179
200
|
/** Route one task job by SHAPE (spec §6.3): a workflow turn runs the replay → decision path, a step
|
|
180
201
|
* runs the `runStepHandler` → result path. Both kinds share a token's queue but never each other's. */
|
|
181
202
|
async #runJob(data) {
|
|
203
|
+
const controller = this.#concurrencyController();
|
|
182
204
|
const startedAt = Date.now();
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
controller.onStart();
|
|
206
|
+
// Only step durations feed the latency gradient; a workflow turn still counts as in-flight work
|
|
207
|
+
// and as forward progress for stall detection.
|
|
208
|
+
if (isWorkflowTask(data)) {
|
|
209
|
+
try {
|
|
186
210
|
return await this.#runWorkflowTask(data);
|
|
187
|
-
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
controller.onSettle(Date.now() - startedAt, true, 'workflow');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
let ok = false;
|
|
217
|
+
try {
|
|
218
|
+
ok = await this.#runTask(data);
|
|
188
219
|
}
|
|
189
220
|
finally {
|
|
190
|
-
|
|
191
|
-
this.#recentCompletions.push({ at: Date.now(), ms: Date.now() - startedAt });
|
|
192
|
-
if (this.#recentCompletions.length > 200)
|
|
193
|
-
this.#recentCompletions.shift();
|
|
221
|
+
controller.onSettle(Date.now() - startedAt, ok, 'step');
|
|
194
222
|
}
|
|
195
223
|
}
|
|
196
224
|
/** Worker side: replay a workflow turn and publish its {@link WorkflowDecision} on `${P}-decisions`
|
|
@@ -235,6 +263,7 @@ export class BullMQTransport {
|
|
|
235
263
|
throw err;
|
|
236
264
|
}
|
|
237
265
|
}
|
|
266
|
+
/** Run one step and publish its result. Resolves `true` when the step completed. */
|
|
238
267
|
async #runTask(task) {
|
|
239
268
|
let result;
|
|
240
269
|
try {
|
|
@@ -257,6 +286,7 @@ export class BullMQTransport {
|
|
|
257
286
|
}
|
|
258
287
|
try {
|
|
259
288
|
await this.#queue(resultsName(this.#effectivePrefix())).add('result', result, resultJobOptions());
|
|
289
|
+
return result.status === 'completed';
|
|
260
290
|
}
|
|
261
291
|
catch (err) {
|
|
262
292
|
// The results-queue publish itself threw (e.g. Redis blipped). We still hold `task`, so
|
|
@@ -296,7 +326,7 @@ export class BullMQTransport {
|
|
|
296
326
|
onResult(handler) {
|
|
297
327
|
if (this.#resultsWorker)
|
|
298
328
|
return;
|
|
299
|
-
this.#resultsWorker = this.#deps.makeWorker(resultsName(this.#effectivePrefix()), (job) => handler(job.data));
|
|
329
|
+
this.#resultsWorker = this.#deps.makeWorker(resultsName(this.#effectivePrefix()), (job) => handler(job.data), { concurrency: this.#resultConcurrency });
|
|
300
330
|
this.#resultsWorker.on('failed', (job, err) => {
|
|
301
331
|
if (!job || (job.attemptsMade ?? 0) < (job.opts?.attempts ?? 1))
|
|
302
332
|
return;
|
|
@@ -342,42 +372,12 @@ export class BullMQTransport {
|
|
|
342
372
|
// worker liveness registry — `${P}-worker-heartbeat:${token}:${instanceId}` (SET … EX 35)
|
|
343
373
|
// ---------------------------------------------------------------------------
|
|
344
374
|
/**
|
|
345
|
-
* The live execution/resource snapshot each heartbeat carries
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* cards permanently on "no status". Cheap by construction: process-level reads plus a bounded
|
|
349
|
-
* completions window.
|
|
375
|
+
* The live execution/resource snapshot each heartbeat carries, which lights up the console's worker
|
|
376
|
+
* cards (concurrency mode and live limit, in-flight, RSS/CPU, throughput, p95, last adjustment):
|
|
377
|
+
* the same {@link WorkerStatus} the NestJS fleet stamps.
|
|
350
378
|
*/
|
|
351
379
|
#workerStatus() {
|
|
352
|
-
|
|
353
|
-
const windowStart = now - 60_000;
|
|
354
|
-
const recent = this.#recentCompletions.filter((c) => c.at >= windowStart);
|
|
355
|
-
const durations = [...this.#recentCompletions].map((c) => c.ms).sort((a, b) => a - b);
|
|
356
|
-
const p95 = durations.length
|
|
357
|
-
? durations[Math.min(durations.length - 1, Math.floor(durations.length * 0.95))]
|
|
358
|
-
: undefined;
|
|
359
|
-
const mem = process.memoryUsage();
|
|
360
|
-
// CPU% since the previous beat (user+system µs over wall-clock µs).
|
|
361
|
-
let cpuPct;
|
|
362
|
-
const cpuNow = process.cpuUsage();
|
|
363
|
-
if (this.#lastCpu) {
|
|
364
|
-
const elapsedUs = (now - this.#lastCpu.at) * 1000;
|
|
365
|
-
const usedUs = cpuNow.user - this.#lastCpu.usage.user + (cpuNow.system - this.#lastCpu.usage.system);
|
|
366
|
-
if (elapsedUs > 0)
|
|
367
|
-
cpuPct = Math.round((usedUs / elapsedUs) * 100);
|
|
368
|
-
}
|
|
369
|
-
this.#lastCpu = { usage: cpuNow, at: now };
|
|
370
|
-
return {
|
|
371
|
-
runtime: 'node',
|
|
372
|
-
// One BullMQ Worker per handled routing token, each at the adapter's default concurrency (1)
|
|
373
|
-
// — so this instance can run up to one job per token simultaneously.
|
|
374
|
-
concurrency: { mode: 'fixed', limit: Math.max(1, this.#taskWorkers.size) },
|
|
375
|
-
inFlight: this.#inFlightJobs,
|
|
376
|
-
rssBytes: mem.rss,
|
|
377
|
-
...(cpuPct !== undefined ? { cpuPct } : {}),
|
|
378
|
-
throughputPerMin: recent.length,
|
|
379
|
-
...(p95 !== undefined ? { p95Ms: p95 } : {}),
|
|
380
|
-
};
|
|
380
|
+
return this.#concurrencyController().snapshot();
|
|
381
381
|
}
|
|
382
382
|
/** Refresh EVERY handled routing token's TTL'd liveness key on ONE shared 10s interval until
|
|
383
383
|
* `close()`. A newly added token beats immediately so a freshly-registered handler is visible
|
|
@@ -630,6 +630,7 @@ export class BullMQTransport {
|
|
|
630
630
|
/** Close all workers/queues and disconnect pub/sub + heartbeat connections so the process can exit. */
|
|
631
631
|
async close() {
|
|
632
632
|
this.#closing = true;
|
|
633
|
+
this.#controller?.stop();
|
|
633
634
|
if (this.#workerHeartbeatTimer)
|
|
634
635
|
clearInterval(this.#workerHeartbeatTimer);
|
|
635
636
|
this.#workerHeartbeatTimer = undefined;
|