291 lines
7.8 KiB
TypeScript
291 lines
7.8 KiB
TypeScript
import type {
|
|
CoordinatorToPlatformMessages,
|
|
TaskRunExecution,
|
|
TaskRunExecutionResult,
|
|
} from "@trigger.dev/core/v3";
|
|
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
|
|
import type { Prisma, TaskRunAttempt } from "@trigger.dev/database";
|
|
import { logger } from "~/services/logger.server";
|
|
import { marqs } from "~/v3/marqs/index.server";
|
|
import { socketIo } from "../handleSocketIo.server";
|
|
import { sharedQueueTasks } from "../marqs/sharedQueueConsumer.server";
|
|
import { FINAL_ATTEMPT_STATUSES, isFinalRunStatus } from "../taskStatus";
|
|
import { BaseService } from "./baseService.server";
|
|
|
|
export class ResumeAttemptService extends BaseService {
|
|
private _logger = logger;
|
|
|
|
public async call(
|
|
params: InferSocketMessageSchema<typeof CoordinatorToPlatformMessages, "READY_FOR_RESUME">
|
|
): Promise<void> {
|
|
this._logger.debug(`ResumeAttemptService.call()`, params);
|
|
|
|
const latestAttemptSelect = {
|
|
orderBy: {
|
|
number: "desc",
|
|
},
|
|
take: 1,
|
|
select: {
|
|
id: true,
|
|
number: true,
|
|
status: true,
|
|
},
|
|
} satisfies Prisma.TaskRunInclude["attempts"];
|
|
|
|
const attempt = await this._prisma.taskRunAttempt.findFirst({
|
|
where: {
|
|
friendlyId: params.attemptFriendlyId,
|
|
},
|
|
include: {
|
|
taskRun: true,
|
|
dependencies: {
|
|
select: {
|
|
taskRun: {
|
|
select: {
|
|
attempts: latestAttemptSelect,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
take: 1,
|
|
},
|
|
batchDependencies: {
|
|
select: {
|
|
items: {
|
|
select: {
|
|
taskRun: {
|
|
select: {
|
|
attempts: latestAttemptSelect,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!attempt) {
|
|
this._logger.error("Could not find attempt", params);
|
|
return;
|
|
}
|
|
|
|
this._logger = logger.child({
|
|
attemptId: attempt.id,
|
|
attemptFriendlyId: attempt.friendlyId,
|
|
taskRun: attempt.taskRun,
|
|
});
|
|
|
|
if (isFinalRunStatus(attempt.taskRun.status)) {
|
|
this._logger.error("Run is not resumable");
|
|
return;
|
|
}
|
|
|
|
let completedAttemptIds: string[] = [];
|
|
|
|
switch (params.type) {
|
|
case "WAIT_FOR_DURATION": {
|
|
this._logger.debug("Sending duration wait resume message");
|
|
|
|
await this.#setPostResumeStatuses(attempt);
|
|
|
|
socketIo.coordinatorNamespace.emit("RESUME_AFTER_DURATION", {
|
|
version: "v1",
|
|
attemptId: attempt.id,
|
|
attemptFriendlyId: attempt.friendlyId,
|
|
});
|
|
break;
|
|
}
|
|
case "WAIT_FOR_TASK": {
|
|
if (attempt.dependencies.length) {
|
|
// We only care about the latest dependency
|
|
const dependentAttempt = attempt.dependencies[0].taskRun.attempts[0];
|
|
|
|
if (!dependentAttempt) {
|
|
this._logger.error("No dependent attempt");
|
|
return;
|
|
}
|
|
|
|
completedAttemptIds = [dependentAttempt.id];
|
|
} else {
|
|
this._logger.error("No task dependency");
|
|
return;
|
|
}
|
|
|
|
await this.#handleDependencyResume(attempt, completedAttemptIds);
|
|
|
|
break;
|
|
}
|
|
case "WAIT_FOR_BATCH": {
|
|
if (attempt.batchDependencies) {
|
|
// We only care about the latest batch dependency
|
|
const dependentBatchItems = attempt.batchDependencies[0].items;
|
|
|
|
if (!dependentBatchItems) {
|
|
this._logger.error("No dependent batch items");
|
|
return;
|
|
}
|
|
|
|
//find the best attempt for each batch item
|
|
//it should be the most recent one in a final state
|
|
const finalAttempts = dependentBatchItems
|
|
.map((item) => {
|
|
return item.taskRun.attempts
|
|
.filter((a) => FINAL_ATTEMPT_STATUSES.includes(a.status))
|
|
.sort((a, b) => b.number - a.number)
|
|
.at(0);
|
|
})
|
|
.filter(Boolean);
|
|
|
|
completedAttemptIds = finalAttempts.map((a) => a.id);
|
|
|
|
if (completedAttemptIds.length !== dependentBatchItems.length) {
|
|
this._logger.error("[ResumeAttemptService] not all batch items have attempts", {
|
|
runId: attempt.taskRunId,
|
|
completedAttemptIds,
|
|
finalAttempts,
|
|
dependentBatchItems,
|
|
});
|
|
|
|
return;
|
|
}
|
|
} else {
|
|
this._logger.error("No batch dependency");
|
|
return;
|
|
}
|
|
|
|
await this.#handleDependencyResume(attempt, completedAttemptIds);
|
|
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async #handleDependencyResume(attempt: TaskRunAttempt, completedAttemptIds: string[]) {
|
|
if (completedAttemptIds.length === 0) {
|
|
this._logger.error("No completed attempt IDs");
|
|
return;
|
|
}
|
|
|
|
const completions: TaskRunExecutionResult[] = [];
|
|
const executions: TaskRunExecution[] = [];
|
|
|
|
for (const completedAttemptId of completedAttemptIds) {
|
|
const completedAttempt = await this._prisma.taskRunAttempt.findFirst({
|
|
where: {
|
|
id: completedAttemptId,
|
|
taskRun: {
|
|
lockedAt: {
|
|
not: null,
|
|
},
|
|
lockedById: {
|
|
not: null,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!completedAttempt) {
|
|
this._logger.error("Completed attempt not found", { completedAttemptId });
|
|
await marqs?.acknowledgeMessage(
|
|
attempt.taskRunId,
|
|
"Cannot find completed attempt in ResumeAttemptService"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const logger = this._logger.child({
|
|
completedAttemptId: completedAttempt.id,
|
|
completedAttemptFriendlyId: completedAttempt.friendlyId,
|
|
completedRunId: completedAttempt.taskRunId,
|
|
});
|
|
|
|
const resumePayload = await sharedQueueTasks.getResumePayload(completedAttempt.id);
|
|
|
|
if (!resumePayload) {
|
|
logger.error("Failed to get resume payload");
|
|
await marqs?.acknowledgeMessage(
|
|
attempt.taskRunId,
|
|
"Failed to get resume payload in ResumeAttemptService"
|
|
);
|
|
return;
|
|
}
|
|
|
|
completions.push(resumePayload.completion);
|
|
executions.push(resumePayload.execution);
|
|
}
|
|
|
|
await this.#setPostResumeStatuses(attempt);
|
|
|
|
socketIo.coordinatorNamespace.emit("RESUME_AFTER_DEPENDENCY", {
|
|
version: "v1",
|
|
runId: attempt.taskRunId,
|
|
attemptId: attempt.id,
|
|
attemptFriendlyId: attempt.friendlyId,
|
|
completions,
|
|
executions,
|
|
});
|
|
}
|
|
|
|
async #setPostResumeStatuses(attempt: TaskRunAttempt) {
|
|
try {
|
|
const updatedAttempt = await this._prisma.taskRunAttempt.update({
|
|
where: {
|
|
id: attempt.id,
|
|
},
|
|
data: {
|
|
status: "EXECUTING",
|
|
taskRun: {
|
|
update: {
|
|
data: {
|
|
status: attempt.number > 1 ? "RETRYING_AFTER_FAILURE" : "EXECUTING",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
taskRun: {
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
this._logger.debug("Set post resume statuses", {
|
|
run: {
|
|
id: updatedAttempt.taskRun.id,
|
|
status: updatedAttempt.taskRun.status,
|
|
},
|
|
attempt: {
|
|
id: updatedAttempt.id,
|
|
status: updatedAttempt.status,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
this._logger.error("Failed to set post resume statuses", {
|
|
error:
|
|
error instanceof Error
|
|
? {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
}
|
|
: error,
|
|
});
|
|
}
|
|
}
|
|
}
|