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

211 lines
6.0 KiB
TypeScript

import { type Prisma } from "@trigger.dev/database";
import assertNever from "assert-never";
import { logger } from "~/services/logger.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 { CancelTaskAttemptDependenciesService } from "./cancelTaskAttemptDependencies.server";
import type { CancelableTaskRun } from "./cancelTaskRun.server";
import { FinalizeTaskRunService } from "./finalizeTaskRun.server";
import { tryCatch } from "@trigger.dev/core/utils";
import { getEventRepositoryForStore } from "../eventRepository/index.server";
type ExtendedTaskRun = Prisma.TaskRunGetPayload<{
include: {
runtimeEnvironment: true;
lockedToVersion: true;
};
}>;
type ExtendedTaskRunAttempt = Prisma.TaskRunAttemptGetPayload<{
include: {
backgroundWorker: true;
};
}>;
export type CancelTaskRunServiceOptions = {
reason?: string;
cancelAttempts?: boolean;
cancelledAt?: Date;
bulkActionId?: string;
};
export class CancelTaskRunServiceV1 extends BaseService {
public async call(taskRun: CancelableTaskRun, options?: CancelTaskRunServiceOptions) {
const opts = {
reason: "Task run was cancelled by user",
cancelAttempts: true,
cancelledAt: new Date(),
...options,
};
// Make sure the task run is in a cancellable state
if (!isCancellableRunStatus(taskRun.status)) {
logger.info("Task run is not in a cancellable state", {
runId: taskRun.id,
status: taskRun.status,
});
//add the bulk action id to the run
if (opts.bulkActionId) {
await this._prisma.taskRun.update({
where: { id: taskRun.id },
data: {
bulkActionGroupIds: {
push: opts.bulkActionId,
},
},
});
}
return;
}
const finalizeService = new FinalizeTaskRunService();
const cancelledTaskRun = await finalizeService.call({
id: taskRun.id,
status: "CANCELED",
completedAt: opts.cancelledAt,
bulkActionId: opts.bulkActionId,
include: {
attempts: {
where: {
status: {
in: CANCELLABLE_ATTEMPT_STATUSES,
},
},
include: {
backgroundWorker: true,
dependencies: {
include: {
taskRun: true,
},
},
batchTaskRunItems: {
include: {
taskRun: true,
},
},
},
},
runtimeEnvironment: true,
lockedToVersion: true,
project: true,
},
attemptStatus: "CANCELED",
error: {
type: "STRING_ERROR",
raw: opts.reason,
},
});
const eventRepository = await getEventRepositoryForStore(
cancelledTaskRun.taskEventStore,
cancelledTaskRun.runtimeEnvironment.organizationId
);
const [cancelRunEventError] = await tryCatch(
eventRepository.cancelRunEvent({
reason: opts.reason,
run: cancelledTaskRun,
cancelledAt: opts.cancelledAt,
})
);
if (cancelRunEventError) {
logger.error("[CancelTaskRunServiceV1] Failed to cancel run event", {
error: cancelRunEventError,
runId: cancelledTaskRun.id,
});
}
// Cancel any in progress attempts
if (opts.cancelAttempts) {
await this.#cancelPotentiallyRunningAttempts(cancelledTaskRun, cancelledTaskRun.attempts);
await this.#cancelRemainingRunWorkers(cancelledTaskRun);
}
return {
id: cancelledTaskRun.id,
};
}
async #cancelPotentiallyRunningAttempts(
run: ExtendedTaskRun,
attempts: ExtendedTaskRunAttempt[]
) {
for (const attempt of attempts) {
await CancelTaskAttemptDependenciesService.enqueue(attempt.id);
if (run.runtimeEnvironment.type === "DEVELOPMENT") {
// Signal the task run attempt to stop
await devPubSub.publish(
`backgroundWorker:${attempt.backgroundWorkerId}:${attempt.id}`,
"CANCEL_ATTEMPT",
{
attemptId: attempt.friendlyId,
backgroundWorkerId: attempt.backgroundWorker.friendlyId,
taskRunId: run.friendlyId,
}
);
} else {
switch (attempt.status) {
case "EXECUTING": {
// We need to send a cancel message to the coordinator
socketIo.coordinatorNamespace.emit("REQUEST_ATTEMPT_CANCELLATION", {
version: "v1",
attemptId: attempt.id,
attemptFriendlyId: attempt.friendlyId,
});
break;
}
case "PENDING":
case "PAUSED": {
logger.debug("Cancelling pending or paused attempt", {
attempt,
});
const service = new CancelAttemptService();
await service.call(
attempt.friendlyId,
run.id,
new Date(),
"Task run was cancelled by user"
);
break;
}
case "CANCELED":
case "COMPLETED":
case "FAILED": {
// Do nothing
break;
}
default: {
assertNever(attempt.status);
}
}
}
}
}
async #cancelRemainingRunWorkers(run: ExtendedTaskRun) {
if (run.runtimeEnvironment.type === "DEVELOPMENT") {
// Nothing to do
return;
}
// Broadcast cancel message to all coordinators
socketIo.coordinatorNamespace.emit("REQUEST_RUN_CANCELLATION", {
version: "v1",
runId: run.id,
// Give the attempts some time to exit gracefully. If the runs supports lazy attempts, it also supports exit delays.
delayInMs: run.lockedToVersion?.supportsLazyAttempts ? 5_000 : undefined,
});
}
}