diff --git a/internal-packages/run-store/src/redisSnapshotStore.test.ts b/internal-packages/run-store/src/redisSnapshotStore.test.ts index 8b738e6b1..39004b81a 100644 --- a/internal-packages/run-store/src/redisSnapshotStore.test.ts +++ b/internal-packages/run-store/src/redisSnapshotStore.test.ts @@ -1254,6 +1254,53 @@ describe("hash tag and keyPrefix", () => { }); describe("observability", () => { + redisTest("cycle-key bytes cover records, not just order", async ({ redisOptions }) => { + // The plan puts a metric on the wp: KEY size. records dominates that key once + // populated, so measuring order alone understates it by orders of magnitude. + const calls: Array<[string, number]> = []; + const metrics = { + recordAppend: () => {}, + recordEntryBytes: () => {}, + recordCycleKeyBytes: (b: number) => calls.push(["cycleBytes", b]), + recordCycleCount: () => {}, + recordSkippedNoKeyspace: () => {}, + recordCycleMismatch: () => {}, + recordLatency: () => {}, + }; + const store = new RedisSnapshotStore({ redisOptions, completedTtlMs: 60_000, metrics }); + try { + const records: CompletedWaitpointRecord[] = [ + { + id: "w_a", + friendlyId: "waitpoint_a", + type: "MANUAL", + completedAt: "2026-01-01T00:00:00.000Z", + outputType: "application/json", + outputIsError: false, + output: { inline: "y".repeat(20_000) }, + }, + ]; + const orderJson = JSON.stringify(["w_a"]); + const recordsJson = JSON.stringify(records); + + await store.append({ + entry: entry({ id: "snap_1" }), + kind: "birth", + isTerminal: false, + cycle: { kind: "new", completedWaitpoints: [{ id: "w_a", index: 0 }], records }, + }); + + expect(calls).toEqual([ + [ + "cycleBytes", + Buffer.byteLength(orderJson, "utf8") + Buffer.byteLength(recordsJson, "utf8"), + ], + ]); + } finally { + await store.quit(); + } + }); + redisTest("records sizes and outcomes without ever rejecting", async ({ redisOptions }) => { const calls: unknown[][] = []; const metrics = { diff --git a/internal-packages/run-store/src/redisSnapshotStore.ts b/internal-packages/run-store/src/redisSnapshotStore.ts index f82355a32..10175988f 100644 --- a/internal-packages/run-store/src/redisSnapshotStore.ts +++ b/internal-packages/run-store/src/redisSnapshotStore.ts @@ -301,11 +301,17 @@ export class RedisSnapshotStore { args.expectedCur !== undefined ? "1" : "0" )) as string[]; - return this.#interpretAppend(reply, raw, orderJson, args.entry.runId); + return this.#interpretAppend(reply, raw, orderJson, records, args.entry.runId); }); } - #interpretAppend(reply: string[], raw: string, orderJson: string, runId: string): AppendResult { + #interpretAppend( + reply: string[], + raw: string, + orderJson: string, + records: string, + runId: string + ): AppendResult { if (reply[0] === SKIPPED) { this.metrics?.recordSkippedNoKeyspace(); this.metrics?.recordAppend("skippedNoKeyspace", "none"); @@ -326,7 +332,7 @@ export class RedisSnapshotStore { if (cycleMismatch) { this.metrics?.recordCycleMismatch(); } - this.#observeSizes(raw, orderJson, cycleSeq, runId); + this.#observeSizes(raw, orderJson, records, cycleSeq, runId); this.metrics?.recordAppend("written", ttl); return { outcome: "written", @@ -337,14 +343,21 @@ export class RedisSnapshotStore { }; } - #observeSizes(raw: string, orderJson: string, cycleSeq: number, runId: string): void { + #observeSizes( + raw: string, + orderJson: string, + records: string, + cycleSeq: number, + runId: string + ): void { const entryBytes = Buffer.byteLength(raw, "utf8"); this.metrics?.recordEntryBytes(entryBytes); if (this.highWater.entryBytes !== undefined && entryBytes > this.highWater.entryBytes) { this.logger.warn("RedisSnapshotStore entry above high-water mark", { runId, entryBytes }); } if (orderJson !== "") { - const cycleBytes = Buffer.byteLength(orderJson, "utf8"); + // The whole wp: key, not just its order field: records dominates it once populated. + const cycleBytes = Buffer.byteLength(orderJson, "utf8") + Buffer.byteLength(records, "utf8"); this.metrics?.recordCycleKeyBytes(cycleBytes); if (this.highWater.cycleKeyBytes !== undefined && cycleBytes > this.highWater.cycleKeyBytes) { this.logger.warn("RedisSnapshotStore cycle key above high-water mark", {