@glubean/runner 0.7.0 → 0.8.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.
Files changed (71) hide show
  1. package/dist/engine-bridge.d.ts +4 -0
  2. package/dist/engine-bridge.d.ts.map +1 -1
  3. package/dist/engine-bridge.js +10 -1
  4. package/dist/engine-bridge.js.map +1 -1
  5. package/dist/executor.d.ts +2 -2
  6. package/dist/executor.d.ts.map +1 -1
  7. package/dist/executor.js +9 -226
  8. package/dist/executor.js.map +1 -1
  9. package/dist/harness.js +3 -79
  10. package/dist/harness.js.map +1 -1
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +17 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/load/continuation-pool.d.ts +82 -0
  16. package/dist/load/continuation-pool.d.ts.map +1 -0
  17. package/dist/load/continuation-pool.js +154 -0
  18. package/dist/load/continuation-pool.js.map +1 -0
  19. package/dist/load/execute-iteration.d.ts +126 -0
  20. package/dist/load/execute-iteration.d.ts.map +1 -0
  21. package/dist/load/execute-iteration.js +367 -0
  22. package/dist/load/execute-iteration.js.map +1 -0
  23. package/dist/load/histogram.d.ts +63 -0
  24. package/dist/load/histogram.d.ts.map +1 -0
  25. package/dist/load/histogram.js +149 -0
  26. package/dist/load/histogram.js.map +1 -0
  27. package/dist/load/orchestrator.d.ts +55 -0
  28. package/dist/load/orchestrator.d.ts.map +1 -0
  29. package/dist/load/orchestrator.js +571 -0
  30. package/dist/load/orchestrator.js.map +1 -0
  31. package/dist/load/reducer.d.ts +109 -0
  32. package/dist/load/reducer.d.ts.map +1 -0
  33. package/dist/load/reducer.js +718 -0
  34. package/dist/load/reducer.js.map +1 -0
  35. package/dist/load/route-key.d.ts +38 -0
  36. package/dist/load/route-key.d.ts.map +1 -0
  37. package/dist/load/route-key.js +107 -0
  38. package/dist/load/route-key.js.map +1 -0
  39. package/dist/load/samples.d.ts +83 -0
  40. package/dist/load/samples.d.ts.map +1 -0
  41. package/dist/load/samples.js +269 -0
  42. package/dist/load/samples.js.map +1 -0
  43. package/dist/load/sink.d.ts +127 -0
  44. package/dist/load/sink.d.ts.map +1 -0
  45. package/dist/load/sink.js +351 -0
  46. package/dist/load/sink.js.map +1 -0
  47. package/dist/load/subprocess.d.ts +83 -0
  48. package/dist/load/subprocess.d.ts.map +1 -0
  49. package/dist/load/subprocess.js +229 -0
  50. package/dist/load/subprocess.js.map +1 -0
  51. package/dist/load/threshold.d.ts +44 -0
  52. package/dist/load/threshold.d.ts.map +1 -0
  53. package/dist/load/threshold.js +197 -0
  54. package/dist/load/threshold.js.map +1 -0
  55. package/dist/load/timeline.d.ts +36 -0
  56. package/dist/load/timeline.d.ts.map +1 -0
  57. package/dist/load/timeline.js +158 -0
  58. package/dist/load/timeline.js.map +1 -0
  59. package/dist/load-harness.d.ts +2 -0
  60. package/dist/load-harness.d.ts.map +1 -0
  61. package/dist/load-harness.js +105 -0
  62. package/dist/load-harness.js.map +1 -0
  63. package/dist/runner-resolve.d.ts +53 -0
  64. package/dist/runner-resolve.d.ts.map +1 -0
  65. package/dist/runner-resolve.js +264 -0
  66. package/dist/runner-resolve.js.map +1 -0
  67. package/dist/workflow/event-timeline.d.ts +3 -0
  68. package/dist/workflow/event-timeline.d.ts.map +1 -0
  69. package/dist/workflow/event-timeline.js +72 -0
  70. package/dist/workflow/event-timeline.js.map +1 -0
  71. package/package.json +4 -4
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Bounded continuation backlog for producer release (M6).
3
+ *
4
+ * When a load iteration calls `ctx.report.primaryComplete(..., { releaseProducerSlot:
5
+ * true })`, its producer slot is freed to start a NEW primary iteration while the
6
+ * rest of that iteration (the continuation — typically a poll-for-result loop) runs
7
+ * in the background. This pool bounds how many continuations may be in flight at
8
+ * once (`maxOutstanding`): when full, the producer is either back-pressured
9
+ * (`block-producer` — `admit()` waits for a free slot, so the primary ingress rate
10
+ * self-throttles to the continuation drain rate) or the iteration fails
11
+ * (`fail-iteration` — `admit()` throws). An unset `maxOutstanding` is unbounded
12
+ * (release frees the slot with no backlog cap).
13
+ *
14
+ * A parked `admit()` is bounded by `drainTimeoutMs` (from when it parks), if set, so
15
+ * a backlog that never frees (a hung continuation) can't hang the run — even before
16
+ * its bound is reached, when EVERY slot is parked and none can advance. The run's
17
+ * wall-clock (duration) deadline additionally `closeImmediate()`s the pool, cancelling
18
+ * parked producers at once.
19
+ *
20
+ * Single-process / single-threaded: all bookkeeping is synchronous between awaits,
21
+ * so the counters never race. `maxConcurrent` is recorded in the config but
22
+ * coincides with `maxOutstanding` here (every in-flight continuation is an actively
23
+ * scheduled async flow), so this pool enforces the one bound.
24
+ */
25
+ /** Thrown by `admit()` under `fail-iteration` when the backlog is full. */
26
+ export class ContinuationBacklogFullError extends Error {
27
+ constructor() {
28
+ super("continuation backlog is full");
29
+ this.name = "ContinuationBacklogFullError";
30
+ }
31
+ }
32
+ /** Thrown by a parked `admit()` when its drain bound expires or the run's wall-clock
33
+ * deadline closes the pool. Carries how long the producer was stalled and why. */
34
+ export class AdmissionCancelledError extends Error {
35
+ waitMs;
36
+ reason;
37
+ constructor(waitMs, reason) {
38
+ super(`continuation admission cancelled (${reason})`);
39
+ this.waitMs = waitMs;
40
+ this.reason = reason;
41
+ this.name = "AdmissionCancelledError";
42
+ }
43
+ }
44
+ export class ContinuationPool {
45
+ maxOutstanding;
46
+ onBacklogFull;
47
+ drainTimeoutMs;
48
+ now;
49
+ inFlight = 0;
50
+ peak = 0;
51
+ closed = false; // the run's wall-clock deadline — reject new parks at once
52
+ waiters = [];
53
+ constructor(maxOutstanding, onBacklogFull, drainTimeoutMs, now) {
54
+ this.maxOutstanding = maxOutstanding;
55
+ this.onBacklogFull = onBacklogFull;
56
+ this.drainTimeoutMs = drainTimeoutMs;
57
+ this.now = now;
58
+ }
59
+ /** Continuations currently in flight (admitted, not yet released). */
60
+ get outstanding() {
61
+ return this.inFlight;
62
+ }
63
+ /** Highest concurrent in-flight count reached (for the backlog report). */
64
+ get peakOutstanding() {
65
+ return this.peak;
66
+ }
67
+ /** Whether the backlog is at capacity right now (the next admit would block/fail). */
68
+ get atCapacity() {
69
+ return this.maxOutstanding !== undefined && this.inFlight >= this.maxOutstanding;
70
+ }
71
+ /**
72
+ * Reserve a continuation slot. Returns the back-pressure wait in ms (0 if a slot
73
+ * was immediately free). Throws `ContinuationBacklogFullError` under
74
+ * `fail-iteration` when full, or `AdmissionCancelledError` when a parked wait is
75
+ * cut short by its drain bound or the run's wall-clock deadline.
76
+ */
77
+ async admit() {
78
+ // The run's wall-clock deadline closes the pool and takes precedence over EVERYTHING
79
+ // below — capacity AND backlog policy. Once it fires, every later release is
80
+ // released-for-drain (runDeadlineReached): admitting a fresh continuation as a normal
81
+ // `producer:released` (free capacity) would misaccount post-deadline work, and
82
+ // failing it (fail-iteration) would spuriously fail a primary the run simply outran.
83
+ // The tail still drains either way; this is purely correct deadline accounting.
84
+ if (this.closed) {
85
+ throw new AdmissionCancelledError(0, "runDeadlineReached"); // wall-clock up: never admit anew
86
+ }
87
+ if (!this.atCapacity) {
88
+ this.acquire();
89
+ return 0;
90
+ }
91
+ if (this.onBacklogFull === "fail-iteration") {
92
+ throw new ContinuationBacklogFullError();
93
+ }
94
+ // block-producer: park until a releasing continuation hands its slot over, the
95
+ // drain timeout expires, or the run's wall-clock deadline closes the pool.
96
+ const start = this.now();
97
+ const waiter = { resolve: () => { } };
98
+ const granted = await new Promise((resolve) => {
99
+ waiter.resolve = resolve;
100
+ this.waiters.push(waiter);
101
+ if (this.drainTimeoutMs !== undefined) {
102
+ waiter.timer = setTimeout(() => this.cancel(waiter, "drainTimeout"), this.drainTimeoutMs);
103
+ }
104
+ });
105
+ const waited = Math.max(0, this.now() - start);
106
+ if (!granted)
107
+ throw new AdmissionCancelledError(waited, waiter.reason ?? "runDeadlineReached");
108
+ return waited;
109
+ }
110
+ /**
111
+ * Free a continuation slot. If a producer is parked, hand the slot DIRECTLY to it
112
+ * (in-flight count unchanged — capacity transfers) so no third producer can slip
113
+ * in between; otherwise decrement.
114
+ */
115
+ release() {
116
+ const next = this.waiters.shift();
117
+ if (next) {
118
+ if (next.timer !== undefined)
119
+ clearTimeout(next.timer);
120
+ next.resolve(true); // grant the slot
121
+ return;
122
+ }
123
+ this.inFlight = Math.max(0, this.inFlight - 1);
124
+ }
125
+ /**
126
+ * The run's wall-clock (duration) deadline: cancel every parked producer at once
127
+ * and reject any later park immediately, so a slot blocked on a release can't
128
+ * outrun the bound (its tail still drains, bounded by the run's drain phase).
129
+ */
130
+ closeImmediate() {
131
+ this.closed = true;
132
+ for (const w of [...this.waiters])
133
+ this.cancel(w, "runDeadlineReached");
134
+ }
135
+ /** Remove a parked producer and cancel its wait, recording WHY (its per-park drain
136
+ * bound expired, or the run's wall-clock deadline closed the pool) so `admit()`
137
+ * can throw the right cause. */
138
+ cancel(waiter, reason) {
139
+ const i = this.waiters.indexOf(waiter);
140
+ if (i < 0)
141
+ return; // already granted / cancelled
142
+ this.waiters.splice(i, 1);
143
+ if (waiter.timer !== undefined)
144
+ clearTimeout(waiter.timer);
145
+ waiter.reason = reason;
146
+ waiter.resolve(false);
147
+ }
148
+ acquire() {
149
+ this.inFlight += 1;
150
+ if (this.inFlight > this.peak)
151
+ this.peak = this.inFlight;
152
+ }
153
+ }
154
+ //# sourceMappingURL=continuation-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation-pool.js","sourceRoot":"","sources":["../../src/load/continuation-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,2EAA2E;AAC3E,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IACrD;QACE,KAAK,CAAC,8BAA8B,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAC7C,CAAC;CACF;AAQD;mFACmF;AACnF,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAC3B;IAAyB;IAA9C,YAAqB,MAAc,EAAW,MAA6B;QACzE,KAAK,CAAC,qCAAqC,MAAM,GAAG,CAAC,CAAC;QADnC,WAAM,GAAN,MAAM,CAAQ;QAAW,WAAM,GAAN,MAAM,CAAuB;QAEzE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAWD,MAAM,OAAO,gBAAgB;IAOR;IACA;IACA;IACA;IATX,QAAQ,GAAG,CAAC,CAAC;IACb,IAAI,GAAG,CAAC,CAAC;IACT,MAAM,GAAG,KAAK,CAAC,CAAC,2DAA2D;IAClE,OAAO,GAAa,EAAE,CAAC;IAExC,YACmB,cAAkC,EAClC,aAAkD,EAClD,cAAkC,EAClC,GAAiB;QAHjB,mBAAc,GAAd,cAAc,CAAoB;QAClC,kBAAa,GAAb,aAAa,CAAqC;QAClD,mBAAc,GAAd,cAAc,CAAoB;QAClC,QAAG,GAAH,GAAG,CAAc;IACjC,CAAC;IAEJ,sEAAsE;IACtE,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,2EAA2E;IAC3E,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,sFAAsF;IACtF,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,cAAc,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC;IACnF,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,qFAAqF;QACrF,6EAA6E;QAC7E,sFAAsF;QACtF,+EAA+E;QAC/E,qFAAqF;QACrF,gFAAgF;QAChF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,uBAAuB,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,kCAAkC;QAChG,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,IAAI,CAAC,aAAa,KAAK,gBAAgB,EAAE,CAAC;YAC5C,MAAM,IAAI,4BAA4B,EAAE,CAAC;QAC3C,CAAC;QACD,+EAA+E;QAC/E,2EAA2E;QAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,MAAM,GAAW,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;QAC7C,MAAM,OAAO,GAAG,MAAM,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;YACrD,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;YACzB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;gBACtC,MAAM,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,oBAAoB,CAAC,CAAC;QAC/F,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACH,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAClC,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;gBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,iBAAiB;YACrC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;IACjD,CAAC;IAED;;;;OAIG;IACH,cAAc;QACZ,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC;IAC1E,CAAC;IAED;;qCAEiC;IACzB,MAAM,CAAC,MAAc,EAAE,MAA6B;QAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,8BAA8B;QACjD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1B,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;YAAE,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC3D,CAAC;CACF"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Single load-iteration executor — runs ONE scenario iteration through the shared
3
+ * engine `RunnerCore`, reusing the exact test() execution surface (ctx.http /
4
+ * session / vars / steps / branch / poll / setup / teardown) via the engine's
5
+ * generic `ctxExtensions` seam.
6
+ *
7
+ * A `LoadScenario` is compiled ONCE (`compileLoadScenario`) into engine steps:
8
+ * `LoadStepDefinition` mirrors the SDK's step/branch/poll runtime shape, which the
9
+ * engine detects structurally — so the conversion is a re-label, not a re-built
10
+ * run-loop. Compilation also resolves each step's `LoadAssertionFailureMode` into
11
+ * the engine's neutral `continueOnAssertionFailure` hint (the load `continue`
12
+ * default keeps a transaction running after a soft assertion failure) and carries
13
+ * `meta.group` through for report attribution. Load-only ctx members (`input` /
14
+ * `producerSlot` / `iteration` / `now` / `report`) ride in on `ctxExtensions`.
15
+ *
16
+ * The executor owns the iteration lifecycle: it brackets the run with
17
+ * `iteration:start` / `iteration:end` on the sink and threads a per-iteration
18
+ * `report` whose checkpoints emit through the same sink. Concurrency, pacing,
19
+ * feeder allocation, and the producer-release boundary live in the orchestrator
20
+ * (M3-f) and the phase split (M5/M6) — this function runs exactly one iteration.
21
+ */
22
+ import type { RunnerCore, StepDef, TestDef, TestFn, TestResult } from "@glubean/engine";
23
+ import type { LoadAssertionFailureMode, LoadErrorKind, LoadIteration, LoadProducerSlot, LoadScenario } from "@glubean/sdk/load";
24
+ import { type ContinuationPool } from "./continuation-pool.js";
25
+ import type { LoadIterationEnvelope, LoadSink } from "./sink.js";
26
+ /** Phantom-typed Input so a compiled scenario stays coupled to its input type. */
27
+ declare const COMPILED_INPUT: unique symbol;
28
+ /**
29
+ * A `LoadScenario` lowered to engine-ready steps, computed once and reused across
30
+ * every iteration of the run (only the per-iteration `meta.id` differs). The
31
+ * `steps` already carry the resolved `continueOnAssertionFailure` hint + group.
32
+ */
33
+ export interface CompiledLoadScenario<Input = unknown> {
34
+ /** @internal phantom — preserves the Input type through compilation. */
35
+ readonly [COMPILED_INPUT]?: (input: Input) => void;
36
+ scenarioId: string;
37
+ name: string;
38
+ setup?: TestFn;
39
+ steps: StepDef[];
40
+ teardown?: TestFn;
41
+ }
42
+ /** Options for resolving a scenario's assertion-failure policy chain. */
43
+ export interface CompileLoadScenarioOptions {
44
+ /**
45
+ * The `loadRunner`-level default `onFailure` (the lowest-precedence rung of the
46
+ * resolution chain). Omitted → the documented default `"continue"`.
47
+ */
48
+ defaultOnFailure?: LoadAssertionFailureMode;
49
+ }
50
+ /** Compile a `LoadScenario` to reusable engine steps (do this ONCE per scenario). */
51
+ export declare function compileLoadScenario<Input>(scenario: LoadScenario<Input, any>, opts?: CompileLoadScenarioOptions): CompiledLoadScenario<Input>;
52
+ /** Re-label a compiled scenario as an engine `TestDef` for one iteration run. */
53
+ export declare function loadScenarioToTestDef(compiled: CompiledLoadScenario<any>, iterationId: string): TestDef;
54
+ /** Inputs for one iteration run. */
55
+ export interface RunLoadIterationArgs<Input = unknown> {
56
+ core: RunnerCore;
57
+ sink: LoadSink;
58
+ /** The compiled scenario (compile once with `compileLoadScenario`, reuse). */
59
+ scenario: CompiledLoadScenario<Input>;
60
+ envelope: LoadIterationEnvelope;
61
+ /** Per-iteration input produced by `loadRunner().input` (becomes `ctx.input`). */
62
+ input: Input;
63
+ producerSlot: LoadProducerSlot;
64
+ iteration: LoadIteration;
65
+ /** Per-iteration session overlay (copy-on-write isolation lands in M3-f). */
66
+ session?: Record<string, unknown>;
67
+ /** Attribution for `iteration:start` (feeder allocation lands in M3-f). */
68
+ inputKey?: string;
69
+ feederKeys?: Record<string, string>;
70
+ /** Monotonic clock for the iteration duration; defaults to `performance.now`. */
71
+ now?: () => number;
72
+ /** Producer-release wiring (M6). When present, `ctx.report.primaryComplete(...,
73
+ * { releaseProducerSlot: true })` reserves a continuation slot from this pool
74
+ * (back-pressure / fail-iteration) and frees the producer slot at the boundary;
75
+ * absent → the M5 closed model (release is a no-op phase split). */
76
+ continuation?: {
77
+ pool: ContinuationPool;
78
+ };
79
+ /** Run-level abort signal. Threaded to the engine so the run can truly cancel this
80
+ * iteration's in-flight HTTP / poll tail — the orchestrator fires it at finalization
81
+ * to kill continuation tails the drain phase abandons (instead of leaving them to run
82
+ * to completion in the background). */
83
+ signal?: AbortSignal;
84
+ }
85
+ /**
86
+ * Handle to a started iteration whose producer slot may be released mid-run (M6).
87
+ * `primaryDone` lets the orchestrator free the slot at the right moment;
88
+ * `completed` is the whole iteration (continuation included) for drain + counting.
89
+ */
90
+ export interface LoadIterationHandle {
91
+ /** Resolves when the PRODUCER SLOT is free to start the next primary: at the
92
+ * release boundary (`released: true`) or, with no release, at full completion. */
93
+ primaryDone: Promise<{
94
+ released: boolean;
95
+ }>;
96
+ /** Resolves when the whole iteration (incl. any continuation) settles. */
97
+ completed: Promise<RunLoadIterationResult>;
98
+ }
99
+ /** Outcome of one iteration run (the engine `TestResult` plus the load verdict). */
100
+ export interface RunLoadIterationResult {
101
+ ok: boolean;
102
+ durationMs: number;
103
+ errorKind?: LoadErrorKind;
104
+ /** The scenario called `ctx.skip()` — the iteration opted out (not a failure).
105
+ * Distinct skip accounting in the artifact is future work; for now a skipped
106
+ * iteration is reported `ok` so it never pollutes failure/assertion stats. */
107
+ skipped?: boolean;
108
+ /** The engine result, or null if the core itself threw (infra error). */
109
+ result: TestResult | null;
110
+ }
111
+ /**
112
+ * Start one scenario iteration through the engine core, bracketing it with
113
+ * `iteration:start` / `iteration:end`. Returns a handle: `primaryDone` resolves
114
+ * when the producer slot is free (release boundary, or full completion when no
115
+ * release), and `completed` resolves when the whole iteration settles. Never
116
+ * rejects: an infra-level throw from the core is recorded as a failed iteration.
117
+ */
118
+ export declare function startLoadIteration<Input>(args: RunLoadIterationArgs<Input>): LoadIterationHandle;
119
+ /**
120
+ * Run one scenario iteration to completion (closed model). The M5/M3 entry: it
121
+ * awaits the WHOLE iteration. The orchestrator uses `startLoadIteration` directly
122
+ * for M6 producer-release scheduling.
123
+ */
124
+ export declare function runLoadIteration<Input>(args: RunLoadIterationArgs<Input>): Promise<RunLoadIterationResult>;
125
+ export {};
126
+ //# sourceMappingURL=execute-iteration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute-iteration.d.ts","sourceRoot":"","sources":["../../src/load/execute-iteration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,367 @@
1
+ import { isLoadBranchStep } from "@glubean/sdk/load";
2
+ import { AdmissionCancelledError, ContinuationBacklogFullError } from "./continuation-pool.js";
3
+ /**
4
+ * Resolve a step's effective `onFailure` (most specific wins):
5
+ * step `assertions.onFailure` > scenario `assertions.onFailure`
6
+ * > loadRunner default > `"continue"`.
7
+ */
8
+ function resolveOnFailure(
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ step, scenarioMode, runnerDefault) {
11
+ return (step.meta.assertions?.onFailure ?? scenarioMode ?? runnerDefault ?? "continue");
12
+ }
13
+ /** Lower one load step (recursing into branch sub-steps) to an engine StepDef.
14
+ * `inheritedGroup` is the group of an enclosing `.group()`-wrapped branch — only
15
+ * branch LEAVES emit `step_start`, so the branch node's group must flow down. */
16
+ function compileStep(
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ step, scenarioMode, runnerDefault, inheritedGroup) {
19
+ const onFailure = resolveOnFailure(step, scenarioMode, runnerDefault);
20
+ // A step's own group wins; otherwise it inherits the enclosing branch's group.
21
+ const effectiveGroup = step.meta.group ?? inheritedGroup;
22
+ // Preserve the WHOLE step meta (name / group / timeout / retries / retryDelay /
23
+ // backoff — all read by the engine) and only ADD the resolved policy hint + the
24
+ // effective group. Only `continue` keeps the transaction running after a soft
25
+ // assertion failure; `skipRemainingSteps` / `abortIteration` both halt (the
26
+ // engine treats them the same — the iteration is marked failed and the rest
27
+ // skipped either way). The load-only `assertions` field rides along inertly; the
28
+ // engine never reads it.
29
+ const meta = {
30
+ ...step.meta,
31
+ ...(effectiveGroup !== undefined ? { group: effectiveGroup } : {}),
32
+ continueOnAssertionFailure: onFailure === "continue",
33
+ };
34
+ // Branch/poll are detected structurally by the engine via these fields; carry
35
+ // them through, recursing so nested case/default steps inherit the policy chain
36
+ // AND the enclosing group.
37
+ if (isLoadBranchStep(step)) {
38
+ const branch = step.branch;
39
+ return {
40
+ meta,
41
+ fn: step.fn,
42
+ branch: {
43
+ ...branch,
44
+ cases: branch.cases.map((c) => ({
45
+ ...c,
46
+ steps: c.steps.map((s) => compileStep(s, scenarioMode, runnerDefault, effectiveGroup)),
47
+ })),
48
+ default: branch.default.map((s) => compileStep(s, scenarioMode, runnerDefault, effectiveGroup)),
49
+ },
50
+ };
51
+ }
52
+ // Normal or poll step (poll carries its own `.poll` data the engine reads).
53
+ return { meta, fn: step.fn, ...("poll" in step ? { poll: step.poll } : {}) };
54
+ }
55
+ /** Compile a `LoadScenario` to reusable engine steps (do this ONCE per scenario). */
56
+ export function compileLoadScenario(
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ scenario, opts = {}) {
59
+ const scenarioMode = scenario.meta.assertions?.onFailure;
60
+ return {
61
+ scenarioId: scenario.meta.id,
62
+ name: scenario.meta.id,
63
+ setup: scenario.setup,
64
+ steps: scenario.steps.map((s) => compileStep(s, scenarioMode, opts.defaultOnFailure)),
65
+ teardown: scenario.teardown,
66
+ };
67
+ }
68
+ /** Re-label a compiled scenario as an engine `TestDef` for one iteration run. */
69
+ export function loadScenarioToTestDef(
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ compiled, iterationId) {
72
+ return {
73
+ meta: { id: iterationId, name: compiled.name },
74
+ type: "steps",
75
+ setup: compiled.setup,
76
+ steps: compiled.steps,
77
+ teardown: compiled.teardown,
78
+ };
79
+ }
80
+ /**
81
+ * Map an engine `TestResult` to a load `errorKind`.
82
+ *
83
+ * A load scenario is always lowered to a STEPS `TestDef`, and the engine catches
84
+ * step/poll/teardown throws — so a top-level throw (`result.threw`) means the
85
+ * scenario `setup` failed: classify it as `setupError`. For caught step/poll
86
+ * failures the engine surfaces the error NAME in `stepErrorName`, so a request
87
+ * timeout (`TimeoutError`), step timeout (`StepTimeoutError`), or HTTP error
88
+ * (`HTTPError`) still classifies correctly; a soft assertion failure leaves no
89
+ * thrown error and the generic "assertion failed" text.
90
+ *
91
+ * NOTE: with the engine's default `throwHttpErrors: false` a non-2xx response does
92
+ * NOT throw — it surfaces as the endpoint's `ok:false` / status in
93
+ * `request:observed` (endpoint error rate), not as an iteration `errorKind`; only
94
+ * an explicit HTTP throw yields `"http"` here.
95
+ */
96
+ function classifyIterationError(result, runAborted) {
97
+ // A run-level abort (the orchestrator's `signal`) — distinct from a user failure. Only
98
+ // when the orchestrator actually aborted THIS iteration: the engine surfaces it as a
99
+ // top-level "AbortError" throw (a simple/setup body awaiting an aborted request), a
100
+ // caught step/poll "AbortError", or the post-loop "run aborted" verdict (an opaque,
101
+ // non-cancellable await that outran the abort). A scenario that aborts its OWN request
102
+ // raises the same AbortError, so gate on `runAborted` — else an ordinary user
103
+ // cancellation would be mislabelled and real setup/step errors under-reported.
104
+ if (runAborted && (result.errorName === "AbortError" || result.stepErrorName === "AbortError" || result.error === "run aborted")) {
105
+ return "aborted";
106
+ }
107
+ if (result.threw)
108
+ return "setupError";
109
+ const name = result.stepErrorName;
110
+ const msg = result.error ?? "";
111
+ if (name === "TimeoutError" || name === "StepTimeoutError" || /timed out/i.test(msg))
112
+ return "timeout";
113
+ if (name === "HTTPError")
114
+ return "http";
115
+ // A backlog-full release rejection (fail-iteration policy) failed the iteration.
116
+ if (name === "ContinuationBacklogFullError")
117
+ return "continuationBacklogFull";
118
+ if (name !== undefined)
119
+ return "stepError";
120
+ if (msg === "" || msg === "assertion failed")
121
+ return "assertion";
122
+ return "stepError";
123
+ }
124
+ /**
125
+ * A per-iteration `report` whose signals emit through the sink with attribution.
126
+ *
127
+ * `primaryComplete` records the measurement boundary (M5): the FIRST call stamps
128
+ * `primaryDurationMs` (iteration start → now), emits `producer:primaryCompleted`,
129
+ * and flips the sink into this iteration's continuation phase; later calls are
130
+ * duplicates. With `releaseProducerSlot: true` AND a `release` coordinator (M6) it
131
+ * also frees the producer slot via the continuation pool (back-pressure /
132
+ * fail-iteration); without a coordinator release is a no-op phase split.
133
+ */
134
+ function makeIterationReport(sink, env, start, now, release, rejectDuplicateRelease) {
135
+ let primaryCompleted = false;
136
+ return {
137
+ checkpoint(id, data) {
138
+ sink.emitCheckpoint(env, id, data);
139
+ },
140
+ async primaryComplete(id, data) {
141
+ if (primaryCompleted) {
142
+ // A second boundary in the same iteration is ignored (one boundary per
143
+ // logical iteration); report it as a duplicate for diagnostics. A duplicate
144
+ // that re-requested release is recorded as a rejected duplicate signal.
145
+ if (data?.releaseProducerSlot === true)
146
+ rejectDuplicateRelease?.(id);
147
+ return { measuredPrimaryComplete: false, releasedProducerSlot: false, duplicate: true, backpressureMs: 0 };
148
+ }
149
+ primaryCompleted = true;
150
+ // `releaseProducerSlot` only requests a release when a coordinator is wired (M6).
151
+ // Without one (the M5-only phase split), it's a documented no-op — so report
152
+ // releaseRequested:false, else the reducer would treat this boundary as a producer
153
+ // parked on backlog and surface spurious blockedOnBacklog / a continuation summary.
154
+ const releaseRequested = data?.releaseProducerSlot === true && release !== undefined;
155
+ const primaryDurationMs = Math.max(0, now() - start);
156
+ sink.emitPrimaryCompleted(env, { primaryId: id, primaryDurationMs, releaseRequested });
157
+ if (releaseRequested && release) {
158
+ // Release the producer slot (may back-pressure, or throw under
159
+ // fail-iteration → the iteration fails). Returns the full receipt.
160
+ return release({ primaryId: id, primaryDurationMs });
161
+ }
162
+ return { measuredPrimaryComplete: true, releasedProducerSlot: false, duplicate: false, backpressureMs: 0 };
163
+ },
164
+ };
165
+ }
166
+ /**
167
+ * Start one scenario iteration through the engine core, bracketing it with
168
+ * `iteration:start` / `iteration:end`. Returns a handle: `primaryDone` resolves
169
+ * when the producer slot is free (release boundary, or full completion when no
170
+ * release), and `completed` resolves when the whole iteration settles. Never
171
+ * rejects: an infra-level throw from the core is recorded as a failed iteration.
172
+ */
173
+ export function startLoadIteration(args) {
174
+ const { core, sink, scenario, envelope, input, producerSlot, iteration, session } = args;
175
+ const pool = args.continuation?.pool;
176
+ const now = args.now ?? (() => performance.now());
177
+ sink.beginIteration(envelope);
178
+ sink.emitIterationStart(envelope, {
179
+ ...(args.inputKey !== undefined ? { inputKey: args.inputKey } : {}),
180
+ ...(args.feederKeys !== undefined ? { feederKeys: args.feederKeys } : {}),
181
+ });
182
+ // Stamp the iteration start BEFORE building the report so `primaryComplete` can
183
+ // measure `primaryDurationMs` from it; `start` is also the end-to-end baseline.
184
+ const start = now();
185
+ // The producer-slot lifecycle (see SlotDisposition). Starts "held"; the release
186
+ // coordinator moves it to "released" / "releasedForDrain" at most once. The `as`
187
+ // widens past the "held" literal so reads in `completed` see the full union — the
188
+ // coordinator's writes are in an async closure CFA can't fold back into the narrowing.
189
+ let slot = "held";
190
+ // The iteration couldn't honor the continuation backlog bound — a `fail-iteration`
191
+ // release hit a full backlog, or a `block-producer` release parked past its drain
192
+ // bound — so it MUST fail, regardless of step retries or a tail that later settles.
193
+ let failedBacklogBound = false;
194
+ // The iteration has fully ended (its `completed` finalizer ran). Guards a release
195
+ // still parked on back-pressure so it hands its slot back instead of leaking.
196
+ let iterationSettled = false;
197
+ let resolvePrimaryDone;
198
+ const primaryDone = new Promise((r) => { resolvePrimaryDone = r; });
199
+ // M6 release coordinator (only wired when a continuation pool is provided).
200
+ const release = pool
201
+ ? async ({ primaryId, primaryDurationMs }) => {
202
+ try {
203
+ const backpressureMs = await pool.admit(); // back-pressure, or throw under fail-iteration
204
+ if (iterationSettled) {
205
+ // The iteration already ended (e.g. a step timeout) while this release
206
+ // was parked on back-pressure. Hand the just-acquired slot straight back
207
+ // and do NOT release the producer slot or emit a late producer:released —
208
+ // otherwise the slot would leak at capacity and deadlock future releases.
209
+ pool.release();
210
+ return { measuredPrimaryComplete: true, releasedProducerSlot: false, duplicate: false, backpressureMs };
211
+ }
212
+ slot = "released";
213
+ sink.emitProducerReleased(envelope, {
214
+ releaseId: primaryId,
215
+ primaryDurationMs,
216
+ continuationBacklog: pool.outstanding,
217
+ backpressureMs,
218
+ });
219
+ resolvePrimaryDone({ released: true }); // free the producer slot NOW
220
+ return { measuredPrimaryComplete: true, releasedProducerSlot: true, duplicate: false, backpressureMs };
221
+ }
222
+ catch (e) {
223
+ if (e instanceof AdmissionCancelledError) {
224
+ if (iterationSettled) {
225
+ // The iteration already ended (e.g. a step timeout) before the cancel
226
+ // hit this parked admit. It's no longer tracked — don't emit a
227
+ // rejection or resolve primaryDone (already done), just return.
228
+ return { measuredPrimaryComplete: true, releasedProducerSlot: false, duplicate: false, backpressureMs: e.waitMs };
229
+ }
230
+ // Either the run's wall-clock deadline closed the pool, or this release's
231
+ // own drain bound expired (the backlog never freed). Both free the producer
232
+ // for liveness and leave the continuation tail to be drained at run-end
233
+ // (no pool slot acquired), but they're DIFFERENT outcomes:
234
+ // - runDeadlineReached: the run is ending; the primary was already complete
235
+ // and is released-for-drain — NOT an iteration failure.
236
+ // - drainTimeout: the backlog stayed full past the drain bound mid-run — the
237
+ // same terminal outcome as fail-iteration, so the iteration MUST fail (else
238
+ // the slot would silently run over the configured backlog bound).
239
+ sink.emitProducerReleaseRejected(envelope, {
240
+ releaseId: primaryId,
241
+ reason: e.reason,
242
+ waitMs: e.waitMs,
243
+ continuationBacklog: pool.outstanding,
244
+ });
245
+ slot = "releasedForDrain"; // freed at the boundary, no pool slot to pair with release()
246
+ if (e.reason === "drainTimeout")
247
+ failedBacklogBound = true;
248
+ resolvePrimaryDone({ released: true }); // free the slot + track the tail
249
+ return { measuredPrimaryComplete: true, releasedProducerSlot: false, duplicate: false, backpressureMs: e.waitMs };
250
+ }
251
+ if (e instanceof ContinuationBacklogFullError) {
252
+ sink.emitProducerReleaseRejected(envelope, {
253
+ releaseId: primaryId,
254
+ reason: "continuationBacklogFull",
255
+ waitMs: 0,
256
+ continuationBacklog: pool.outstanding,
257
+ });
258
+ // fail-iteration: the iteration MUST fail. Throw the TYPED error so a
259
+ // non-retried step classifies as "continuationBacklogFull"; also latch the
260
+ // flag so that if the step has `retries`, the retry's duplicate
261
+ // primaryComplete (which returns success) can't let the iteration pass. The
262
+ // slot stays "held" — the throw propagates and the finalizer frees it.
263
+ failedBacklogBound = true;
264
+ }
265
+ throw e;
266
+ }
267
+ }
268
+ : undefined;
269
+ // Record a duplicate release request (a second primaryComplete that re-asks for
270
+ // release) so the continuation summary's duplicateReleaseSignals reflects it.
271
+ const rejectDuplicateRelease = pool
272
+ ? (id) => sink.emitProducerReleaseRejected(envelope, {
273
+ releaseId: id,
274
+ reason: "duplicateRelease",
275
+ waitMs: 0,
276
+ continuationBacklog: pool.outstanding,
277
+ })
278
+ : undefined;
279
+ const scope = {
280
+ ...(session !== undefined ? { session } : {}),
281
+ ...(args.signal !== undefined ? { signal: args.signal } : {}),
282
+ ctxExtensions: {
283
+ input,
284
+ producerSlot,
285
+ iteration,
286
+ now,
287
+ report: makeIterationReport(sink, envelope, start, now, release, rejectDuplicateRelease),
288
+ },
289
+ };
290
+ const completed = (async () => {
291
+ let result = null;
292
+ let ok = false;
293
+ let skipped = false;
294
+ let errorKind;
295
+ try {
296
+ result = await core.run(loadScenarioToTestDef(scenario, envelope.iterationId), scope);
297
+ if (result.status === "skipped") {
298
+ // `ctx.skip()` opted this iteration out — not a success and not a failure.
299
+ ok = true;
300
+ skipped = true;
301
+ }
302
+ else {
303
+ ok = result.status === "ok";
304
+ // Only treat an AbortError as a cancellation when the orchestrator's run signal
305
+ // actually aborted this iteration (a scenario aborting its own request is a real
306
+ // user failure, not an orchestrator cancellation).
307
+ errorKind = ok ? undefined : classifyIterationError(result, args.signal?.aborted === true);
308
+ }
309
+ }
310
+ catch {
311
+ // The engine resolves user errors into a TestResult; reaching here means the
312
+ // core itself threw (infra). Record it as a runner-crash-flavoured failure.
313
+ ok = false;
314
+ errorKind = "runnerCrash";
315
+ }
316
+ // A backlog-bound failure is terminal regardless of step retries / a tail that
317
+ // later settles: the iteration couldn't honor the backlog bound, so it fails. This
318
+ // covers both fail-iteration (full → throw immediately) and block-producer
319
+ // drain-timeout (parked past the drain bound → give up) — a retry's duplicate
320
+ // primaryComplete returns success and could otherwise let the iteration (and a
321
+ // re-run primary side effect) pass.
322
+ if (failedBacklogBound) {
323
+ ok = false;
324
+ errorKind = "continuationBacklogFull";
325
+ }
326
+ const durationMs = now() - start;
327
+ // Mark the iteration done BEFORE finalizing, so a release still parked on
328
+ // back-pressure (its step having timed out) returns its slot instead of leaking.
329
+ iterationSettled = true;
330
+ try {
331
+ sink.emitIterationEnd(envelope, { ok, durationMs, ...(errorKind !== undefined ? { errorKind } : {}) });
332
+ sink.endIteration(envelope.iterationId);
333
+ }
334
+ catch {
335
+ // Finalization is best-effort — a sink hiccup must neither reject this
336
+ // promise nor (via the finally below) hang the producer slot.
337
+ }
338
+ finally {
339
+ // Settle the slot per its end-state (see SlotDisposition). This MUST run so the
340
+ // slot loop's `await primaryDone` never hangs.
341
+ // - "released": pair the acquired pool slot with a release now the tail settled.
342
+ // - "held": no release happened — free the producer slot it held to the end.
343
+ // - "releasedForDrain": primaryDone already resolved at the boundary, no pool slot held.
344
+ if (slot === "released")
345
+ pool?.release();
346
+ else if (slot === "held")
347
+ resolvePrimaryDone({ released: false });
348
+ }
349
+ return {
350
+ ok,
351
+ durationMs,
352
+ ...(errorKind !== undefined ? { errorKind } : {}),
353
+ ...(skipped ? { skipped: true } : {}),
354
+ result,
355
+ };
356
+ })();
357
+ return { primaryDone, completed };
358
+ }
359
+ /**
360
+ * Run one scenario iteration to completion (closed model). The M5/M3 entry: it
361
+ * awaits the WHOLE iteration. The orchestrator uses `startLoadIteration` directly
362
+ * for M6 producer-release scheduling.
363
+ */
364
+ export async function runLoadIteration(args) {
365
+ return startLoadIteration(args).completed;
366
+ }
367
+ //# sourceMappingURL=execute-iteration.js.map