Files
triggerdotdev--trigger.dev/apps/webapp/app/v3/services/createCheckpoint.server.ts
T
2026-07-13 13:32:57 +08:00

445 lines
12 KiB
TypeScript

import type { CoordinatorToPlatformMessages, ManualCheckpointMetadata } from "@trigger.dev/core/v3";
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
import type { Checkpoint, CheckpointRestoreEvent } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { isFreezableAttemptStatus, isFreezableRunStatus } from "../taskStatus";
import { BaseService } from "./baseService.server";
import { CreateCheckpointRestoreEventService } from "./createCheckpointRestoreEvent.server";
import { ResumeBatchRunService } from "./resumeBatchRun.server";
import { ResumeDependentParentsService } from "./resumeDependentParents.server";
import { CheckpointId } from "@trigger.dev/core/v3/isomorphic";
export class CreateCheckpointService extends BaseService {
public async call(
params: Omit<
InferSocketMessageSchema<typeof CoordinatorToPlatformMessages, "CHECKPOINT_CREATED">,
"version"
>
): Promise<
| {
success: true;
checkpoint: Checkpoint;
event: CheckpointRestoreEvent;
keepRunAlive: boolean;
}
| {
success: false;
keepRunAlive?: boolean;
}
> {
logger.debug(`Creating checkpoint`, params);
const attempt = await this._prisma.taskRunAttempt.findFirst({
where: {
friendlyId: params.attemptFriendlyId,
},
include: {
taskRun: true,
backgroundWorker: {
select: {
id: true,
deployment: {
select: {
imageReference: true,
},
},
},
},
},
});
if (!attempt) {
logger.error("Attempt not found", params);
return {
success: false,
};
}
if (
!isFreezableAttemptStatus(attempt.status) ||
!isFreezableRunStatus(attempt.taskRun.status)
) {
logger.error("Unfreezable state", {
attempt: {
id: attempt.id,
status: attempt.status,
},
run: {
id: attempt.taskRunId,
status: attempt.taskRun.status,
},
params,
});
return {
success: false,
keepRunAlive: true,
};
}
const imageRef = attempt.backgroundWorker.deployment?.imageReference;
if (!imageRef) {
logger.error("Missing deployment or image ref", {
attemptId: attempt.id,
workerId: attempt.backgroundWorker.id,
params,
});
return {
success: false,
};
}
const { reason } = params;
// Check if we should accept this checkpoint
switch (reason.type) {
case "MANUAL": {
// Always accept manual checkpoints
break;
}
case "WAIT_FOR_DURATION": {
// Always accept duration checkpoints
break;
}
case "WAIT_FOR_TASK": {
const childRun = await this._prisma.taskRun.findFirst({
where: {
friendlyId: reason.friendlyId,
},
select: {
dependency: {
select: {
resumedAt: true,
},
},
},
});
if (!childRun) {
logger.error("CreateCheckpointService: Pre-check - WAIT_FOR_TASK child run not found", {
friendlyId: reason.friendlyId,
params,
});
return {
success: false,
keepRunAlive: false,
};
}
if (childRun.dependency?.resumedAt) {
logger.info("CreateCheckpointService: Child run already resumed", {
childRun,
params,
});
return {
success: false,
keepRunAlive: true,
};
}
break;
}
case "WAIT_FOR_BATCH": {
// Routed by friendlyId so a run-ops id (NEW-resident) batch is found on the owning DB;
// env-scoped to the dependent attempt's run (a batch shares its dependent's env). Read the
// primary: a batch that just resumed the parent may lag the replica, and a stale resumedAt
// (null) would checkpoint (suspend) an already-resumed run -> it stalls until a sweep.
const batchRun = await this.runStore.findBatchTaskRunByFriendlyId(
reason.batchFriendlyId,
attempt.taskRun.runtimeEnvironmentId,
undefined,
this._prisma
);
if (!batchRun) {
logger.error("CreateCheckpointService: Pre-check - Batch not found", {
batchFriendlyId: reason.batchFriendlyId,
params,
});
return {
success: false,
keepRunAlive: false,
};
}
if (batchRun.resumedAt) {
logger.info("CreateCheckpointService: Batch already resumed", {
batchRun,
params,
});
return {
success: false,
keepRunAlive: true,
};
}
break;
}
default: {
break;
}
}
//sleep to test slow checkpoints
// Sleep a random value between 4 and 30 seconds
// await new Promise((resolve) => {
// const waitSeconds = Math.floor(Math.random() * 26) + 4;
// logger.log(`Sleep for ${waitSeconds} seconds`);
// setTimeout(resolve, waitSeconds * 1000);
// });
let metadata: string;
if (params.reason.type === "MANUAL") {
metadata = JSON.stringify({
...params.reason,
attemptId: attempt.id,
previousAttemptStatus: attempt.status,
previousRunStatus: attempt.taskRun.status,
} satisfies ManualCheckpointMetadata);
} else {
metadata = JSON.stringify(params.reason);
}
const checkpoint = await this._prisma.checkpoint.create({
data: {
...CheckpointId.generate(),
runtimeEnvironmentId: attempt.taskRun.runtimeEnvironmentId,
projectId: attempt.taskRun.projectId,
attemptId: attempt.id,
attemptNumber: attempt.number,
runId: attempt.taskRunId,
location: params.location,
type: params.docker ? "DOCKER" : "KUBERNETES",
reason: params.reason.type,
metadata,
imageRef,
},
});
const eventService = new CreateCheckpointRestoreEventService(this._prisma);
await this._prisma.taskRunAttempt.update({
where: {
id: attempt.id,
},
data: {
status: params.reason.type === "RETRYING_AFTER_FAILURE" ? undefined : "PAUSED",
taskRun: {
update: {
status: "WAITING_TO_RESUME",
},
},
},
});
let checkpointEvent: CheckpointRestoreEvent | undefined;
switch (reason.type) {
case "MANUAL":
case "WAIT_FOR_DURATION": {
let restoreAtUnixTimeMs: number;
if (reason.type === "MANUAL") {
// Restore immediately if not specified, useful for live migration
restoreAtUnixTimeMs = reason.restoreAtUnixTimeMs ?? Date.now();
} else {
restoreAtUnixTimeMs = reason.now + reason.ms;
}
checkpointEvent = await eventService.checkpoint({
checkpointId: checkpoint.id,
});
if (checkpointEvent) {
await marqs.requeueMessage(
attempt.taskRunId,
{
type: "RESUME_AFTER_DURATION",
resumableAttemptId: attempt.id,
checkpointEventId: checkpointEvent.id,
},
restoreAtUnixTimeMs,
"resume"
);
return {
success: true,
checkpoint,
event: checkpointEvent,
keepRunAlive: false,
};
}
break;
}
case "WAIT_FOR_TASK": {
checkpointEvent = await eventService.checkpoint({
checkpointId: checkpoint.id,
dependencyFriendlyRunId: reason.friendlyId,
});
if (checkpointEvent) {
//heartbeats will start again when the run resumes
logger.log("CreateCheckpointService: Canceling heartbeat", {
attemptId: attempt.id,
taskRunId: attempt.taskRunId,
type: "WAIT_FOR_TASK",
reason,
params,
});
await marqs?.cancelHeartbeat(attempt.taskRunId);
const childRun = await this._prisma.taskRun.findFirst({
where: {
friendlyId: reason.friendlyId,
},
});
if (!childRun) {
logger.error("CreateCheckpointService: WAIT_FOR_TASK child run not found", {
friendlyId: reason.friendlyId,
params,
});
return {
success: true,
checkpoint,
event: checkpointEvent,
keepRunAlive: false,
};
}
const resumeService = new ResumeDependentParentsService(this._prisma);
const result = await resumeService.call({ id: childRun.id });
if (result.success) {
logger.log("CreateCheckpointService: Resumed dependent parents", {
result,
childRun,
attempt,
checkpointEvent,
params,
});
} else {
logger.error("CreateCheckpointService: Failed to resume dependent parents", {
result,
childRun,
attempt,
checkpointEvent,
params,
});
}
return {
success: true,
checkpoint,
event: checkpointEvent,
keepRunAlive: false,
};
}
break;
}
case "WAIT_FOR_BATCH": {
checkpointEvent = await eventService.checkpoint({
checkpointId: checkpoint.id,
batchDependencyFriendlyId: reason.batchFriendlyId,
});
if (checkpointEvent) {
//heartbeats will start again when the run resumes
logger.log("CreateCheckpointService: Canceling heartbeat", {
attemptId: attempt.id,
taskRunId: attempt.taskRunId,
type: "WAIT_FOR_BATCH",
params,
});
await marqs?.cancelHeartbeat(attempt.taskRunId);
// Routed by friendlyId; read the primary (this._prisma) so a just-resumed batch that still
// lags the replica doesn't leave a stale resumedAt and suspend an already-resumed run.
const batchRun = await this.runStore.findBatchTaskRunByFriendlyId(
reason.batchFriendlyId,
attempt.taskRun.runtimeEnvironmentId,
undefined,
this._prisma
);
if (!batchRun) {
logger.error("CreateCheckpointService: Batch not found", {
friendlyId: reason.batchFriendlyId,
params,
});
return {
success: true,
checkpoint,
event: checkpointEvent,
keepRunAlive: false,
};
}
//if there's a message in the queue, we make sure the checkpoint event is on it
await marqs.replaceMessage(attempt.taskRun.id, {
checkpointEventId: checkpointEvent.id,
});
await ResumeBatchRunService.enqueue(batchRun.id, batchRun.batchVersion === "v3");
return {
success: true,
checkpoint,
event: checkpointEvent,
keepRunAlive: false,
};
}
break;
}
case "RETRYING_AFTER_FAILURE": {
checkpointEvent = await eventService.checkpoint({
checkpointId: checkpoint.id,
});
// ACK is already handled by attempt completion
break;
}
default: {
break;
}
}
if (!checkpointEvent) {
logger.error("No checkpoint event", {
attemptId: attempt.id,
checkpointId: checkpoint.id,
params,
});
await marqs?.acknowledgeMessage(
attempt.taskRunId,
"No checkpoint event in CreateCheckpointService"
);
return {
success: false,
};
}
return {
success: true,
checkpoint,
event: checkpointEvent,
keepRunAlive: false,
};
}
}