chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,753 @@
|
||||
import type {
|
||||
AttemptStatus,
|
||||
RunStatus,
|
||||
SerializedError,
|
||||
TriggerFunction,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import {
|
||||
TaskRunError,
|
||||
conditionallyImportPacket,
|
||||
createJsonErrorObject,
|
||||
logger,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { parsePacketAsJson } from "@trigger.dev/core/v3/utils/ioSerialization";
|
||||
import { BatchId } from "@trigger.dev/core/v3/isomorphic";
|
||||
import { getUserProvidedIdempotencyKey } from "@trigger.dev/core/v3/serverOnly";
|
||||
import type { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import type { API_VERSIONS, RunStatusUnspecifiedApiVersion } from "~/api/versions";
|
||||
import { CURRENT_API_VERSION } from "~/api/versions";
|
||||
import { $replica, prisma } from "~/db.server";
|
||||
import { regionForDisplay } from "~/runEngine/concerns/workerQueueSplit.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import {
|
||||
findRunByIdWithMollifierFallback,
|
||||
type SyntheticRun,
|
||||
} from "~/v3/mollifier/readFallback.server";
|
||||
import { generatePresignedUrl } from "~/v3/objectStore.server";
|
||||
import { runStore } from "~/v3/runStore.server";
|
||||
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
||||
import { tracer } from "~/v3/tracer.server";
|
||||
import { startSpanWithEnv } from "~/v3/tracing.server";
|
||||
|
||||
const commonRunSelect = {
|
||||
id: true,
|
||||
friendlyId: true,
|
||||
status: true,
|
||||
taskIdentifier: true,
|
||||
createdAt: true,
|
||||
startedAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
expiredAt: true,
|
||||
delayUntil: true,
|
||||
metadata: true,
|
||||
metadataType: true,
|
||||
ttl: true,
|
||||
costInCents: true,
|
||||
baseCostInCents: true,
|
||||
usageDurationMs: true,
|
||||
idempotencyKey: true,
|
||||
idempotencyKeyOptions: true,
|
||||
isTest: true,
|
||||
depth: true,
|
||||
scheduleId: true,
|
||||
workerQueue: true,
|
||||
region: true,
|
||||
lockedToVersionId: true,
|
||||
resumeParentOnCompletion: true,
|
||||
batch: {
|
||||
select: {
|
||||
id: true,
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
runTags: true,
|
||||
} satisfies Prisma.TaskRunSelect;
|
||||
|
||||
type CommonRelatedRun = Prisma.Result<
|
||||
typeof prisma.taskRun,
|
||||
{ select: typeof commonRunSelect },
|
||||
"findFirstOrThrow"
|
||||
>;
|
||||
|
||||
// commonRunSelect carries only the scalar `lockedToVersionId`; `version` is resolved via the
|
||||
// control-plane resolver and folded back on for the run and each related run, since
|
||||
// `createCommonRunStructure` reads `.lockedToVersion?.version` for all of them.
|
||||
type CommonRelatedRunWithVersion = CommonRelatedRun & {
|
||||
lockedToVersion: { version: string } | null;
|
||||
};
|
||||
|
||||
// Full shape returned by findRun() — the commonRunSelect fields plus the
|
||||
// extras the route handler reads. Declared explicitly (not inferred via
|
||||
// ReturnType<typeof findRun>) so findRun can return a synthesised buffered
|
||||
// run without the type becoming self-referential. Exported so the
|
||||
// buffer-synthesis helper below can match this shape under unit test.
|
||||
export type FoundRun = CommonRelatedRunWithVersion & {
|
||||
traceId: string;
|
||||
payload: string;
|
||||
payloadType: string;
|
||||
output: string | null;
|
||||
outputType: string;
|
||||
error: Prisma.JsonValue;
|
||||
attempts: { id: string }[];
|
||||
attemptNumber: number | null;
|
||||
engine: "V1" | "V2";
|
||||
taskEventStore: string;
|
||||
parentTaskRun: CommonRelatedRunWithVersion | null;
|
||||
rootTaskRun: CommonRelatedRunWithVersion | null;
|
||||
childRuns: CommonRelatedRunWithVersion[];
|
||||
// True when this run was synthesised from the mollifier buffer rather
|
||||
// than read from Postgres. Callers that would otherwise query backing
|
||||
// stores keyed on PG identifiers (e.g. ClickHouse event lookups by
|
||||
// traceId) can short-circuit to an empty response — buffered runs
|
||||
// haven't executed and have no events to fetch.
|
||||
isBuffered: boolean;
|
||||
};
|
||||
|
||||
export class ApiRetrieveRunPresenter {
|
||||
constructor(private readonly apiVersion: API_VERSIONS) {}
|
||||
|
||||
public static async findRun(
|
||||
friendlyId: string,
|
||||
env: AuthenticatedEnvironment
|
||||
): Promise<FoundRun | null> {
|
||||
const pgRow = await runStore.findRun(
|
||||
{
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
{
|
||||
select: {
|
||||
...commonRunSelect,
|
||||
traceId: true,
|
||||
payload: true,
|
||||
payloadType: true,
|
||||
output: true,
|
||||
outputType: true,
|
||||
error: true,
|
||||
attempts: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
attemptNumber: true,
|
||||
engine: true,
|
||||
taskEventStore: true,
|
||||
parentTaskRun: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
rootTaskRun: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
childRuns: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
$replica
|
||||
);
|
||||
|
||||
if (pgRow) {
|
||||
// Dedup distinct locked-version ids so each hits the resolver exactly once
|
||||
// (unbounded childRuns mostly share the same lockedToVersionId).
|
||||
const distinctVersionIds = new Set<string>();
|
||||
const collect = (id: string | null) => {
|
||||
if (id) {
|
||||
distinctVersionIds.add(id);
|
||||
}
|
||||
};
|
||||
collect(pgRow.lockedToVersionId);
|
||||
collect(pgRow.parentTaskRun?.lockedToVersionId ?? null);
|
||||
collect(pgRow.rootTaskRun?.lockedToVersionId ?? null);
|
||||
for (const child of pgRow.childRuns) {
|
||||
collect(child.lockedToVersionId);
|
||||
}
|
||||
|
||||
const versionById = new Map<string, string | null>(
|
||||
await Promise.all(
|
||||
[...distinctVersionIds].map(
|
||||
async (id) =>
|
||||
[
|
||||
id,
|
||||
(await controlPlaneResolver.resolveRunLockedWorker({ lockedToVersionId: id }))
|
||||
?.lockedToVersion?.version ?? null,
|
||||
] as const
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const resolveVersion = (id: string | null): { version: string } | null => {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const version = versionById.get(id) ?? null;
|
||||
return version !== null ? { version } : null;
|
||||
};
|
||||
|
||||
const foldVersion = (run: CommonRelatedRun): CommonRelatedRunWithVersion => ({
|
||||
...run,
|
||||
lockedToVersion: resolveVersion(run.lockedToVersionId),
|
||||
});
|
||||
|
||||
return {
|
||||
...pgRow,
|
||||
lockedToVersion: resolveVersion(pgRow.lockedToVersionId),
|
||||
parentTaskRun: pgRow.parentTaskRun ? foldVersion(pgRow.parentTaskRun) : null,
|
||||
rootTaskRun: pgRow.rootTaskRun ? foldVersion(pgRow.rootTaskRun) : null,
|
||||
childRuns: pgRow.childRuns.map((child) => foldVersion(child)),
|
||||
isBuffered: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Postgres miss → fall back to the mollifier buffer. When the gate
|
||||
// diverted a trigger, the run lives in Redis until the drainer replays
|
||||
// it through engine.trigger. Synthesise the FoundRun shape so call()
|
||||
// returns a `QUEUED` (or `FAILED`) response with empty output, no
|
||||
// attempts, no relations.
|
||||
const buffered = await findRunByIdWithMollifierFallback({
|
||||
runId: friendlyId,
|
||||
environmentId: env.id,
|
||||
organizationId: env.organizationId,
|
||||
});
|
||||
|
||||
if (!buffered) return null;
|
||||
|
||||
return synthesiseFoundRunFromBuffer(buffered);
|
||||
}
|
||||
|
||||
public async call(taskRun: FoundRun, env: AuthenticatedEnvironment) {
|
||||
return startSpanWithEnv(tracer, "ApiRetrieveRunPresenter.call", env, async () => {
|
||||
let $payload: any;
|
||||
let $payloadPresignedUrl: string | undefined;
|
||||
let $output: any;
|
||||
let $outputPresignedUrl: string | undefined;
|
||||
|
||||
const payloadPacket = await conditionallyImportPacket({
|
||||
data: taskRun.payload,
|
||||
dataType: taskRun.payloadType,
|
||||
});
|
||||
|
||||
if (
|
||||
payloadPacket.dataType === "application/store" &&
|
||||
typeof payloadPacket.data === "string"
|
||||
) {
|
||||
const signed = await generatePresignedUrl(
|
||||
env.project.externalRef,
|
||||
env.slug,
|
||||
payloadPacket.data,
|
||||
"GET"
|
||||
);
|
||||
|
||||
if (signed.success) {
|
||||
$payloadPresignedUrl = signed.url;
|
||||
} else {
|
||||
logger.error(`Failed to generate presigned URL for payload: ${signed.error}`, {
|
||||
taskRunId: taskRun.id,
|
||||
payload: payloadPacket.data,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$payload = await parsePacketAsJson(payloadPacket);
|
||||
}
|
||||
|
||||
if (taskRun.status === "COMPLETED_SUCCESSFULLY") {
|
||||
const outputPacket = await conditionallyImportPacket({
|
||||
data: taskRun.output ?? undefined,
|
||||
dataType: taskRun.outputType,
|
||||
});
|
||||
|
||||
if (
|
||||
outputPacket.dataType === "application/store" &&
|
||||
typeof outputPacket.data === "string"
|
||||
) {
|
||||
const signed = await generatePresignedUrl(
|
||||
env.project.externalRef,
|
||||
env.slug,
|
||||
outputPacket.data,
|
||||
"GET"
|
||||
);
|
||||
|
||||
if (signed.success) {
|
||||
$outputPresignedUrl = signed.url;
|
||||
} else {
|
||||
logger.error(`Failed to generate presigned URL for output: ${signed.error}`, {
|
||||
taskRunId: taskRun.id,
|
||||
output: outputPacket.data,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$output = await parsePacketAsJson(outputPacket);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...(await createCommonRunStructure(taskRun, this.apiVersion)),
|
||||
payload: $payload,
|
||||
payloadPresignedUrl: $payloadPresignedUrl,
|
||||
output: $output,
|
||||
outputPresignedUrl: $outputPresignedUrl,
|
||||
error: ApiRetrieveRunPresenter.apiErrorFromError(taskRun.error),
|
||||
schedule: await resolveSchedule(taskRun),
|
||||
// We're removing attempts from the API
|
||||
attemptCount:
|
||||
taskRun.engine === "V1" ? taskRun.attempts.length : (taskRun.attemptNumber ?? 0),
|
||||
attempts: [],
|
||||
relatedRuns: {
|
||||
root: taskRun.rootTaskRun
|
||||
? await createCommonRunStructure(taskRun.rootTaskRun, this.apiVersion)
|
||||
: undefined,
|
||||
parent: taskRun.parentTaskRun
|
||||
? await createCommonRunStructure(taskRun.parentTaskRun, this.apiVersion)
|
||||
: undefined,
|
||||
children: await Promise.all(
|
||||
taskRun.childRuns.map(async (r) => await createCommonRunStructure(r, this.apiVersion))
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static apiErrorFromError(error: Prisma.JsonValue): SerializedError | undefined {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorData = TaskRunError.safeParse(error);
|
||||
|
||||
if (errorData.success) {
|
||||
return createJsonErrorObject(errorData.data);
|
||||
}
|
||||
}
|
||||
|
||||
static isStatusFinished(status: RunStatus | RunStatusUnspecifiedApiVersion) {
|
||||
return (
|
||||
status === "COMPLETED" ||
|
||||
status === "FAILED" ||
|
||||
status === "CANCELED" ||
|
||||
status === "INTERRUPTED" ||
|
||||
status === "CRASHED" ||
|
||||
status === "SYSTEM_FAILURE"
|
||||
);
|
||||
}
|
||||
|
||||
static apiStatusFromRunStatus(
|
||||
status: TaskRunStatus,
|
||||
apiVersion: API_VERSIONS
|
||||
): RunStatus | RunStatusUnspecifiedApiVersion {
|
||||
switch (apiVersion) {
|
||||
case CURRENT_API_VERSION: {
|
||||
return this.apiStatusFromRunStatusV2(status);
|
||||
}
|
||||
default: {
|
||||
return this.apiStatusFromRunStatusV1(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static apiStatusFromRunStatusV1(status: TaskRunStatus): RunStatusUnspecifiedApiVersion {
|
||||
switch (status) {
|
||||
case "DELAYED": {
|
||||
return "DELAYED";
|
||||
}
|
||||
case "PENDING_VERSION": {
|
||||
return "PENDING_VERSION";
|
||||
}
|
||||
case "WAITING_FOR_DEPLOY": {
|
||||
return "WAITING_FOR_DEPLOY";
|
||||
}
|
||||
case "PENDING": {
|
||||
return "QUEUED";
|
||||
}
|
||||
case "PAUSED":
|
||||
case "WAITING_TO_RESUME": {
|
||||
return "FROZEN";
|
||||
}
|
||||
case "RETRYING_AFTER_FAILURE": {
|
||||
return "REATTEMPTING";
|
||||
}
|
||||
case "DEQUEUED":
|
||||
case "EXECUTING": {
|
||||
return "EXECUTING";
|
||||
}
|
||||
case "CANCELED": {
|
||||
return "CANCELED";
|
||||
}
|
||||
case "COMPLETED_SUCCESSFULLY": {
|
||||
return "COMPLETED";
|
||||
}
|
||||
case "SYSTEM_FAILURE": {
|
||||
return "SYSTEM_FAILURE";
|
||||
}
|
||||
case "INTERRUPTED": {
|
||||
return "INTERRUPTED";
|
||||
}
|
||||
case "CRASHED": {
|
||||
return "CRASHED";
|
||||
}
|
||||
case "COMPLETED_WITH_ERRORS": {
|
||||
return "FAILED";
|
||||
}
|
||||
case "EXPIRED": {
|
||||
return "EXPIRED";
|
||||
}
|
||||
case "TIMED_OUT": {
|
||||
return "TIMED_OUT";
|
||||
}
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static apiStatusFromRunStatusV2(status: TaskRunStatus): RunStatus {
|
||||
switch (status) {
|
||||
case "DELAYED": {
|
||||
return "DELAYED";
|
||||
}
|
||||
case "PENDING_VERSION": {
|
||||
return "PENDING_VERSION";
|
||||
}
|
||||
case "WAITING_FOR_DEPLOY": {
|
||||
return "PENDING_VERSION";
|
||||
}
|
||||
case "PENDING": {
|
||||
return "QUEUED";
|
||||
}
|
||||
case "PAUSED":
|
||||
case "WAITING_TO_RESUME": {
|
||||
return "WAITING";
|
||||
}
|
||||
case "DEQUEUED": {
|
||||
return "DEQUEUED";
|
||||
}
|
||||
case "RETRYING_AFTER_FAILURE":
|
||||
case "EXECUTING": {
|
||||
return "EXECUTING";
|
||||
}
|
||||
case "CANCELED": {
|
||||
return "CANCELED";
|
||||
}
|
||||
case "COMPLETED_SUCCESSFULLY": {
|
||||
return "COMPLETED";
|
||||
}
|
||||
case "SYSTEM_FAILURE": {
|
||||
return "SYSTEM_FAILURE";
|
||||
}
|
||||
case "CRASHED": {
|
||||
return "CRASHED";
|
||||
}
|
||||
case "INTERRUPTED":
|
||||
case "COMPLETED_WITH_ERRORS": {
|
||||
return "FAILED";
|
||||
}
|
||||
case "EXPIRED": {
|
||||
return "EXPIRED";
|
||||
}
|
||||
case "TIMED_OUT": {
|
||||
return "TIMED_OUT";
|
||||
}
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static apiBooleanHelpersFromTaskRunStatus(status: TaskRunStatus, apiVersion: API_VERSIONS) {
|
||||
return ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(
|
||||
ApiRetrieveRunPresenter.apiStatusFromRunStatus(status, apiVersion)
|
||||
);
|
||||
}
|
||||
|
||||
static apiBooleanHelpersFromRunStatus(status: RunStatus | RunStatusUnspecifiedApiVersion) {
|
||||
const isQueued =
|
||||
status === "QUEUED" ||
|
||||
status === "WAITING_FOR_DEPLOY" ||
|
||||
status === "DELAYED" ||
|
||||
status === "PENDING_VERSION";
|
||||
const isExecuting =
|
||||
status === "EXECUTING" ||
|
||||
status === "REATTEMPTING" ||
|
||||
status === "FROZEN" ||
|
||||
status === "DEQUEUED";
|
||||
const isCompleted =
|
||||
status === "COMPLETED" ||
|
||||
status === "CANCELED" ||
|
||||
status === "FAILED" ||
|
||||
status === "CRASHED" ||
|
||||
status === "INTERRUPTED" ||
|
||||
status === "SYSTEM_FAILURE";
|
||||
const isFailed = isCompleted && status !== "COMPLETED";
|
||||
const isSuccess = isCompleted && status === "COMPLETED";
|
||||
const isCancelled = status === "CANCELED";
|
||||
const isWaiting = status === "WAITING";
|
||||
|
||||
return {
|
||||
isQueued,
|
||||
isExecuting,
|
||||
isCompleted,
|
||||
isFailed,
|
||||
isSuccess,
|
||||
isCancelled,
|
||||
isWaiting,
|
||||
};
|
||||
}
|
||||
|
||||
static apiStatusFromAttemptStatus(status: TaskRunAttemptStatus): AttemptStatus {
|
||||
switch (status) {
|
||||
case "PENDING": {
|
||||
return "PENDING";
|
||||
}
|
||||
case "PAUSED": {
|
||||
return "PAUSED";
|
||||
}
|
||||
case "EXECUTING": {
|
||||
return "EXECUTING";
|
||||
}
|
||||
case "COMPLETED": {
|
||||
return "COMPLETED";
|
||||
}
|
||||
case "FAILED": {
|
||||
return "FAILED";
|
||||
}
|
||||
case "CANCELED": {
|
||||
return "CANCELED";
|
||||
}
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSchedule(run: CommonRelatedRun) {
|
||||
if (!run.scheduleId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const schedule = await prisma.taskSchedule.findFirst({
|
||||
where: {
|
||||
id: run.scheduleId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: schedule.friendlyId,
|
||||
externalId: schedule.externalId ?? undefined,
|
||||
deduplicationKey: schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : undefined,
|
||||
generator: {
|
||||
type: "CRON" as const,
|
||||
expression: schedule.generatorExpression,
|
||||
description: schedule.generatorDescription,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createCommonRunStructure(
|
||||
run: CommonRelatedRunWithVersion,
|
||||
apiVersion: API_VERSIONS
|
||||
) {
|
||||
const metadata = await parsePacketAsJson({
|
||||
data: run.metadata ?? undefined,
|
||||
dataType: run.metadataType,
|
||||
});
|
||||
|
||||
return {
|
||||
id: run.friendlyId,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
idempotencyKey: getUserProvidedIdempotencyKey(run),
|
||||
version: run.lockedToVersion?.version,
|
||||
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(run.status, apiVersion),
|
||||
createdAt: run.createdAt,
|
||||
startedAt: run.startedAt ?? undefined,
|
||||
updatedAt: run.updatedAt,
|
||||
finishedAt: run.completedAt ?? undefined,
|
||||
expiredAt: run.expiredAt ?? undefined,
|
||||
delayedUntil: run.delayUntil ?? undefined,
|
||||
ttl: run.ttl ?? undefined,
|
||||
costInCents: run.costInCents,
|
||||
baseCostInCents: run.baseCostInCents,
|
||||
durationMs: run.usageDurationMs,
|
||||
isTest: run.isTest,
|
||||
depth: run.depth,
|
||||
tags: [...(run.runTags ?? [])].sort((a: string, b: string) => a.localeCompare(b)),
|
||||
...ApiRetrieveRunPresenter.apiBooleanHelpersFromTaskRunStatus(run.status, apiVersion),
|
||||
triggerFunction: resolveTriggerFunction(run),
|
||||
batchId: run.batch?.friendlyId,
|
||||
metadata,
|
||||
region: regionForDisplay(run.region, run.workerQueue),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTriggerFunction(run: CommonRelatedRun): TriggerFunction {
|
||||
if (run.batch) {
|
||||
return run.resumeParentOnCompletion ? "batchTriggerAndWait" : "batchTrigger";
|
||||
} else {
|
||||
return run.resumeParentOnCompletion ? "triggerAndWait" : "trigger";
|
||||
}
|
||||
}
|
||||
|
||||
// Build a FoundRun-shaped object from a buffered (mollified) run. The run
|
||||
// is in the Redis buffer; engine.trigger hasn't created the Postgres row
|
||||
// yet, so every field that comes from execution state (output, attempts,
|
||||
// completedAt, cost, relations) takes a default. The presenter's call()
|
||||
// handles QUEUED-state runs without surprise.
|
||||
function bufferedStatusToTaskRunStatus(status: SyntheticRun["status"]): TaskRunStatus {
|
||||
switch (status) {
|
||||
case "FAILED":
|
||||
return "SYSTEM_FAILURE";
|
||||
case "CANCELED":
|
||||
return "CANCELED";
|
||||
default:
|
||||
return "PENDING";
|
||||
}
|
||||
}
|
||||
|
||||
// The PG path stores `TaskRun.payload` as `String?`, so in production
|
||||
// the buffered snapshot's `payload` is always a string. We defensively
|
||||
// coerce other types instead of silently dropping them: an object gets
|
||||
// JSON-stringified (matches how the trigger path would serialise it),
|
||||
// anything truly unrenderable falls back to an empty string. The log
|
||||
// line surfaces format drift to ops without crashing the read path.
|
||||
function synthesisePayload(buffered: SyntheticRun): string {
|
||||
const payload = buffered.payload;
|
||||
if (typeof payload === "string") return payload;
|
||||
if (payload === undefined || payload === null) return "";
|
||||
try {
|
||||
const serialised = JSON.stringify(payload);
|
||||
logger.warn("ApiRetrieveRunPresenter: buffered snapshot.payload non-string coerced", {
|
||||
runFriendlyId: buffered.friendlyId,
|
||||
payloadType: typeof payload,
|
||||
});
|
||||
return typeof serialised === "string" ? serialised : "";
|
||||
} catch {
|
||||
logger.error("ApiRetrieveRunPresenter: buffered snapshot.payload unserialisable", {
|
||||
runFriendlyId: buffered.friendlyId,
|
||||
payloadType: typeof payload,
|
||||
});
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror synthesisePayload for metadata. The PG path stores
|
||||
// `TaskRun.metadata` as `String?`, and the snapshot writes it from
|
||||
// `metadataPacket.data` (also a string), so in production it is always a
|
||||
// string or absent. We coerce defensively — an object gets JSON-stringified
|
||||
// (matching how the trigger path serialises it) rather than silently
|
||||
// dropped to null, and the log line surfaces format drift to ops.
|
||||
function synthesiseMetadata(buffered: SyntheticRun): string | null {
|
||||
const metadata = buffered.metadata;
|
||||
if (typeof metadata === "string") return metadata;
|
||||
if (metadata === undefined || metadata === null) return null;
|
||||
try {
|
||||
const serialised = JSON.stringify(metadata);
|
||||
logger.warn("ApiRetrieveRunPresenter: buffered snapshot.metadata non-string coerced", {
|
||||
runFriendlyId: buffered.friendlyId,
|
||||
metadataType: typeof metadata,
|
||||
});
|
||||
return typeof serialised === "string" ? serialised : null;
|
||||
} catch {
|
||||
logger.error("ApiRetrieveRunPresenter: buffered snapshot.metadata unserialisable", {
|
||||
runFriendlyId: buffered.friendlyId,
|
||||
metadataType: typeof metadata,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for unit testing. Used by `findRun()` above when the
|
||||
// Postgres lookup misses and the buffer carries the run — keep the shape
|
||||
// in lockstep with `FoundRun`'s field list so `call()` treats a synthesised
|
||||
// buffered run identically to a freshly-triggered PG row.
|
||||
export function synthesiseFoundRunFromBuffer(buffered: SyntheticRun): FoundRun {
|
||||
const status: TaskRunStatus = bufferedStatusToTaskRunStatus(buffered.status);
|
||||
|
||||
const errorJson: Prisma.JsonValue = buffered.error
|
||||
? {
|
||||
type: "STRING_ERROR",
|
||||
raw: `${buffered.error.code}: ${buffered.error.message}`,
|
||||
}
|
||||
: null;
|
||||
|
||||
const metadata: string | null = synthesiseMetadata(buffered);
|
||||
|
||||
return {
|
||||
// `id` is the internal cuid (Prisma TaskRun.id column), `friendlyId`
|
||||
// is the user-facing `run_xxx` token. Downstream logging keyed off
|
||||
// `taskRun.id` correlates with other systems via the cuid — using
|
||||
// the friendlyId here breaks log correlation. `SyntheticRun` carries
|
||||
// the cuid alongside the friendlyId for exactly this reason
|
||||
// (RunId.fromFriendlyId in readFallback.server.ts).
|
||||
id: buffered.id,
|
||||
friendlyId: buffered.friendlyId,
|
||||
status,
|
||||
taskIdentifier: buffered.taskIdentifier ?? "",
|
||||
createdAt: buffered.createdAt,
|
||||
startedAt: null,
|
||||
updatedAt: buffered.cancelledAt ?? buffered.createdAt,
|
||||
// PG-resident SYSTEM_FAILURE rows always have `completedAt` set by
|
||||
// the engine; the buffer-synth path must match so SDK consumers
|
||||
// that poll on `isCompleted` and then read `finishedAt` see a real
|
||||
// timestamp instead of `undefined`. CANCELED already had this via
|
||||
// `buffered.cancelledAt`; fall back to `buffered.createdAt` for
|
||||
// FAILED (the buffer entry has no separate "failedAt" — the
|
||||
// best-available approximation of when the terminal state landed
|
||||
// is the entry's creation time).
|
||||
completedAt: buffered.cancelledAt ?? (status === "SYSTEM_FAILURE" ? buffered.createdAt : null),
|
||||
expiredAt: null,
|
||||
delayUntil: buffered.delayUntil ?? null,
|
||||
metadata,
|
||||
metadataType: buffered.metadataType ?? "application/json",
|
||||
ttl: buffered.ttl ?? null,
|
||||
costInCents: 0,
|
||||
baseCostInCents: 0,
|
||||
usageDurationMs: 0,
|
||||
idempotencyKey: buffered.idempotencyKey ?? null,
|
||||
idempotencyKeyOptions: buffered.idempotencyKeyOptions ?? null,
|
||||
isTest: buffered.isTest,
|
||||
depth: buffered.depth,
|
||||
// Scheduled triggers go through the same TriggerTaskService path as
|
||||
// API triggers and aren't bypassed by the mollifier gate, so a
|
||||
// scheduled run can land in the buffer with its scheduleId set on the
|
||||
// snapshot. Forward it so resolveSchedule() can hydrate the `schedule`
|
||||
// field in the API response instead of silently dropping it until the
|
||||
// drainer materialises.
|
||||
scheduleId: buffered.scheduleId ?? null,
|
||||
// commonRunSelect now carries the scalar id; a buffered run has no locked worker yet.
|
||||
lockedToVersionId: null,
|
||||
lockedToVersion: buffered.lockedToVersion ? { version: buffered.lockedToVersion } : null,
|
||||
resumeParentOnCompletion: buffered.resumeParentOnCompletion,
|
||||
// Reconstruct the batch from the snapshot's internal id so a buffered
|
||||
// run reports the same `batchId` / triggerFunction as it will once
|
||||
// materialised, and so batch-scoped JWTs authorise against it (the
|
||||
// route authorization callbacks read `run.batch?.friendlyId`).
|
||||
batch: buffered.batchId
|
||||
? { id: buffered.batchId, friendlyId: BatchId.toFriendlyId(buffered.batchId) }
|
||||
: null,
|
||||
runTags: buffered.tags,
|
||||
traceId: buffered.traceId ?? "",
|
||||
payload: synthesisePayload(buffered),
|
||||
payloadType: buffered.payloadType ?? "application/json",
|
||||
output: null,
|
||||
outputType: "application/json",
|
||||
error: errorJson,
|
||||
attempts: [],
|
||||
attemptNumber: null,
|
||||
engine: "V2",
|
||||
taskEventStore: "taskEvent",
|
||||
// Empty string when absent (matches syntheticSpanRun.server.ts and lets
|
||||
// `createCommonRunStructure`'s `run.workerQueue || undefined` coerce the
|
||||
// API response's `region` to undefined instead of advertising a
|
||||
// misleading "main" region for a not-yet-assigned buffered run).
|
||||
workerQueue: buffered.workerQueue ?? "",
|
||||
region: buffered.region ?? "",
|
||||
parentTaskRun: null,
|
||||
rootTaskRun: null,
|
||||
childRuns: [],
|
||||
isBuffered: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user