chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import type { MollifierBuffer } from "@trigger.dev/redis-worker";
|
||||
import { RunId } from "@trigger.dev/core/v3/isomorphic";
|
||||
import { IdempotencyKeyOptionsSchema } from "@trigger.dev/core/v3/schemas";
|
||||
import type { z } from "zod";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { deserialiseMollifierSnapshot } from "./mollifierSnapshot.server";
|
||||
import { getMollifierBuffer } from "./mollifierBuffer.server";
|
||||
|
||||
export type ReadFallbackInput = {
|
||||
runId: string;
|
||||
environmentId: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export type SyntheticRun = {
|
||||
// Snapshot-derived TaskRun primary key. Used by ReplayTaskRunService
|
||||
// for logging and by callers passing this object where a TaskRun is
|
||||
// expected (cast). Derived deterministically from `friendlyId`.
|
||||
id: string;
|
||||
friendlyId: string;
|
||||
status: "QUEUED" | "FAILED" | "CANCELED";
|
||||
// Set when the customer cancelled the run via the dashboard or API
|
||||
// while it was buffered. The drainer's cancel bifurcation reads this
|
||||
// on next pop and writes a CANCELED PG row directly (skipping
|
||||
// materialisation). Reflected back into the UI by the synthesised
|
||||
// SpanRun so the run-detail page shows the cancelled state even before
|
||||
// the drainer materialises it.
|
||||
cancelledAt: Date | undefined;
|
||||
cancelReason: string | undefined;
|
||||
// Reschedule patch (`set_delay`) writes `delayUntil` into the snapshot.
|
||||
// Surfacing it on SyntheticRun lets the retrieve-run shape reflect the
|
||||
// pending delay before the drainer materialises the PG row.
|
||||
delayUntil: Date | undefined;
|
||||
taskIdentifier: string | undefined;
|
||||
createdAt: Date;
|
||||
|
||||
payload: unknown;
|
||||
payloadType: string | undefined;
|
||||
metadata: unknown;
|
||||
metadataType: string | undefined;
|
||||
// Seed-metadata mirrors what `triggerTask.server.ts` writes into the
|
||||
// snapshot: the original metadataPacket data preserved separately from
|
||||
// any later customer mutations. ReplayTaskRunService uses these to
|
||||
// rebuild the replay's metadata.
|
||||
seedMetadata: string | undefined;
|
||||
seedMetadataType: string | undefined;
|
||||
|
||||
idempotencyKey: string | undefined;
|
||||
// Surfaced for the cached-hit expiration check in IdempotencyKeyConcern.
|
||||
// The PG-resident path enforces this (clears key, allows new run when
|
||||
// expired). For buffered runs the snapshot carries the same field — we
|
||||
// expose it here so the cached-hit branch can apply the same check
|
||||
// rather than indefinitely returning the buffered run's id.
|
||||
idempotencyKeyExpiresAt: Date | undefined;
|
||||
// `{ key, scope }` object form, matching how the SDK serialises and PG
|
||||
// stores it. Previously typed as `string[]` (legacy/incorrect — Prisma
|
||||
// is `Json?` carrying the schema-shaped object). `getUserProvidedIdempotencyKey`
|
||||
// and `extractIdempotencyKeyScope` both parse via the same Zod schema;
|
||||
// they returned `undefined` for the array-shape, which silently
|
||||
// demoted the response to surface the hash instead of the user-
|
||||
// provided key for buffered runs — a contract divergence from
|
||||
// PG-resident runs. See the regression test in `mollifierReadFallback.test.ts`.
|
||||
idempotencyKeyOptions: z.infer<typeof IdempotencyKeyOptionsSchema> | undefined;
|
||||
isTest: boolean;
|
||||
depth: number;
|
||||
ttl: string | undefined;
|
||||
tags: string[];
|
||||
// Mirror of `tags` under the PG field name. ReplayTaskRunService reads
|
||||
// `existingTaskRun.runTags`; both names are kept here so a synthetic
|
||||
// run can be passed wherever the PG-shape `runTags` is expected.
|
||||
runTags: string[];
|
||||
lockedToVersion: string | undefined;
|
||||
resumeParentOnCompletion: boolean;
|
||||
parentTaskRunId: string | undefined;
|
||||
|
||||
// Allocated at gate-accept time and embedded in the snapshot so the run's
|
||||
// trace is continuous from QUEUED-in-buffer through executing post-drain.
|
||||
traceId: string | undefined;
|
||||
spanId: string | undefined;
|
||||
parentSpanId: string | undefined;
|
||||
|
||||
// Replay-relevant fields populated from the engine-trigger snapshot.
|
||||
// ReplayTaskRunService reads each of these from the existing TaskRun;
|
||||
// when the original lives in the buffer we synthesise them here.
|
||||
runtimeEnvironmentId: string | undefined;
|
||||
engine: "V2";
|
||||
workerQueue: string | undefined;
|
||||
region: string | undefined;
|
||||
queue: string | undefined;
|
||||
concurrencyKey: string | undefined;
|
||||
machinePreset: string | undefined;
|
||||
realtimeStreamsVersion: string | undefined;
|
||||
|
||||
// Additional snapshot-sourced fields used when synthesising a SpanRun
|
||||
// for the dashboard's right-side details panel. All optional because
|
||||
// older snapshots may not carry them.
|
||||
maxAttempts: number | undefined;
|
||||
maxDurationInSeconds: number | undefined;
|
||||
replayedFromTaskRunFriendlyId: string | undefined;
|
||||
annotations: unknown;
|
||||
traceContext: unknown;
|
||||
scheduleId: string | undefined;
|
||||
batchId: string | undefined;
|
||||
parentTaskRunFriendlyId: string | undefined;
|
||||
rootTaskRunFriendlyId: string | undefined;
|
||||
|
||||
error?: { code: string; message: string };
|
||||
};
|
||||
|
||||
export type ReadFallbackDeps = {
|
||||
getBuffer?: () => MollifierBuffer | null;
|
||||
};
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function asStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) && value.every((v) => typeof v === "string")
|
||||
? (value as string[])
|
||||
: [];
|
||||
}
|
||||
|
||||
function asDate(value: unknown): Date | undefined {
|
||||
const raw = asString(value);
|
||||
if (!raw) return undefined;
|
||||
const parsed = new Date(raw);
|
||||
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
||||
}
|
||||
|
||||
// Snapshot ids are written by engine.trigger as INTERNAL ids (cuids); the
|
||||
// SyntheticRun contract exposes friendlyIds. `RunId.toFriendlyId` is
|
||||
// already used for the synthetic run's own id (line 155); reuse it for
|
||||
// parent/root so consumers see the same shape as the PG path.
|
||||
function internalRunIdToFriendlyId(internalId: string | undefined): string | undefined {
|
||||
if (!internalId) return undefined;
|
||||
return RunId.toFriendlyId(internalId);
|
||||
}
|
||||
|
||||
export async function findRunByIdWithMollifierFallback(
|
||||
input: ReadFallbackInput,
|
||||
deps: ReadFallbackDeps = {}
|
||||
): Promise<SyntheticRun | null> {
|
||||
const buffer = (deps.getBuffer ?? getMollifierBuffer)();
|
||||
if (!buffer) return null;
|
||||
|
||||
try {
|
||||
const entry = await buffer.getEntry(input.runId);
|
||||
if (!entry) return null;
|
||||
|
||||
if (entry.envId !== input.environmentId || entry.orgId !== input.organizationId) {
|
||||
logger.warn("mollifier read-fallback auth mismatch", {
|
||||
runId: input.runId,
|
||||
callerEnvId: input.environmentId,
|
||||
callerOrgId: input.organizationId,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = deserialiseMollifierSnapshot(entry.payload);
|
||||
// Parse via the canonical schema (`{ key: string, scope: "run" |
|
||||
// "attempt" | "global" }`) rather than the legacy Array.isArray
|
||||
// check. The SDK and Prisma both store this as an object; the array
|
||||
// form never matches, so a buffered run's response previously fell
|
||||
// back to the server-side hash in `getUserProvidedIdempotencyKey`
|
||||
// instead of the customer-supplied key — diverging from how
|
||||
// materialised runs render the same field.
|
||||
const idempotencyKeyOptionsParsed = IdempotencyKeyOptionsSchema.safeParse(
|
||||
snapshot.idempotencyKeyOptions
|
||||
);
|
||||
const idempotencyKeyOptions = idempotencyKeyOptionsParsed.success
|
||||
? idempotencyKeyOptionsParsed.data
|
||||
: undefined;
|
||||
|
||||
const tags = asStringArray(snapshot.tags);
|
||||
const environment =
|
||||
snapshot.environment && typeof snapshot.environment === "object"
|
||||
? (snapshot.environment as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
const cancelledAt = asDate(snapshot.cancelledAt);
|
||||
const cancelReason = asString(snapshot.cancelReason);
|
||||
let status: SyntheticRun["status"] = "QUEUED";
|
||||
if (cancelledAt) {
|
||||
status = "CANCELED";
|
||||
} else if (entry.status === "FAILED") {
|
||||
status = "FAILED";
|
||||
}
|
||||
const delayUntil = asDate(snapshot.delayUntil);
|
||||
|
||||
return {
|
||||
id: RunId.fromFriendlyId(entry.runId),
|
||||
friendlyId: entry.runId,
|
||||
status,
|
||||
cancelledAt,
|
||||
cancelReason,
|
||||
delayUntil,
|
||||
taskIdentifier: asString(snapshot.taskIdentifier),
|
||||
createdAt: entry.createdAt,
|
||||
|
||||
payload: snapshot.payload,
|
||||
payloadType: asString(snapshot.payloadType),
|
||||
metadata: snapshot.metadata,
|
||||
metadataType: asString(snapshot.metadataType),
|
||||
seedMetadata: asString(snapshot.seedMetadata),
|
||||
seedMetadataType: asString(snapshot.seedMetadataType),
|
||||
|
||||
idempotencyKey: asString(snapshot.idempotencyKey),
|
||||
idempotencyKeyExpiresAt: asDate(snapshot.idempotencyKeyExpiresAt),
|
||||
idempotencyKeyOptions,
|
||||
isTest: snapshot.isTest === true,
|
||||
depth: typeof snapshot.depth === "number" ? snapshot.depth : 0,
|
||||
ttl: asString(snapshot.ttl),
|
||||
tags,
|
||||
runTags: tags,
|
||||
lockedToVersion: asString(snapshot.taskVersion),
|
||||
resumeParentOnCompletion: snapshot.resumeParentOnCompletion === true,
|
||||
parentTaskRunId: asString(snapshot.parentTaskRunId),
|
||||
|
||||
traceId: asString(snapshot.traceId),
|
||||
spanId: asString(snapshot.spanId),
|
||||
parentSpanId: asString(snapshot.parentSpanId),
|
||||
|
||||
runtimeEnvironmentId: asString(environment?.id) ?? entry.envId,
|
||||
engine: "V2",
|
||||
workerQueue: asString(snapshot.workerQueue),
|
||||
region: asString(snapshot.region),
|
||||
queue: asString(snapshot.queue),
|
||||
concurrencyKey: asString(snapshot.concurrencyKey),
|
||||
machinePreset: asString(snapshot.machine),
|
||||
realtimeStreamsVersion: asString(snapshot.realtimeStreamsVersion),
|
||||
|
||||
maxAttempts: typeof snapshot.maxAttempts === "number" ? snapshot.maxAttempts : undefined,
|
||||
maxDurationInSeconds:
|
||||
typeof snapshot.maxDurationInSeconds === "number"
|
||||
? snapshot.maxDurationInSeconds
|
||||
: undefined,
|
||||
replayedFromTaskRunFriendlyId: asString(snapshot.replayedFromTaskRunFriendlyId),
|
||||
annotations: snapshot.annotations,
|
||||
traceContext: snapshot.traceContext,
|
||||
scheduleId: asString(snapshot.scheduleId),
|
||||
// The engine.trigger input embeds the batch as `{ id, index }` (see
|
||||
// triggerTask.server.ts #buildEngineTriggerInput), not as a flat
|
||||
// `batchId`. The nested `id` is the batch's internal cuid — the same
|
||||
// value PG stores in `TaskRun.batchId` — so callers reconstruct the
|
||||
// friendly id via `BatchId.toFriendlyId` exactly as the PG path does.
|
||||
batchId: asString((snapshot.batch as { id?: unknown } | undefined)?.id),
|
||||
// The snapshot only carries the INTERNAL parent/root ids
|
||||
// (`parentTaskRunId` / `rootTaskRunId` — what engine.trigger consumes),
|
||||
// not the friendlyIds the SyntheticRun contract expects. Convert
|
||||
// internal → friendly here so consumers don't have to special-case
|
||||
// the buffered path.
|
||||
parentTaskRunFriendlyId: internalRunIdToFriendlyId(asString(snapshot.parentTaskRunId)),
|
||||
rootTaskRunFriendlyId: internalRunIdToFriendlyId(asString(snapshot.rootTaskRunId)),
|
||||
|
||||
error: entry.lastError,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error("mollifier read-fallback errored — fail-open to null", {
|
||||
runId: input.runId,
|
||||
err: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user