Files
triggerdotdev--trigger.dev/apps/webapp/app/v3/services/batchTriggerTask.server.ts
Eric Allam 4f95c9de4e v3: Cancel awaited subtasks and reliable rate-limit recovery (#1200)
* v3: cancel subtasks when parent task runs are cancelled

* v3: recover from server rate limiting errors in a more reliable way

- Changing from sliding window to token bucket in the API rate limiter, to help smooth out traffic
- Adding spans to the API Client core & SDK functions
- Added waiting spans when retrying in the API Client
- Retrying in the API Client now respects the x-ratelimit-reset
- Retrying ApiError’s in tasks now respects the x-ratelimit-reset
- Added AbortTaskRunError that when thrown will stop retries
- Added idempotency keys SDK functions and automatically injecting the run ID when inside a task
- Added the ability to configure ApiRequestOptions (retries only for now) globally and on specific calls
- Implement the maxAttempts TaskRunOption (it wasn’t doing anything before)

* Adding some docs about the request options

* Fix type error

* Remove context propagation through graphile jobs

* Remove logger

* only select a subset of task run columns

* limit columns selected in batchTrigger as well

* added idempotency doc

* allow scoped idempotency keys, and fixed an issue with the unique index on BatchTaskRun and TaskRun

* Removed old cancel task run children code
2024-07-05 10:30:52 +01:00

153 lines
4.7 KiB
TypeScript

import { BatchTriggerTaskRequestBody, logger } from "@trigger.dev/core/v3";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { TriggerTaskService } from "./triggerTask.server";
import { batchTaskRunItemStatusForRunStatus } from "~/models/taskRun.server";
import { isFinalAttemptStatus, isFinalRunStatus } from "../taskStatus";
export type BatchTriggerTaskServiceOptions = {
idempotencyKey?: string;
triggerVersion?: string;
traceContext?: Record<string, string | undefined>;
spanParentAsLink?: boolean;
};
export class BatchTriggerTaskService extends BaseService {
public async call(
taskId: string,
environment: AuthenticatedEnvironment,
body: BatchTriggerTaskRequestBody,
options: BatchTriggerTaskServiceOptions = {}
) {
return await this.traceWithEnv("call()", environment, async (span) => {
span.setAttribute("taskId", taskId);
const existingBatch = options.idempotencyKey
? await this._prisma.batchTaskRun.findUnique({
where: {
runtimeEnvironmentId_taskIdentifier_idempotencyKey: {
runtimeEnvironmentId: environment.id,
idempotencyKey: options.idempotencyKey,
taskIdentifier: taskId,
},
},
include: {
items: {
include: {
taskRun: {
select: {
friendlyId: true,
},
},
},
},
},
})
: undefined;
if (existingBatch) {
span.setAttribute("batchId", existingBatch.friendlyId);
return {
batch: existingBatch,
runs: existingBatch.items.map((item) => item.taskRun.friendlyId),
};
}
const dependentAttempt = body?.dependentAttempt
? await this._prisma.taskRunAttempt.findUnique({
where: { friendlyId: body.dependentAttempt },
include: {
taskRun: {
select: {
id: true,
status: true,
},
},
},
})
: undefined;
if (
dependentAttempt &&
(isFinalAttemptStatus(dependentAttempt.status) ||
isFinalRunStatus(dependentAttempt.taskRun.status))
) {
logger.debug("Dependent attempt or run is in a terminal state", {
dependentAttempt: dependentAttempt,
});
if (isFinalAttemptStatus(dependentAttempt.status)) {
throw new ServiceValidationError(
`Cannot batch trigger ${taskId} as the parent attempt has a status of ${dependentAttempt.status}`
);
} else {
throw new ServiceValidationError(
`Cannot batch trigger ${taskId} as the parent run has a status of ${dependentAttempt.taskRun.status}`
);
}
}
const batch = await this._prisma.batchTaskRun.create({
data: {
friendlyId: generateFriendlyId("batch"),
runtimeEnvironmentId: environment.id,
idempotencyKey: options.idempotencyKey,
taskIdentifier: taskId,
dependentTaskAttemptId: dependentAttempt?.id,
},
});
const triggerTaskService = new TriggerTaskService();
const runs: string[] = [];
let index = 0;
for (const item of body.items) {
try {
const run = await triggerTaskService.call(
taskId,
environment,
{
...item,
options: {
...item.options,
dependentBatch: dependentAttempt?.id ? batch.friendlyId : undefined, // Only set dependentBatch if dependentAttempt is set which means batchTriggerAndWait was called
},
},
{
triggerVersion: options.triggerVersion,
traceContext: options.traceContext,
spanParentAsLink: options.spanParentAsLink,
batchId: batch.friendlyId,
}
);
if (run) {
await this._prisma.batchTaskRunItem.create({
data: {
batchTaskRunId: batch.id,
taskRunId: run.id,
status: batchTaskRunItemStatusForRunStatus(run.status),
},
});
runs.push(run.friendlyId);
}
index++;
} catch (error) {
logger.error("[BatchTriggerTaskService] Error triggering task", {
taskId,
error,
});
}
}
span.setAttribute("batchId", batch.friendlyId);
return { batch, runs };
});
}
}