Skip to content

Commit 3653811

Browse files
committed
refactor(run-store): name the records-read gate and unit test it
The gate was an inline predicate inside a private method, so the cost decision it drives had only integration coverage. Extracts it as mayHoldRecords beside deriveOrder and deriveDistinctIds, its sibling pure helpers, and covers it with unit tests: empty, all-legacy, one store id, a store id among legacy ones, index-less, all four waitpoint types, and a foreign prefix. The mixed case is the one a per-organisation rollout produces, where a run holds waitpoints minted either side of the flip. One store-format id is enough to require the read.
1 parent 132b4d9 commit 3653811

3 files changed

Lines changed: 66 additions & 16 deletions

File tree

internal-packages/run-store/src/redisSnapshotStore.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { expect, describe, vi } from "vitest";
44
import { redisTest, slotOf } from "@internal/testcontainers";
55
import { createRedisClient } from "@internal/redis";
66
import { Logger } from "@trigger.dev/core/logger";
7+
import { generateWaitpointId } from "@trigger.dev/core/v3/isomorphic";
78
import {
9+
mayHoldRecords,
810
snapshotKeys,
911
deriveOrder,
1012
isValidFor,
@@ -24,6 +26,46 @@ describe("snapshotKeys", () => {
2426
});
2527
});
2628

29+
describe("mayHoldRecords", () => {
30+
it("is false for an empty set", () => {
31+
expect(mayHoldRecords([])).toBe(false);
32+
});
33+
34+
it("is false when every id is legacy", () => {
35+
expect(mayHoldRecords([{ id: "w_cuid_a", index: 0 }, { id: "w_cuid_b" }])).toBe(false);
36+
});
37+
38+
it("is true when one id is store-format", () => {
39+
expect(mayHoldRecords([{ id: generateWaitpointId("MANUAL"), index: 0 }])).toBe(true);
40+
});
41+
42+
// The mixed case is the one a per-organisation rollout produces: a run holding waitpoints
43+
// minted either side of the flip. One store-format id is enough to require the read.
44+
it("is true when a store-format id sits among legacy ones", () => {
45+
expect(
46+
mayHoldRecords([
47+
{ id: "w_cuid_a", index: 0 },
48+
{ id: generateWaitpointId("MANUAL"), index: 1 },
49+
{ id: "w_cuid_c" },
50+
])
51+
).toBe(true);
52+
});
53+
54+
it("reads the id, not the index, so an index-less store id still counts", () => {
55+
expect(mayHoldRecords([{ id: generateWaitpointId("DATETIME") }])).toBe(true);
56+
});
57+
58+
it("counts every store waitpoint type", () => {
59+
for (const t of ["RUN", "BATCH", "DATETIME", "MANUAL"] as const) {
60+
expect(mayHoldRecords([{ id: generateWaitpointId(t) }])).toBe(true);
61+
}
62+
});
63+
64+
it("is false for a foreign prefix that is not a waitpoint id", () => {
65+
expect(mayHoldRecords([{ id: "run_0123456789abcdefghijklm" }])).toBe(false);
66+
});
67+
});
68+
2769
describe("deriveOrder", () => {
2870
it("drops entries with no index, sorts by index, and maps to id", () => {
2971
expect(

internal-packages/run-store/src/redisSnapshotStore.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type Result,
77
} from "@internal/redis";
88
import { Logger } from "@trigger.dev/core/logger";
9+
import { parseWaitpointId } from "@trigger.dev/core/v3/isomorphic";
910
import type { CompletedWaitpoint } from "@trigger.dev/core/v3/schemas";
1011

1112
export type SnapshotKeys = { e: string; idx: string; cur: string; seq: string };
@@ -48,6 +49,22 @@ export function deriveDistinctIds(completedWaitpoints: CompletedWaitpointRef[]):
4849
return [...new Set(completedWaitpoints.map((w) => w.id))];
4950
}
5051

52+
/**
53+
* Whether a cycle carrying these ids could hold a record set.
54+
*
55+
* A record set is written for store-format waitpoint ids and nothing else, so a cycle whose ids
56+
* are all legacy has none, and a read for one could only ever return nothing. Callers use this to
57+
* skip that read, which keeps a deployment holding no store-format waitpoint at zero extra round
58+
* trips on the resume path.
59+
*
60+
* This is the one place this module looks INSIDE a waitpoint id rather than treating the record
61+
* set as opaque. It is named and exported rather than inlined so the cost decision it drives is
62+
* testable on its own.
63+
*/
64+
export function mayHoldRecords(completedWaitpoints: CompletedWaitpointRef[]): boolean {
65+
return completedWaitpoints.some((w) => parseWaitpointId(w.id).format === "b32hexW");
66+
}
67+
5168
// isValid is derived, never stored, so the entry JSON stays byte-identical to the caller's document.
5269
export function isValidFor(entry: { error?: unknown }): boolean {
5370
return !entry.error;

internal-packages/run-store/src/taskRunExecutionSnapshotStore.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// Each order is chosen so the crash state is the harmless one. A lost cross-store write is never
1313
// recovered by a transaction or an outbox: recovery is always the existing stall-and-repair job.
1414
import { Logger } from "@trigger.dev/core/logger";
15-
import { generateInternalId, parseWaitpointId } from "@trigger.dev/core/v3/isomorphic";
15+
import { generateInternalId } from "@trigger.dev/core/v3/isomorphic";
1616
import { DelegatingRunStore } from "./delegatingRunStore.js";
1717
import type {
1818
CompletedWaitpointRecord,
@@ -21,7 +21,7 @@ import type {
2121
SnapshotEntryInput,
2222
SnapshotRead,
2323
} from "./redisSnapshotStore.js";
24-
import { deriveDistinctIds, deriveOrder } from "./redisSnapshotStore.js";
24+
import { deriveDistinctIds, deriveOrder, mayHoldRecords } from "./redisSnapshotStore.js";
2525
import {
2626
entryFromCompletion,
2727
entryFromCreateExecutionSnapshot,
@@ -630,22 +630,13 @@ export class TaskRunExecutionSnapshotStore extends DelegatingRunStore {
630630
// cycle already minted. But the script may refuse the pointer and mint a replacement
631631
// from these refs, and a replacement minted with no records holds ids that nothing
632632
// resolves. So carry the surviving cycle's records for that branch.
633-
// Read only when a record could exist. A record set is written for store-format waitpoint
634-
// ids and nothing else, so a cycle whose ids are all legacy has none, and the read could
635-
// only ever return nothing. Gating on the id keeps a deployment with no store-format
636-
// waitpoint at zero extra round trips, and confines the cost to the organisations that
637-
// actually hold them.
638-
//
639-
// This is the one place the snapshot store looks INSIDE a waitpoint id rather than
640-
// treating the record set as opaque. It buys a per-append round trip on the resume path,
641-
// which is worth the narrower layering.
642-
const mayHaveRecords = completedWaitpoints.some(
643-
(w) => parseWaitpointId(w.id).format === "b32hexW"
644-
);
645-
633+
// Read only when a record could exist: see mayHoldRecords. A legacy-only cycle has none,
634+
// so the read would return nothing and the round trip is pure cost.
646635
const carried =
647636
records ??
648-
(mayHaveRecords ? await this.#recordsForCycle(runId, head.cycle.cycleSeq) : undefined);
637+
(mayHoldRecords(completedWaitpoints)
638+
? await this.#recordsForCycle(runId, head.cycle.cycleSeq)
639+
: undefined);
649640

650641
return {
651642
kind: "carryForward",

0 commit comments

Comments
 (0)