Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b50613f
feat(run-store): accept a caller-supplied execution-snapshot id
d-cs Aug 24, 2026
db8390c
feat(run-store): add a generated pass-through RunStore base for decor…
d-cs Aug 24, 2026
e8ac9b3
feat(run-store): build snapshot entries from write-site inputs, with …
d-cs Aug 24, 2026
95dd2a6
feat(run-store): add a snapshotWrites flag that omits every snapshot …
d-cs Aug 24, 2026
6cb49c8
feat(run-store): dual-write execution snapshots in a crash-safe order
d-cs Aug 24, 2026
fbe91ac
test(run-store): cover the snapshot staging facade and the wrapped st…
d-cs Aug 24, 2026
ac7b37b
feat(run-store): read a snapshot window by createdAt cursor
d-cs Aug 24, 2026
f73d806
feat(run-store): serve snapshot reads from Redis with a Postgres fall…
d-cs Aug 24, 2026
910039b
feat(run-store): reap orphaned snapshot keyspaces under both sweep rules
d-cs Aug 24, 2026
8119466
test(run-engine): run the snapshot flows against the decorator with r…
d-cs Aug 24, 2026
aee3f07
fix(run-store): give a snapshot one identity and one instant across b…
d-cs Aug 24, 2026
ea0e17b
chore(run-store): treat the run-store scripts directory as an entry p…
d-cs Aug 24, 2026
1d4eb7b
fix(run-store): address review on the sweep, and make the timestamp p…
d-cs Aug 24, 2026
94b9c68
test(run-store): prove interface parity in the compiler, not in a fake
d-cs Aug 25, 2026
f01d299
fix(run-store): restore a name dropped from the generated list
d-cs Aug 25, 2026
4f7162c
Merge remote-tracking branch 'origin/main' into feat/snapshot-store-d…
d-cs Aug 25, 2026
b02b426
fix(run-store): keep the fork guard on a staged append, and survive a…
d-cs Aug 25, 2026
7bba6a8
chore(run-store): drop the pass-through generator, keep its output as…
d-cs Aug 25, 2026
a553562
test(run-engine): guard the sweeper's copy of the terminal-status list
d-cs Aug 25, 2026
0f6c6d1
refactor(run-store): type every forwarder on the pass-through base
d-cs Aug 25, 2026
e03a185
fix(run-store): forward the parameters two members were dropping
d-cs Aug 25, 2026
e9ce909
fix(run-store): keep completed waitpoints that have no batch index
d-cs Aug 25, 2026
57018fc
fix(run-store): three more index-less waitpoint losses on the read path
d-cs Aug 25, 2026
cff7fb9
fix(run-store): hydrate the column the read path was omitting
d-cs Aug 25, 2026
bde40b3
fix(run-store): four defects from an adversarial sweep of the read path
d-cs Aug 25, 2026
c4469f7
fix(run-store): a refused wait cycle no longer leaves an unreadable head
d-cs Aug 25, 2026
b964f86
fix(run-store): ignore the read cohort at the last dial position
d-cs Aug 25, 2026
a229d99
Merge remote-tracking branch 'origin/main' into feat/snapshot-store-d…
d-cs Aug 25, 2026
ba91f75
fix(run-store): make the snapshot sweep and store correct against a R…
d-cs Aug 25, 2026
aa71fd7
Merge remote-tracking branch 'origin/main' into feat/snapshot-store-d…
d-cs Aug 25, 2026
f7292ac
fix(run-store): keep an omitted completedWaitpointOrder omitted on th…
d-cs Aug 26, 2026
2267b18
Merge remote-tracking branch 'origin/main' into feat/snapshot-store-d…
d-cs Aug 26, 2026
07c3398
chore(run-store): apply oxfmt to the two new sweeper test files
d-cs Aug 26, 2026
1bba0a0
docs(run-store): correct two stale comments on the orphan-marker path
d-cs Aug 26, 2026
6e976d0
feat(run-store): drop the unimplemented compare dial position, cover …
d-cs Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions internal-packages/redis/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { Redis, type RedisOptions } from "ioredis";
import { type Cluster, Redis, type RedisOptions } from "ioredis";
import { Logger } from "@trigger.dev/core/logger";

export { Redis, type Callback, type RedisOptions, type Result, type RedisCommander } from "ioredis";
export {
Redis,
Cluster,
type Callback,
type RedisOptions,
type ClusterNode,
type ClusterOptions,
type Result,
type RedisCommander,
} from "ioredis";

/**
* Either endpoint shape. A component that only issues key-addressed commands works against both, so
* it should accept this rather than pin itself to a standalone connection. Commands with no key —
* SCAN above all — do NOT fan out across a cluster, so anything that issues one must iterate
* `cluster.nodes("master")` itself.
*/
export type RedisClient = Redis | Cluster;

/**
* Reply-error -> reconnect mapping. Without this hook, an ElastiCache
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// The snapshot sweeper needs to know which run statuses are terminal, and it cannot import that
// list: run-engine depends on run-store, not the other way round. So the list is duplicated, and
// this is the only thing that keeps the copy honest.
//
// Without it, a status added here and not there makes the sweeper treat a finished run as live and
// never apply its completion expiry. A status removed here and not there makes it treat a live run
// as finished. The second one reaps state a run is still using.
import { describe, expect, it } from "vitest";
import { FINAL_RUN_STATUSES } from "@internal/run-store";
import { getFinalRunStatuses } from "../statuses.js";

describe("terminal run statuses", () => {
it("match between the engine and the snapshot sweeper", () => {
expect([...FINAL_RUN_STATUSES].sort()).toEqual([...getFinalRunStatuses()].sort());
});

it("are not empty, so the comparison cannot pass vacuously", () => {
expect(FINAL_RUN_STATUSES.length).toBeGreaterThan(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Builds the snapshot-store decorator over a real PostgresRunStore, for injection through the
// engine's `store` option — the seam runStoreInjectability.test.ts already proves.
//
// The point of injecting it is that the engine suites keep their own assertions: the same flows,
// the same expectations, a different store underneath.
import {
PostgresRunStore,
RedisSnapshotStore,
TaskRunExecutionSnapshotStore,
type SnapshotFaultInjector,
type SnapshotRepairEnqueuer,
type SnapshotStoreMode,
} from "@internal/run-store";
import type { PrismaClient } from "@trigger.dev/database";
import type { RedisOptions } from "@internal/redis";

const COMPLETED_TTL_MS = 72 * 60 * 60 * 1000;

export type DecoratedStoreHarness = {
store: TaskRunExecutionSnapshotStore;
redis: RedisSnapshotStore;
/** Every read the decorator served, and which store answered it. */
reads: { method: string; source: "redis" | "postgres" }[];
/** Every append outcome, keyed by the write site that produced it. */
writes: { site: string; outcome: string }[];
/** Runs handed to the repair job because their append was lost. */
repairs: { runId: string; snapshotId: string; executionStatus: string }[];
quit(): Promise<void>;
};

export function buildDecoratedStore(opts: {
prisma: PrismaClient;
redisOptions: RedisOptions;
mode: SnapshotStoreMode;
readPercent?: number;
faults?: SnapshotFaultInjector;
onAppendFailure?: SnapshotRepairEnqueuer;
}): DecoratedStoreHarness {
const redis = new RedisSnapshotStore({
redisOptions: opts.redisOptions,
completedTtlMs: COMPLETED_TTL_MS,
});

const reads: DecoratedStoreHarness["reads"] = [];
const writes: DecoratedStoreHarness["writes"] = [];
const repairs: DecoratedStoreHarness["repairs"] = [];

const store = new TaskRunExecutionSnapshotStore(
new PostgresRunStore({ prisma: opts.prisma as never, readOnlyPrisma: opts.prisma as never }),
{
store: redis,
mode: opts.mode,
readPercent: opts.readPercent ?? 100,
...(opts.faults && { faults: opts.faults }),
onAppendFailure: async (args) => {
repairs.push(args);
await opts.onAppendFailure?.(args);
},
metrics: {
recordWrite: (site, outcome) => writes.push({ site, outcome }),
recordAppendFailed: () => {},
recordRead: (method, source) => reads.push({ method, source }),
},
}
);

return {
store,
redis,
reads,
writes,
repairs,
quit: () => redis.quit(),
};
}
Loading