-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathapi.v1.runs.$runParam.replay.ts
More file actions
155 lines (142 loc) · 5.99 KB
/
Copy pathapi.v1.runs.$runParam.replay.ts
File metadata and controls
155 lines (142 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import type { TaskRun } from "@trigger.dev/database";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
import { sanitizeTriggerSource } from "~/utils/triggerSource";
import { clientSafeErrorMessage } from "~/utils/prismaErrors";
const ParamsSchema = z.object({
/* This is the run friendly ID */
runParam: z.string(),
});
// Subset of TaskRun fields that ReplayTaskRunService.call actually
// reads from `existingTaskRun`. Validate the buffered fallback against
// this before casting to TaskRun so a buffer-format drift surfaces as a
// 404/422 here rather than as a silent NaN/undefined deep inside
// replay. The full TaskRun type has many more fields the service never
// touches; we only assert the ones it reads.
const BufferedReplayInputSchema = z.object({
id: z.string(),
friendlyId: z.string(),
runtimeEnvironmentId: z.string(),
taskIdentifier: z.string(),
payload: z.string(),
payloadType: z.string(),
queue: z.string(),
isTest: z.boolean(),
traceId: z.string(),
spanId: z.string(),
engine: z.string(),
runTags: z.array(z.string()),
// Nullable / optional fields the service tolerates via `??` fallbacks.
concurrencyKey: z.string().nullable().optional(),
workerQueue: z.string().nullable().optional(),
machinePreset: z.string().nullable().optional(),
realtimeStreamsVersion: z.string().nullable().optional(),
// ReplayTaskRunService.getExistingMetadata reads these to preserve
// the original run's metadata on replay. Without them in the schema
// they'd be stripped by Zod's default key-passthrough behaviour, and
// a buffered-source replay would silently lose metadata that a
// PG-source replay carries over.
seedMetadata: z.string().nullable().optional(),
seedMetadataType: z.string().nullable().optional(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
}
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return json({ error: "Invalid or missing run ID" }, { status: 400 });
}
const { runParam } = parsed.data;
try {
const env = authenticationResult.environment;
// PG-first. Replay works on any status per audit — no
// filter beyond friendlyId is the existing semantic; findFirst with
// env scoping tightens it minimally without changing behaviour for
// a correctly-authed caller.
let taskRun: TaskRun | null = await prisma.taskRun.findFirst({
where: {
friendlyId: runParam,
runtimeEnvironmentId: env.id,
},
});
if (!taskRun) {
// Buffered fallback. SyntheticRun carries every field
// ReplayTaskRunService reads from a TaskRun. Validate the subset of
// fields the service consumes (BufferedReplayInputSchema above)
// before casting; a schema mismatch surfaces as a 404 here rather
// than as a silent undefined deep inside the service.
const buffered = await findRunByIdWithMollifierFallback({
runId: runParam,
environmentId: env.id,
organizationId: env.organizationId,
});
if (buffered) {
const parsed = BufferedReplayInputSchema.safeParse(buffered);
if (parsed.success) {
// Manual sync point: `BufferedReplayInputSchema` covers only
// the subset of `TaskRun` fields `ReplayTaskRunService.call`
// currently reads from `existingTaskRun`. The cast is `as
// unknown as TaskRun` because the full `TaskRun` type carries
// ~40 fields the service never touches; mirroring all of them
// on a synthetic snapshot would be misleading. If a future
// change to `ReplayTaskRunService` reads an additional
// `existingTaskRun` field, **add it to the schema above** —
// otherwise the buffered path will silently feed the service
// `undefined` for that field while the PG-source replay
// works. The `safeParse` + warn-log + 404 below is the
// run-time fail-safe; this comment is the design fail-safe.
taskRun = parsed.data as unknown as TaskRun;
} else {
logger.warn("replay: buffered fallback failed schema validation", {
runParam,
issues: parsed.error.issues.map((issue) => ({
path: issue.path.join("."),
code: issue.code,
})),
});
}
}
}
if (!taskRun) {
return json({ error: "Run not found" }, { status: 404 });
}
const triggerSource =
sanitizeTriggerSource(request.headers.get("x-trigger-source")) ?? "api";
const service = new ReplayTaskRunService();
const newRun = await service.call(taskRun, { triggerSource });
if (!newRun) {
return json({ error: "Failed to create new run" }, { status: 400 });
}
return json({
id: newRun?.friendlyId,
});
} catch (error) {
if (error instanceof Error) {
logger.error("Failed to replay run", {
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
run: runParam,
});
return json({ error: clientSafeErrorMessage(error) }, { status: 400 });
} else {
logger.error("Failed to replay run", { error: JSON.stringify(error), run: runParam });
return json({ error: JSON.stringify(error) }, { status: 400 });
}
}
}