FinalizeRunService (#1250)

* WIP notes on each location where we’ll use finalize

* Initial FinalizeTaskRunService

* ExpireEnqueuedRunService uses FinalizeTaskRunService

* FailedTaskRunService uses FinalizeTaskRunService

* Allow passing in an include when finalizing the run

* CrashTaskRunService using FinalizeTaskRunService

* Remove comments

* Status is optional

* CancelAttemptService using FinalizeTaskRunService

* Import tidy

* CancelTaskRunService using FinalizeTaskRunService

* Import tidying

* CompleteAttemptService system failure switched to FinalizeTaskRunService

* Added more logging to Finalizing

* CompleteAttemptStatus COMPLETED_SUCCESSFULLY

* CompletedAttempt “SYSTEM_FAILURE”

* CompletedService final pair

* Use satisfies so we can derive types from the groups

* Only allow final states to be used with this service

* BaseService tx support, minor improvements
This commit is contained in:
Matt Aitken
2024-08-08 15:32:06 +01:00
committed by GitHub
parent 0d4e3e70c1
commit bb38261947
8 changed files with 175 additions and 144 deletions
@@ -67,15 +67,19 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
EXPIRED: "Task has surpassed its ttl and won't be executed",
};
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY", "DELAYED"];
export const QUEUED_STATUSES = [
"PENDING",
"WAITING_FOR_DEPLOY",
"DELAYED",
] satisfies TaskRunStatus[];
export const RUNNING_STATUSES: TaskRunStatus[] = [
export const RUNNING_STATUSES = [
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
];
] satisfies TaskRunStatus[];
export const FINISHED_STATUSES: TaskRunStatus[] = [
export const FINISHED_STATUSES = [
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
@@ -83,7 +87,9 @@ export const FINISHED_STATUSES: TaskRunStatus[] = [
"SYSTEM_FAILURE",
"CRASHED",
"EXPIRED",
];
] satisfies TaskRunStatus[];
export type FINISHED_STATUSES = (typeof FINISHED_STATUSES)[number];
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
return taskRunStatusDescriptions[status];
+8 -14
View File
@@ -1,10 +1,9 @@
import { TaskRunFailedExecutionResult } from "@trigger.dev/core/v3";
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { TaskRunStatus } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";
import { createExceptionPropertiesFromError, eventRepository } from "./eventRepository.server";
import { BaseService } from "./services/baseService.server";
import { FinalizeTaskRunService } from "./services/finalizeTaskRun.server";
const FAILABLE_TASK_RUN_STATUSES: TaskRunStatus[] = ["EXECUTING", "PENDING", "WAITING_FOR_DEPLOY"];
@@ -40,7 +39,12 @@ export class FailedTaskRunService extends BaseService {
// No more retries, we need to fail the task run
logger.debug("[FailedTaskRunService] Failing task run", { taskRun, completion });
await marqs?.acknowledgeMessage(taskRun.id);
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: taskRun.id,
status: "SYSTEM_FAILURE",
completedAt: new Date(),
});
// Now we need to "complete" the task run event/span
await eventRepository.completeEvent(taskRun.spanId, {
@@ -58,15 +62,5 @@ export class FailedTaskRunService extends BaseService {
},
],
});
await this._prisma.taskRun.update({
where: {
id: taskRun.id,
},
data: {
status: "SYSTEM_FAILURE",
completedAt: new Date(),
},
});
}
}
@@ -1,11 +1,10 @@
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { $transaction, type PrismaClientOrTransaction, prisma } from "~/db.server";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { eventRepository } from "../eventRepository.server";
import { BaseService } from "./baseService.server";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { isCancellableRunStatus } from "../taskStatus";
import { BaseService } from "./baseService.server";
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
import { ResumeTaskRunDependenciesService } from "./resumeTaskRunDependencies.server";
export class CancelAttemptService extends BaseService {
@@ -51,28 +50,25 @@ export class CancelAttemptService extends BaseService {
return;
}
await marqs?.acknowledgeMessage(taskRunId);
await this._prisma.taskRunAttempt.update({
where: {
friendlyId: attemptId,
},
data: {
status: "CANCELED",
completedAt: cancelledAt,
taskRun: {
update: {
data: {
status: isCancellableRunStatus(taskRunAttempt.taskRun.status)
? "INTERRUPTED"
: undefined,
completedAt: isCancellableRunStatus(taskRunAttempt.taskRun.status)
? cancelledAt
: undefined,
},
},
await $transaction(this._prisma, async (tx) => {
await tx.taskRunAttempt.update({
where: {
friendlyId: attemptId,
},
},
data: {
status: "CANCELED",
completedAt: cancelledAt,
},
});
const finalizeService = new FinalizeTaskRunService(tx);
await finalizeService.call({
id: taskRunId,
status: isCancellableRunStatus(taskRunAttempt.taskRun.status) ? "INTERRUPTED" : undefined,
completedAt: isCancellableRunStatus(taskRunAttempt.taskRun.status)
? cancelledAt
: undefined,
});
});
const inProgressEvents = await eventRepository.queryIncompleteEvents({
@@ -1,14 +1,14 @@
import { Prisma, TaskRun } from "@trigger.dev/database";
import { type Prisma, type TaskRun } from "@trigger.dev/database";
import assertNever from "assert-never";
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { eventRepository } from "../eventRepository.server";
import { socketIo } from "../handleSocketIo.server";
import { devPubSub } from "../marqs/devPubSub.server";
import { CANCELLABLE_ATTEMPT_STATUSES, isCancellableRunStatus } from "../taskStatus";
import { BaseService } from "./baseService.server";
import { CancelAttemptService } from "./cancelAttempt.server";
import { CANCELLABLE_ATTEMPT_STATUSES, isCancellableRunStatus } from "../taskStatus";
import { CancelTaskAttemptDependenciesService } from "./cancelTaskAttemptDependencies.server";
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
type ExtendedTaskRun = Prisma.TaskRunGetPayload<{
include: {
@@ -47,18 +47,11 @@ export class CancelTaskRunService extends BaseService {
return;
}
// Remove the task run from the queue if it's there for some reason
await marqs?.acknowledgeMessage(taskRun.id);
// Set the task run status to cancelled
const cancelledTaskRun = await this._prisma.taskRun.update({
where: {
id: taskRun.id,
},
data: {
status: "CANCELED",
completedAt: opts.cancelledAt,
},
const finalizeService = new FinalizeTaskRunService();
const cancelledTaskRun = await finalizeService.call({
id: taskRun.id,
status: "CANCELED",
completedAt: opts.cancelledAt,
include: {
attempts: {
where: {
@@ -8,7 +8,7 @@ import {
flattenAttributes,
sanitizeError,
} from "@trigger.dev/core/v3";
import { PrismaClientOrTransaction } from "~/db.server";
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { safeJsonParse } from "~/utils/json";
@@ -23,6 +23,7 @@ import { TaskRun } from "@trigger.dev/database";
import { PerformTaskAttemptAlertsService } from "./alerts/performTaskAttemptAlerts.server";
import { RetryAttemptService } from "./retryAttempt.server";
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
type FoundAttempt = Awaited<ReturnType<typeof findAttempt>>;
@@ -50,17 +51,30 @@ export class CompleteAttemptService extends BaseService {
id: execution.attempt.id,
});
// Update the task run to be failed
await this._prisma.taskRun.update({
const run = await this._prisma.taskRun.findFirst({
where: {
friendlyId: execution.run.id,
},
data: {
status: "SYSTEM_FAILURE",
completedAt: new Date(),
select: {
id: true,
},
});
if (!run) {
logger.error("[CompleteAttemptService] Task run not found", {
friendlyId: execution.run.id,
});
return "COMPLETED";
}
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: run.id,
status: "SYSTEM_FAILURE",
completedAt: new Date(),
});
// No attempt, so there's no message to ACK
return "COMPLETED";
}
@@ -96,29 +110,26 @@ export class CompleteAttemptService extends BaseService {
taskRunAttempt: NonNullable<FoundAttempt>,
env?: AuthenticatedEnvironment
): Promise<"COMPLETED"> {
await this._prisma.taskRunAttempt.update({
where: { id: taskRunAttempt.id },
data: {
status: "COMPLETED",
completedAt: new Date(),
output: completion.output,
outputType: completion.outputType,
usageDurationMs: completion.usage?.durationMs,
taskRun: {
update: {
data: {
status: "COMPLETED_SUCCESSFULLY",
completedAt: new Date(),
},
},
await $transaction(this._prisma, async (tx) => {
await tx.taskRunAttempt.update({
where: { id: taskRunAttempt.id },
data: {
status: "COMPLETED",
completedAt: new Date(),
output: completion.output,
outputType: completion.outputType,
usageDurationMs: completion.usage?.durationMs,
},
},
});
const finalizeService = new FinalizeTaskRunService(tx);
await finalizeService.call({
id: taskRunAttempt.taskRunId,
status: "COMPLETED_SUCCESSFULLY",
completedAt: new Date(),
});
});
logger.debug("Completed attempt successfully, ACKing message");
await marqs?.acknowledgeMessage(taskRunAttempt.taskRunId);
// Now we need to "complete" the task run event/span
await eventRepository.completeEvent(taskRunAttempt.taskRun.spanId, {
endTime: new Date(),
@@ -255,19 +266,13 @@ export class CompleteAttemptService extends BaseService {
if (!checkpointCreateResult) {
logger.error("Failed to create checkpoint", { checkpoint, execution: execution.run.id });
// Update the task run to be failed
await this._prisma.taskRun.update({
where: {
friendlyId: execution.run.id,
},
data: {
status: "SYSTEM_FAILURE",
completedAt: new Date(),
},
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: taskRunAttempt.taskRunId,
status: "SYSTEM_FAILURE",
completedAt: new Date(),
});
await marqs?.acknowledgeMessage(taskRunAttempt.taskRunId);
return "COMPLETED";
}
@@ -279,11 +284,6 @@ export class CompleteAttemptService extends BaseService {
return "RETRIED";
} else {
// No more retries, we need to fail the task run
logger.debug("Completed attempt, ACKing message", taskRunAttempt);
await marqs?.acknowledgeMessage(taskRunAttempt.taskRunId);
// Now we need to "complete" the task run event/span
await eventRepository.completeEvent(taskRunAttempt.taskRun.spanId, {
endTime: new Date(),
@@ -305,6 +305,13 @@ export class CompleteAttemptService extends BaseService {
sanitizedError.type === "INTERNAL_ERROR" &&
sanitizedError.code === "GRACEFUL_EXIT_TIMEOUT"
) {
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: taskRunAttempt.taskRunId,
status: "SYSTEM_FAILURE",
completedAt: new Date(),
});
// We need to fail all incomplete spans
const inProgressEvents = await eventRepository.queryIncompleteEvents({
attemptId: execution.attempt.id,
@@ -328,25 +335,12 @@ export class CompleteAttemptService extends BaseService {
});
})
);
await this._prisma.taskRun.update({
where: {
id: taskRunAttempt.taskRunId,
},
data: {
status: "SYSTEM_FAILURE",
completedAt: new Date(),
},
});
} else {
await this._prisma.taskRun.update({
where: {
id: taskRunAttempt.taskRunId,
},
data: {
status: "COMPLETED_WITH_ERRORS",
completedAt: new Date(),
},
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: taskRunAttempt.taskRunId,
status: "COMPLETED_WITH_ERRORS",
completedAt: new Date(),
});
}
@@ -7,6 +7,7 @@ import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { ResumeTaskRunDependenciesService } from "./resumeTaskRunDependencies.server";
import { CRASHABLE_ATTEMPT_STATUSES, isCrashableRunStatus } from "../taskStatus";
import { sanitizeError } from "@trigger.dev/core/v3";
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
export type CrashTaskRunServiceOptions = {
reason?: string;
@@ -43,18 +44,11 @@ export class CrashTaskRunService extends BaseService {
return;
}
// Remove the task run from the queue if it's there for some reason
await marqs?.acknowledgeMessage(taskRun.id);
// Set the task run status to crashed
const crashedTaskRun = await this._prisma.taskRun.update({
where: {
id: taskRun.id,
},
data: {
status: "CRASHED",
completedAt: new Date(),
},
const finalizeService = new FinalizeTaskRunService();
const crashedTaskRun = await finalizeService.call({
id: taskRun.id,
status: "CRASHED",
completedAt: new Date(),
include: {
attempts: {
where: {
@@ -1,7 +1,7 @@
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { BaseService } from "./baseService.server";
import { eventRepository } from "../eventRepository.server";
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
export class ExpireEnqueuedRunService extends BaseService {
public async call(runId: string) {
@@ -39,15 +39,12 @@ export class ExpireEnqueuedRunService extends BaseService {
run,
});
await this._prisma.taskRun.update({
where: {
id: run.id,
},
data: {
status: "EXPIRED",
expiredAt: new Date(),
completedAt: new Date(),
},
const finalizeService = new FinalizeTaskRunService();
await finalizeService.call({
id: run.id,
status: "EXPIRED",
expiredAt: new Date(),
completedAt: new Date(),
});
await eventRepository.completeEvent(run.spanId, {
@@ -67,7 +64,5 @@ export class ExpireEnqueuedRunService extends BaseService {
},
],
});
await marqs?.acknowledgeMessage(run.id);
}
}
@@ -0,0 +1,59 @@
import { type Prisma, type TaskRun } from "@trigger.dev/database";
import { type FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { BaseService } from "./baseService.server";
type BaseInput = {
id: string;
status?: FINISHED_STATUSES;
expiredAt?: Date;
completedAt?: Date;
};
type InputWithInclude<T extends Prisma.TaskRunInclude> = BaseInput & {
include: T;
};
type InputWithoutInclude = BaseInput & {
include?: undefined;
};
type Output<T extends Prisma.TaskRunInclude | undefined> = T extends Prisma.TaskRunInclude
? Prisma.TaskRunGetPayload<{ include: T }>
: TaskRun;
export class FinalizeTaskRunService extends BaseService {
public async call<T extends Prisma.TaskRunInclude | undefined>({
id,
status,
expiredAt,
completedAt,
include,
}: T extends Prisma.TaskRunInclude ? InputWithInclude<T> : InputWithoutInclude): Promise<
Output<T>
> {
logger.debug("Finalizing run marqs ack", {
id,
status,
expiredAt,
completedAt,
});
await marqs?.acknowledgeMessage(id);
logger.debug("Finalizing run updating run status", {
id,
status,
expiredAt,
completedAt,
});
const run = await this._prisma.taskRun.update({
where: { id },
data: { status, expiredAt, completedAt },
...(include ? { include } : {}),
});
return run as Output<T>;
}
}