@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.
- package/dist/engine-bridge.d.ts +4 -0
- package/dist/engine-bridge.d.ts.map +1 -1
- package/dist/engine-bridge.js +10 -1
- package/dist/engine-bridge.js.map +1 -1
- package/dist/executor.d.ts +2 -2
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +9 -226
- package/dist/executor.js.map +1 -1
- package/dist/harness.js +3 -79
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/load/continuation-pool.d.ts +82 -0
- package/dist/load/continuation-pool.d.ts.map +1 -0
- package/dist/load/continuation-pool.js +154 -0
- package/dist/load/continuation-pool.js.map +1 -0
- package/dist/load/execute-iteration.d.ts +126 -0
- package/dist/load/execute-iteration.d.ts.map +1 -0
- package/dist/load/execute-iteration.js +367 -0
- package/dist/load/execute-iteration.js.map +1 -0
- package/dist/load/histogram.d.ts +63 -0
- package/dist/load/histogram.d.ts.map +1 -0
- package/dist/load/histogram.js +149 -0
- package/dist/load/histogram.js.map +1 -0
- package/dist/load/orchestrator.d.ts +55 -0
- package/dist/load/orchestrator.d.ts.map +1 -0
- package/dist/load/orchestrator.js +571 -0
- package/dist/load/orchestrator.js.map +1 -0
- package/dist/load/reducer.d.ts +109 -0
- package/dist/load/reducer.d.ts.map +1 -0
- package/dist/load/reducer.js +718 -0
- package/dist/load/reducer.js.map +1 -0
- package/dist/load/route-key.d.ts +38 -0
- package/dist/load/route-key.d.ts.map +1 -0
- package/dist/load/route-key.js +107 -0
- package/dist/load/route-key.js.map +1 -0
- package/dist/load/samples.d.ts +83 -0
- package/dist/load/samples.d.ts.map +1 -0
- package/dist/load/samples.js +269 -0
- package/dist/load/samples.js.map +1 -0
- package/dist/load/sink.d.ts +127 -0
- package/dist/load/sink.d.ts.map +1 -0
- package/dist/load/sink.js +351 -0
- package/dist/load/sink.js.map +1 -0
- package/dist/load/subprocess.d.ts +83 -0
- package/dist/load/subprocess.d.ts.map +1 -0
- package/dist/load/subprocess.js +229 -0
- package/dist/load/subprocess.js.map +1 -0
- package/dist/load/threshold.d.ts +44 -0
- package/dist/load/threshold.d.ts.map +1 -0
- package/dist/load/threshold.js +197 -0
- package/dist/load/threshold.js.map +1 -0
- package/dist/load/timeline.d.ts +36 -0
- package/dist/load/timeline.d.ts.map +1 -0
- package/dist/load/timeline.js +158 -0
- package/dist/load/timeline.js.map +1 -0
- package/dist/load-harness.d.ts +2 -0
- package/dist/load-harness.d.ts.map +1 -0
- package/dist/load-harness.js +105 -0
- package/dist/load-harness.js.map +1 -0
- package/dist/runner-resolve.d.ts +53 -0
- package/dist/runner-resolve.d.ts.map +1 -0
- package/dist/runner-resolve.js +264 -0
- package/dist/runner-resolve.js.map +1 -0
- package/dist/workflow/event-timeline.d.ts +3 -0
- package/dist/workflow/event-timeline.d.ts.map +1 -0
- package/dist/workflow/event-timeline.js +72 -0
- package/dist/workflow/event-timeline.js.map +1 -0
- 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":"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"}
|
|
@@ -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
|