perf: add inline-persist fast path for WAC v2 step() (#8807)

This commit is contained in:
Ruben Fiszel
2026-04-13 16:49:53 +00:00
committed by GitHub
parent 3f5841f84d
commit b3ef4bc26c
15 changed files with 762 additions and 385 deletions
+76
View File
@@ -1494,6 +1494,16 @@ export class WorkflowCtx {
private _suspended = false;
/** When set, the task matching this key executes its inner function directly */
_executingKey: string | null;
/** Serializes fast-path POSTs across concurrent step() calls within one
* workflow invocation. Wraps only the HTTP call, not fn() — so
* `Promise.all([step("a", fn_a), step("b", fn_b)])` still runs the two
* fn() bodies in parallel, only the API requests are ordered. This
* closes the first-write race window against `SELECT FOR UPDATE` on a
* not-yet-created `v2_job_status` row: two concurrent POSTs would both
* see None and overwrite each other's checkpoint because the helper
* writes the whole serialized `_checkpoint` object, not a single
* `completed_steps[key]`. Initialized to a resolved promise. */
private _inlineChain: Promise<void> = Promise.resolve();
constructor(checkpoint: Record<string, any> = {}) {
this.completed = checkpoint?.completed_steps ?? {};
@@ -1652,6 +1662,72 @@ export class WorkflowCtx {
const t0 = Date.now();
const result = await fn();
const durationMs = Date.now() - t0;
// Fast path: POST the delta to the new per-job API endpoint and return the
// result directly so the workflow subprocess continues into the next step()
// without unwinding. Concurrent step() calls (e.g. inside Promise.all) run
// their fn() bodies in parallel, then serialize the API POSTs via a
// per-ctx promise chain (`this._inlineChain`). Serializing the POSTs is
// required because the backend helper writes the whole serialized
// `_checkpoint` object per call, and two concurrent writes against a
// not-yet-created `v2_job_status` row would both see None under
// `SELECT FOR UPDATE` and overwrite each other.
//
// On any failure — network, auth, timeout, source-hash mismatch, old
// backend without the endpoint — fall through to throwing StepSuspend so
// the worker takes the legacy suspend-and-replay path. Gated by
// WM_WAC_INLINE_FAST_PATH (default on) so the old behavior stays
// reachable for A/B testing and rollback.
const fastPathFlagRaw = (getEnv("WM_WAC_INLINE_FAST_PATH") ?? "1").trim().toLowerCase();
const fastPathEnabled =
fastPathFlagRaw !== "0" &&
fastPathFlagRaw !== "false" &&
fastPathFlagRaw !== "off" &&
fastPathFlagRaw !== "no";
const jobId = getEnv("WM_JOB_ID");
const workspace = getEnv("WM_WORKSPACE");
if (fastPathEnabled && jobId && workspace && OpenAPI.BASE && OpenAPI.TOKEN) {
const chainTail = this._inlineChain.then(async () => {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 10_000);
try {
const resp = await fetch(
`${OpenAPI.BASE}/w/${workspace}/jobs/wac/inline_checkpoint/${jobId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${OpenAPI.TOKEN}`,
},
body: JSON.stringify({
key,
result,
started_at: startedAt,
duration_ms: durationMs,
}),
signal: ctrl.signal,
},
);
if (!resp.ok) {
throw new Error(`inline_checkpoint API ${resp.status}`);
}
} finally {
clearTimeout(t);
}
});
// Swallow chain errors so a past failure does not poison future awaits.
this._inlineChain = chainTail.catch(() => {});
try {
await chainTail;
return result as T;
} catch (e) {
console.log(
`WAC v2 inline fast path failed for key ${key}, falling back to suspend: ${e}`,
);
// fall through to the legacy suspend path below
}
}
throw new StepSuspend({ mode: "inline_checkpoint", steps: [], key, result, started_at: startedAt, duration_ms: durationMs });
}
}