v3: handle errors and customizing retrying (#943)

* Add the ability to handle errors at the task and project level

* Cancel in progress runs when disconnecting the dev CLI (in between attempts)

* unify the task executor across dev & prod
This commit is contained in:
Eric Allam
2024-03-14 14:38:13 +00:00
committed by GitHub
parent 819b663ad7
commit 0a33bf7206
27 changed files with 745 additions and 499 deletions
@@ -1,25 +1,9 @@
import { VirtualItem, Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
import {
Fragment,
MutableRefObject,
RefObject,
useCallback,
useEffect,
useImperativeHandle,
useReducer,
useRef,
useState,
} from "react";
import { MutableRefObject, RefObject, useCallback, useEffect, useReducer, useRef } from "react";
import { UnmountClosed } from "react-collapse";
import { cn } from "~/utils/cn";
import { Changes, NodeState, NodesState, reducer } from "./reducer";
import {
applyFilterToState,
concreteStateFromInput,
firstVisibleNode,
lastVisibleNode,
selectedIdFromState,
} from "./utils";
import { NodeState, NodesState, reducer } from "./reducer";
import { applyFilterToState, concreteStateFromInput, selectedIdFromState } from "./utils";
export type TreeViewProps<TData> = {
tree: FlatTree<TData>;
@@ -232,6 +216,7 @@ export function useTree<TData>({
index,
});
},
overscan: 20,
});
const scrollToNodeFn = useCallback(
+1
View File
@@ -10,3 +10,4 @@ export const RUN_CHUNK_EXECUTION_BUFFER = 350;
export const MAX_RUN_CHUNK_EXECUTION_LIMIT = 120000; // 2 minutes
export const VERCEL_RESPONSE_TIMEOUT_STATUS_CODES = [408, 504];
export const MAX_BATCH_TRIGGER_ITEMS = 100;
export const MAX_TASK_RUN_ATTEMPTS = 250;
+4 -4
View File
@@ -611,14 +611,14 @@ export class MarQS {
String(this.options.defaultConcurrency ?? 10)
);
logger.debug("Dequeue message result", {
result,
});
if (!result) {
return;
}
logger.debug("Dequeue message result", {
result,
});
if (result.length !== 2) {
return;
}
@@ -18,6 +18,7 @@ import { CancelAttemptService } from "../services/cancelAttempt.server";
import { CompleteAttemptService } from "../services/completeAttempt.server";
import { attributesFromAuthenticatedEnv } from "../tracer.server";
import { DevSubscriber, devPubSub } from "./devPubSub.server";
import { CancelTaskRunService } from "../services/cancelTaskRun.server";
const tracer = trace.getTracer("devQueueConsumer");
@@ -50,6 +51,7 @@ export class DevQueueConsumer {
private _currentSpan: Span | undefined;
private _endSpanInNextIteration = false;
private _inProgressAttempts: Map<string, string> = new Map(); // Keys are task attempt friendly IDs, values are TaskRun ids/queue message ids
private _inProgressRuns: Map<string, string> = new Map(); // Keys are task run friendly IDs, values are TaskRun internal ids/queue message ids
constructor(
public env: AuthenticatedEnvironment,
@@ -123,7 +125,11 @@ export class DevQueueConsumer {
logger.debug("Task run completed", { taskRunCompletion: completion, execution });
const service = new CompleteAttemptService();
await service.call(completion, execution, this.env);
const result = await service.call(completion, execution, this.env);
if (result === "COMPLETED") {
this._inProgressRuns.delete(execution.run.id);
}
}
public async taskHeartbeat(workerId: string, id: string, seconds: number = 60) {
@@ -148,7 +154,7 @@ export class DevQueueConsumer {
this._enabled = false;
// We need to cancel all the in progress task run attempts and ack the messages so they will stop processing
await this.#cancelInProgressAttempts(reason);
await this.#cancelInProgressRunsAndAttempts(reason);
// We need to unsubscribe from the background worker channels
for (const [id, subscriber] of this._backgroundWorkerSubscriber) {
@@ -161,21 +167,44 @@ export class DevQueueConsumer {
}
}
async #cancelInProgressAttempts(reason: string) {
const service = new CancelAttemptService();
async #cancelInProgressRunsAndAttempts(reason: string) {
const cancelAttemptService = new CancelAttemptService();
const cancelTaskRunService = new CancelTaskRunService();
const cancelledAt = new Date();
const inProgressAttempts = new Map(this._inProgressAttempts);
const inProgressRuns = new Map(this._inProgressRuns);
this._inProgressAttempts.clear();
this._inProgressRuns.clear();
logger.debug("Cancelling in progress attempts", {
const inProgressRunsWithNoInProgressAttempts: string[] = [];
const inProgressAttemptRunIds = new Set(inProgressAttempts.values());
for (const [runId, messageId] of inProgressRuns) {
if (!inProgressAttemptRunIds.has(messageId)) {
inProgressRunsWithNoInProgressAttempts.push(messageId);
}
}
logger.debug("Cancelling in progress runs and attempts", {
attempts: Array.from(inProgressAttempts.keys()),
runs: Array.from(inProgressRuns.keys()),
});
for (const [attemptId, messageId] of inProgressAttempts) {
await this.#cancelInProgressAttempt(attemptId, messageId, service, cancelledAt, reason);
await this.#cancelInProgressAttempt(
attemptId,
messageId,
cancelAttemptService,
cancelledAt,
reason
);
}
for (const runId of inProgressRunsWithNoInProgressAttempts) {
await this.#cancelInProgressRun(runId, cancelTaskRunService, cancelledAt, reason);
}
}
@@ -199,6 +228,32 @@ export class DevQueueConsumer {
}
}
async #cancelInProgressRun(
runId: string,
service: CancelTaskRunService,
cancelledAt: Date,
reason: string
) {
logger.debug("Cancelling in progress run", { runId });
const taskRun = await prisma.taskRun.findUnique({
where: { id: runId },
});
if (!taskRun) {
return;
}
try {
await service.call(taskRun, { reason, cancelAttempts: false, cancelledAt });
} catch (e) {
logger.error("Failed to cancel in progress run", {
runId,
error: e,
});
}
}
#enable() {
if (this._enabled) {
return;
@@ -481,6 +536,7 @@ export class DevQueueConsumer {
});
this._inProgressAttempts.set(taskRunAttempt.friendlyId, message.messageId);
this._inProgressRuns.set(lockedTaskRun.friendlyId, message.messageId);
} catch (e) {
if (e instanceof Error) {
this._currentSpan?.recordException(e);
@@ -499,6 +555,7 @@ export class DevQueueConsumer {
data: {
lockedAt: null,
lockedById: null,
status: "PENDING",
},
}),
prisma.taskRunAttempt.delete({
@@ -508,6 +565,9 @@ export class DevQueueConsumer {
}),
]);
this._inProgressAttempts.delete(taskRunAttempt.friendlyId);
this._inProgressRuns.delete(lockedTaskRun.friendlyId);
// Finally we need to nack the message so it can be retried
await marqs?.nackMessage(message.messageId);
} finally {
@@ -300,7 +300,10 @@ export class SharedQueueConsumer {
return;
}
if (existingTaskRun.status !== "PENDING") {
if (
existingTaskRun.status !== "PENDING" &&
existingTaskRun.status !== "RETRYING_AFTER_FAILURE"
) {
logger.debug("Task run is not pending, aborting", {
queueMessage: message.data,
messageId: message.messageId,
@@ -23,8 +23,21 @@ const CANCELLABLE_ATTEMPT_STATUSES: Array<TaskRunAttemptStatus> = [
"PENDING",
];
export type CancelTaskRunServiceOptions = {
reason?: string;
cancelAttempts?: boolean;
cancelledAt?: Date;
};
export class CancelTaskRunService extends BaseService {
public async call(taskRun: TaskRun) {
public async call(taskRun: TaskRun, 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 (!CANCELLABLE_STATUSES.includes(taskRun.status)) {
return;
@@ -68,59 +81,61 @@ export class CancelTaskRunService extends BaseService {
await Promise.all(
inProgressEvents.map((event) => {
return eventRepository.cancelEvent(event, new Date(), "Task run was cancelled by user");
return eventRepository.cancelEvent(event, opts.cancelledAt, opts.reason);
})
);
// Cancel any in progress attempts
for (const attempt of cancelledTaskRun.attempts) {
if (attempt.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: cancelledTaskRun.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,
});
if (opts.cancelAttempts) {
for (const attempt of cancelledTaskRun.attempts) {
if (attempt.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: cancelledTaskRun.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,
});
break;
}
case "PENDING":
case "PAUSED": {
logger.debug("Cancelling pending or paused attempt", {
attempt,
});
break;
}
case "PENDING":
case "PAUSED": {
logger.debug("Cancelling pending or paused attempt", {
attempt,
});
const service = new CancelAttemptService();
const service = new CancelAttemptService();
await service.call(
attempt.friendlyId,
taskRun.id,
new Date(),
"Task run was cancelled by user"
);
await service.call(
attempt.friendlyId,
taskRun.id,
new Date(),
"Task run was cancelled by user"
);
break;
}
case "CANCELED":
case "COMPLETED":
case "FAILED": {
// Do nothing
break;
}
default: {
assertUnreachable(attempt.status);
break;
}
case "CANCELED":
case "COMPLETED":
case "FAILED": {
// Do nothing
break;
}
default: {
assertUnreachable(attempt.status);
}
}
}
}
@@ -1,12 +1,10 @@
import { Attributes } from "@opentelemetry/api";
import {
RetryOptions,
TaskRunContext,
TaskRunExecution,
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
TaskRunSuccessfulExecutionResult,
defaultRetryOptions,
flattenAttributes,
} from "@trigger.dev/core/v3";
import { PrismaClientOrTransaction } from "~/db.server";
@@ -16,8 +14,9 @@ import { safeJsonParse } from "~/utils/json";
import { eventRepository } from "../eventRepository.server";
import { marqs } from "../marqs.server";
import { BaseService } from "./baseService.server";
import { ResumeTaskRunDependenciesService } from "./resumeTaskRunDependencies.server";
import { CancelAttemptService } from "./cancelAttempt.server";
import { ResumeTaskRunDependenciesService } from "./resumeTaskRunDependencies.server";
import { MAX_TASK_RUN_ATTEMPTS } from "~/consts";
type FoundAttempt = Awaited<ReturnType<typeof findAttempt>>;
@@ -26,7 +25,7 @@ export class CompleteAttemptService extends BaseService {
completion: TaskRunExecutionResult,
execution: TaskRunExecution,
env?: AuthenticatedEnvironment
) {
): Promise<"COMPLETED" | "RETRIED"> {
const taskRunAttempt = await findAttempt(this._prisma, completion.id);
if (!taskRunAttempt) {
@@ -42,7 +41,7 @@ export class CompleteAttemptService extends BaseService {
},
});
return "FAILED";
return "COMPLETED";
}
if (completion.ok) {
@@ -56,7 +55,7 @@ export class CompleteAttemptService extends BaseService {
completion: TaskRunSuccessfulExecutionResult,
taskRunAttempt: NonNullable<FoundAttempt>,
env?: AuthenticatedEnvironment
) {
): Promise<"COMPLETED" | "RETRIED"> {
await this._prisma.taskRunAttempt.update({
where: { friendlyId: completion.id },
data: {
@@ -91,7 +90,7 @@ export class CompleteAttemptService extends BaseService {
await ResumeTaskRunDependenciesService.enqueue(taskRunAttempt.id, this._prisma);
}
return "ACKNOWLEDGED";
return "COMPLETED";
}
async #completeAttemptFailed(
@@ -107,13 +106,15 @@ export class CompleteAttemptService extends BaseService {
// We need to cancel the task run instead of fail it
const cancelService = new CancelAttemptService();
return await cancelService.call(
await cancelService.call(
taskRunAttempt.friendlyId,
taskRunAttempt.taskRunId,
new Date(),
"Cancelled by user",
env
);
return "COMPLETED";
}
await this._prisma.taskRunAttempt.update({
@@ -125,48 +126,31 @@ export class CompleteAttemptService extends BaseService {
},
});
if (completion.retry !== undefined) {
const retryConfig = taskRunAttempt.backgroundWorkerTask.retryConfig
? {
...defaultRetryOptions,
...RetryOptions.parse(taskRunAttempt.backgroundWorkerTask.retryConfig),
}
: undefined;
if (completion.retry !== undefined && taskRunAttempt.number < MAX_TASK_RUN_ATTEMPTS) {
const environment = env ?? (await this.#getEnvironment(execution.environment.id));
const retryAt = new Date(completion.retry.timestamp);
// Retry the task run
await eventRepository.recordEvent(
retryConfig?.maxAttempts
? `Retry ${execution.attempt.number}/${retryConfig?.maxAttempts - 1} delay`
: `Retry #${execution.attempt.number} delay`,
{
taskSlug: taskRunAttempt.taskRun.taskIdentifier,
environment,
attributes: {
metadata: this.#generateMetadataAttributesForNextAttempt(execution),
properties: {
retryAt: retryAt.toISOString(),
factor: retryConfig?.factor,
maxAttempts: retryConfig?.maxAttempts,
minTimeoutInMs: retryConfig?.minTimeoutInMs,
maxTimeoutInMs: retryConfig?.maxTimeoutInMs,
randomize: retryConfig?.randomize,
},
runId: taskRunAttempt.taskRunId,
style: {
icon: "schedule-attempt",
},
queueId: taskRunAttempt.queueId,
queueName: taskRunAttempt.taskRun.queue,
await eventRepository.recordEvent(`Retry #${execution.attempt.number} delay`, {
taskSlug: taskRunAttempt.taskRun.taskIdentifier,
environment,
attributes: {
metadata: this.#generateMetadataAttributesForNextAttempt(execution),
properties: {
retryAt: retryAt.toISOString(),
},
context: taskRunAttempt.taskRun.traceContext as Record<string, string | undefined>,
spanIdSeed: `retry-${taskRunAttempt.number + 1}`,
endTime: retryAt,
}
);
runId: taskRunAttempt.taskRunId,
style: {
icon: "schedule-attempt",
},
queueId: taskRunAttempt.queueId,
queueName: taskRunAttempt.taskRun.queue,
},
context: taskRunAttempt.taskRun.traceContext as Record<string, string | undefined>,
spanIdSeed: `retry-${taskRunAttempt.number + 1}`,
endTime: retryAt,
});
logger.debug("Retrying", { taskRun: taskRunAttempt.taskRun.friendlyId });
@@ -222,7 +206,7 @@ export class CompleteAttemptService extends BaseService {
await ResumeTaskRunDependenciesService.enqueue(taskRunAttempt.id, this._prisma);
}
return "ACKNOWLEDGED";
return "COMPLETED";
}
}
+38 -14
View File
@@ -144,17 +144,17 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
await printStandloneInitialBanner(true);
const { config } = await readConfig(dir, {
const resolvedConfig = await readConfig(dir, {
configFile: options.data.config,
projectRef: options.data.projectRef,
});
logger.debug("Resolved config", { config });
logger.debug("Resolved config", { resolvedConfig });
const apiClient = new CliApiClient(authorization.config.apiUrl, authorization.config.accessToken);
const deploymentEnv = await apiClient.getProjectEnv({
projectRef: config.project,
projectRef: resolvedConfig.config.project,
env: options.data.env,
});
@@ -168,11 +168,15 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
);
intro(
`Preparing to deploy "${deploymentEnv.data.name}" (${config.project}) to ${options.data.env}`
`Preparing to deploy "${deploymentEnv.data.name}" (${resolvedConfig.config.project}) to ${options.data.env}`
);
// Step 1: Build the project into a temporary directory
const compilation = await compileProject(config, options.data);
const compilation = await compileProject(
resolvedConfig.config,
options.data,
resolvedConfig.status === "file" ? resolvedConfig.path : undefined
);
logger.debug("Compilation result", { compilation });
@@ -180,7 +184,9 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
environmentVariablesSpinner.start("Checking environment variables");
const environmentVariables = await environmentClient.getEnvironmentVariables(config.project);
const environmentVariables = await environmentClient.getEnvironmentVariables(
resolvedConfig.config.project
);
if (!environmentVariables.success) {
environmentVariablesSpinner.stop(`Failed to fetch environment variables, skipping check`);
@@ -197,7 +203,7 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
)}. Aborting deployment. ${chalk.bgBlueBright(
terminalLink(
"Manage env vars",
`${authorization.config.apiUrl}/projects/v3/${config.project}/environment-variables`
`${authorization.config.apiUrl}/projects/v3/${resolvedConfig.config.project}/environment-variables`
)
)}`
);
@@ -239,11 +245,11 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
return buildAndPushSelfHostedImage({
imageTag: deploymentResponse.data.imageTag,
cwd: compilation.path,
projectId: config.project,
projectId: resolvedConfig.config.project,
deploymentId: deploymentResponse.data.id,
deploymentVersion: version,
contentHash: deploymentResponse.data.contentHash,
projectRef: config.project,
projectRef: resolvedConfig.config.project,
buildPlatform: options.data.buildPlatform,
});
}
@@ -263,11 +269,11 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
buildToken: deploymentResponse.data.externalBuildData.buildToken,
buildProjectId: deploymentResponse.data.externalBuildData.projectId,
cwd: compilation.path,
projectId: config.project,
projectId: resolvedConfig.config.project,
deploymentId: deploymentResponse.data.id,
deploymentVersion: deploymentResponse.data.version,
contentHash: deploymentResponse.data.contentHash,
projectRef: config.project,
projectRef: resolvedConfig.config.project,
loadImage: options.data.loadImage,
buildPlatform: options.data.buildPlatform,
});
@@ -321,7 +327,7 @@ export async function deployCommand(dir: string, anyOptions: unknown) {
const deploymentLink = terminalLink(
"View deployment",
`${authorization.config.apiUrl}/projects/v3/${config.project}/deployments/${finishedDeployment.id}`
`${authorization.config.apiUrl}/projects/v3/${resolvedConfig.config.project}/deployments/${finishedDeployment.id}`
);
switch (finishedDeployment.status) {
@@ -584,7 +590,11 @@ function extractImageDigest(outputs: string[]) {
}
}
async function compileProject(config: ResolvedConfig, options: DeployCommandOptions) {
async function compileProject(
config: ResolvedConfig,
options: DeployCommandOptions,
configPath?: string
) {
if (!options.skipTypecheck) {
await typecheckProject(config, options);
}
@@ -605,10 +615,24 @@ async function compileProject(config: ResolvedConfig, options: DeployCommandOpti
importResolve("./workers/prod/worker-setup.js", import.meta.url)
).href.replace("file://", "");
const workerContents = workerFacade
let workerContents = workerFacade
.replace("__TASKS__", createTaskFileImports(taskFiles))
.replace("__WORKER_SETUP__", `import { tracingSDK } from "${workerSetupPath}";`);
if (configPath) {
logger.debug("Importing project config from", { configPath });
workerContents = workerContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`import importedConfig from "${configPath}";`
);
} else {
workerContents = workerContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`const importedConfig = undefined;`
);
}
const result = await build({
stdin: {
contents: workerContents,
+28 -4
View File
@@ -138,7 +138,7 @@ async function startDev(
if (config.status === "file") {
logger.log(`${basename(config.path)} changed...`);
logger.debug("New config", { config: config.config });
rerender(await getDevReactElement(config.config, authorization));
rerender(await getDevReactElement(config.config, authorization, config.path));
} else {
logger.debug("New config", { config: config.config });
rerender(await getDevReactElement(config.config, authorization));
@@ -148,7 +148,8 @@ async function startDev(
async function getDevReactElement(
configParam: ResolvedConfig,
authorization: { apiUrl: string; accessToken: string }
authorization: { apiUrl: string; accessToken: string },
configPath?: string
) {
const accessToken = authorization.accessToken;
const apiUrl = authorization.apiUrl;
@@ -175,11 +176,18 @@ async function startDev(
projectName={devEnv.data.name}
debuggerOn={options.debugger}
debugOtel={options.debugOtel}
configPath={configPath}
/>
);
}
const devReactElement = render(await getDevReactElement(config.config, authorization));
const devReactElement = render(
await getDevReactElement(
config.config,
authorization,
config.status === "file" ? config.path : undefined
)
);
rerender = devReactElement.rerender;
@@ -205,6 +213,7 @@ type DevProps = {
projectName: string;
debuggerOn: boolean;
debugOtel: boolean;
configPath?: string;
};
function useDev({
@@ -215,6 +224,7 @@ function useDev({
projectName,
debuggerOn,
debugOtel,
configPath,
}: DevProps) {
useEffect(() => {
const websocketUrl = new URL(apiUrl);
@@ -334,10 +344,24 @@ function useDev({
importResolve("./workers/dev/worker-setup.js", import.meta.url)
).href.replace("file://", "");
const entryPointContents = workerFacade
let entryPointContents = workerFacade
.replace("__TASKS__", createTaskFileImports(taskFiles))
.replace("__WORKER_SETUP__", `import { tracingSDK, sender } from "${workerSetupPath}";`);
if (configPath) {
logger.debug("Importing project config from", { configPath });
entryPointContents = entryPointContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`import importedConfig from "${configPath}";`
);
} else {
entryPointContents = entryPointContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`const importedConfig = undefined;`
);
}
let firstBuild = true;
logger.log(chalk.dim("⎔ Building background worker..."));
+18 -5
View File
@@ -1,11 +1,24 @@
import { TaskMetadataWithFilePath } from "@trigger.dev/core/v3";
import {
HandleErrorFnParams,
HandleErrorResult,
InitFnParams,
InitOutput,
MiddlewareFnParams,
RunFnParams,
TaskMetadataWithFilePath,
} from "@trigger.dev/core/v3";
export type TaskMetadataWithFunctions = TaskMetadataWithFilePath & {
fns: {
run: (payload: any, params: any) => Promise<any>;
init?: (payload: any, params: any) => Promise<void>;
cleanup?: (payload: any, params: any) => Promise<void>;
middleware?: (payload: any, params: any) => Promise<void>;
run: (payload: any, params: RunFnParams<any>) => Promise<any>;
init?: (payload: any, params: InitFnParams) => Promise<InitOutput>;
cleanup?: (payload: any, params: RunFnParams<any>) => Promise<void>;
middleware?: (payload: any, params: MiddlewareFnParams) => Promise<void>;
handleError?: (
payload: any,
error: unknown,
params: HandleErrorFnParams<any>
) => HandleErrorResult;
};
};
@@ -366,9 +366,9 @@ export class BackgroundWorker {
const taskRunProcess = new TaskRunProcess(
this.path,
{
...this.#readEnvVars(),
...this.params.env,
...(payload.environment ?? {}),
...this.#readEnvVars(),
},
this.metadata,
this.params
+16 -161
View File
@@ -1,42 +1,36 @@
import { Config, type TracingSDK } from "@trigger.dev/core/v3";
import { Config, ProjectConfig, TaskExecutor, type TracingSDK } from "@trigger.dev/core/v3";
import "source-map-support/register.js";
__WORKER_SETUP__;
declare const __WORKER_SETUP__: unknown;
__IMPORTED_PROJECT_CONFIG__;
declare const __IMPORTED_PROJECT_CONFIG__: unknown;
declare const importedConfig: ProjectConfig | undefined;
declare const __PROJECT_CONFIG__: Config;
declare const tracingSDK: TracingSDK;
const otelTracer = tracingSDK.getTracer("trigger-dev-worker", packageJson.version);
const otelLogger = tracingSDK.getLogger("trigger-dev-worker", packageJson.version);
import { SpanKind } from "@opentelemetry/api";
import {
ConsoleInterceptor,
DevRuntimeManager,
OtelTaskLogger,
SemanticInternalAttributes,
TaskMetadataWithFilePath,
TaskRunContext,
TaskRunErrorCodes,
TaskRunExecution,
TaskRunExecutionRetry,
TriggerTracer,
ZodMessageHandler,
ZodMessageSender,
accessoryAttributes,
calculateNextRetryDelay,
childToWorkerMessages,
logger,
parseError,
runtime,
taskContextManager,
workerToChildMessages,
type BackgroundWorkerProperties,
} from "@trigger.dev/core/v3";
import * as packageJson from "../../../package.json";
import { flattenAttributes } from "@trigger.dev/core/v3";
import { TaskMetadataWithFunctions } from "../../types.js";
declare const sender: ZodMessageSender<typeof childToWorkerMessages>;
@@ -64,137 +58,6 @@ const TaskFiles: Record<string, string> = {};
__TASKS__;
declare const __TASKS__: Record<string, string>;
class TaskExecutor {
constructor(public task: TaskMetadataWithFunctions) {}
async determineRetrying(
execution: TaskRunExecution,
error: unknown
): Promise<TaskRunExecutionRetry | "skipped" | undefined> {
const retry = this.task.retry ?? __PROJECT_CONFIG__.retries?.default;
if (!retry) {
return;
}
const delay = calculateNextRetryDelay(retry, execution.attempt.number);
if (
typeof __PROJECT_CONFIG__.retries?.enabledInDev === "boolean" &&
!__PROJECT_CONFIG__.retries.enabledInDev
) {
// TODO: trigger a warning saying that retries are disabled in dev
return "skipped";
}
return typeof delay === "undefined" ? undefined : { timestamp: Date.now() + delay, delay };
}
async execute(
execution: TaskRunExecution,
worker: BackgroundWorkerProperties,
traceContext: Record<string, unknown>
) {
const parsedPayload = JSON.parse(execution.run.payload);
const ctx = TaskRunContext.parse(execution);
const attemptMessage = `Attempt ${execution.attempt.number}`;
const output = await taskContextManager.runWith(
{
ctx,
payload: parsedPayload,
worker,
},
async () => {
tracingSDK.asyncResourceDetector.resolveWithAttributes({
...taskContextManager.attributes,
[SemanticInternalAttributes.SDK_VERSION]: this.task.packageVersion,
[SemanticInternalAttributes.SDK_LANGUAGE]: "typescript",
});
return await tracer.startActiveSpan(
attemptMessage,
async (span) => {
return await consoleInterceptor.intercept(console, async () => {
const init = await this.#callTaskInit(parsedPayload, ctx);
try {
const output = await this.#callRun(parsedPayload, ctx, init);
span.setAttributes(flattenAttributes(output, SemanticInternalAttributes.OUTPUT));
return output;
} finally {
await this.#callTaskCleanup(parsedPayload, ctx, init);
}
});
},
{
kind: SpanKind.CONSUMER,
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "attempt",
...flattenAttributes(parsedPayload, SemanticInternalAttributes.PAYLOAD),
...accessoryAttributes({
items: [
{
text: ctx.task.filePath,
},
{
text: `${ctx.task.exportName}.run()`,
},
],
style: "codepath",
}),
},
},
tracer.extractContext(traceContext)
);
}
);
return { output: JSON.stringify(output), outputType: "application/json" };
}
async #callRun(payload: unknown, ctx: TaskRunContext, init: unknown) {
const runFn = this.task.fns.run;
const middlewareFn = this.task.fns.middleware;
if (!runFn) {
throw new Error("Task does not have a run function");
}
if (!middlewareFn) {
return runFn(payload, { ctx });
}
return middlewareFn(payload, { ctx, next: async () => runFn(payload, { ctx, init }) });
}
async #callTaskInit(payload: unknown, ctx: TaskRunContext) {
const initFn = this.task.fns.init;
if (!initFn) {
return {};
}
return tracer.startActiveSpan("init", async (span) => {
return await initFn(payload, { ctx });
});
}
async #callTaskCleanup(payload: unknown, ctx: TaskRunContext, init: unknown) {
const cleanupFn = this.task.fns.cleanup;
if (!cleanupFn) {
return;
}
return tracer.startActiveSpan("cleanup", async (span) => {
return await cleanupFn(payload, { ctx, init });
});
}
}
function getTasks(): Array<TaskMetadataWithFunctions> {
const result: Array<TaskMetadataWithFunctions> = [];
@@ -237,7 +100,16 @@ runtime.registerTasks(tasks);
const taskExecutors: Map<string, TaskExecutor> = new Map();
for (const task of tasks) {
taskExecutors.set(task.id, new TaskExecutor(task));
taskExecutors.set(
task.id,
new TaskExecutor(task, {
tracer,
tracingSDK,
consoleInterceptor,
projectConfig: __PROJECT_CONFIG__,
importedConfig,
})
);
}
let _execution: TaskRunExecution | undefined;
@@ -295,24 +167,7 @@ const handler = new ZodMessageHandler({
return sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
id: execution.attempt.id,
ok: true,
...result,
},
});
} catch (e) {
const retryResult = await executor.determineRetrying(execution, e);
return sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
id: execution.attempt.id,
ok: false,
error: parseError(e),
retry: typeof retryResult === "object" ? retryResult : undefined,
skippedRetrying: retryResult === "skipped",
},
result,
});
} finally {
_execution = undefined;
+19 -150
View File
@@ -1,43 +1,40 @@
import {
Config,
ProdChildToWorkerMessages,
ProdWorkerToChildMessages,
ProjectConfig,
TaskExecutor,
ZodIpcConnection,
type TracingSDK,
Config,
} from "@trigger.dev/core/v3";
import "source-map-support/register.js";
__WORKER_SETUP__;
declare const __WORKER_SETUP__: unknown;
__IMPORTED_PROJECT_CONFIG__;
declare const __IMPORTED_PROJECT_CONFIG__: unknown;
declare const importedConfig: ProjectConfig | undefined;
declare const __PROJECT_CONFIG__: Config;
declare const tracingSDK: TracingSDK;
const otelTracer = tracingSDK.getTracer("trigger-prod-worker", packageJson.version);
const otelLogger = tracingSDK.getLogger("trigger-prod-worker", packageJson.version);
import { SpanKind } from "@opentelemetry/api";
import {
ConsoleInterceptor,
OtelTaskLogger,
ProdRuntimeManager,
SemanticInternalAttributes,
TaskMetadataWithFilePath,
TaskRunContext,
TaskRunErrorCodes,
TaskRunExecution,
TaskRunExecutionRetry,
TriggerTracer,
accessoryAttributes,
calculateNextRetryDelay,
logger,
parseError,
runtime,
taskContextManager,
type BackgroundWorkerProperties,
} from "@trigger.dev/core/v3";
import * as packageJson from "../../../package.json";
import { flattenAttributes } from "@trigger.dev/core/v3";
import { TaskMetadataWithFunctions } from "../../types";
const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger });
@@ -59,129 +56,6 @@ const TaskFiles: Record<string, string> = {};
__TASKS__;
declare const __TASKS__: Record<string, string>;
class TaskExecutor {
constructor(public task: TaskMetadataWithFunctions) {}
async determineRetrying(
execution: TaskRunExecution,
error: unknown
): Promise<TaskRunExecutionRetry | undefined> {
const retry = this.task.retry ?? __PROJECT_CONFIG__.retries?.default;
if (!retry) {
return;
}
const delay = calculateNextRetryDelay(retry, execution.attempt.number);
return typeof delay === "undefined" ? undefined : { timestamp: Date.now() + delay, delay };
}
async execute(
execution: TaskRunExecution,
worker: BackgroundWorkerProperties,
traceContext: Record<string, unknown>
) {
const parsedPayload = JSON.parse(execution.run.payload);
const ctx = TaskRunContext.parse(execution);
const attemptMessage = `Attempt ${execution.attempt.number}`;
const output = await taskContextManager.runWith(
{
ctx,
payload: parsedPayload,
worker,
},
async () => {
tracingSDK.asyncResourceDetector.resolveWithAttributes({
...taskContextManager.attributes,
[SemanticInternalAttributes.SDK_VERSION]: this.task.packageVersion,
[SemanticInternalAttributes.SDK_LANGUAGE]: "typescript",
});
return await tracer.startActiveSpan(
attemptMessage,
async (span) => {
return await consoleInterceptor.intercept(console, async () => {
const init = await this.#callTaskInit(parsedPayload, ctx);
try {
const output = await this.#callRun(parsedPayload, ctx, init);
span.setAttributes(flattenAttributes(output, SemanticInternalAttributes.OUTPUT));
return output;
} finally {
await this.#callTaskCleanup(parsedPayload, ctx, init);
}
});
},
{
kind: SpanKind.CONSUMER,
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "attempt",
...flattenAttributes(parsedPayload, SemanticInternalAttributes.PAYLOAD),
...accessoryAttributes({
items: [
{
text: ctx.task.filePath,
},
{
text: `${ctx.task.exportName}.run()`,
},
],
style: "codepath",
}),
},
},
tracer.extractContext(traceContext)
);
}
);
return { output: JSON.stringify(output), outputType: "application/json" };
}
async #callRun(payload: unknown, ctx: TaskRunContext, init: unknown) {
const runFn = this.task.fns.run;
const middlewareFn = this.task.fns.middleware;
if (!runFn) {
throw new Error("Task does not have a run function");
}
if (!middlewareFn) {
return runFn(payload, { ctx });
}
return middlewareFn(payload, { ctx, next: async () => runFn(payload, { ctx, init }) });
}
async #callTaskInit(payload: unknown, ctx: TaskRunContext) {
const initFn = this.task.fns.init;
if (!initFn) {
return {};
}
return tracer.startActiveSpan("init", async (span) => {
return await initFn(payload, { ctx });
});
}
async #callTaskCleanup(payload: unknown, ctx: TaskRunContext, init: unknown) {
const cleanupFn = this.task.fns.cleanup;
if (!cleanupFn) {
return;
}
return tracer.startActiveSpan("cleanup", async (span) => {
return await cleanupFn(payload, { ctx, init });
});
}
}
function getTasks(): Array<TaskMetadataWithFunctions> {
const result: Array<TaskMetadataWithFunctions> = [];
@@ -224,7 +98,16 @@ runtime.registerTasks(tasks);
const taskExecutors: Map<string, TaskExecutor> = new Map();
for (const task of tasks) {
taskExecutors.set(task.id, new TaskExecutor(task));
taskExecutors.set(
task.id,
new TaskExecutor(task, {
tracer,
tracingSDK,
consoleInterceptor,
projectConfig: __PROJECT_CONFIG__,
importedConfig,
})
);
}
let _execution: TaskRunExecution | undefined;
@@ -283,21 +166,7 @@ const zodIpc = new ZodIpcConnection({
return sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
id: execution.attempt.id,
ok: true,
...result,
},
});
} catch (e) {
return sender.send("TASK_RUN_COMPLETED", {
execution,
result: {
id: execution.attempt.id,
ok: false,
error: parseError(e),
retry: await executor.determineRetrying(execution, e),
},
result,
});
} finally {
_execution = undefined;
+1 -1
View File
@@ -10,7 +10,7 @@ export class ConsoleInterceptor {
// Intercept the console and send logs to the OpenTelemetry logger
// during the execution of the callback
async intercept<T, R extends Promise<T>>(console: Console, callback: () => R): Promise<T> {
async intercept<T>(console: Console, callback: () => Promise<T>): Promise<T> {
// Save the original console methods
const originalConsole = {
log: console.log,
+2 -1
View File
@@ -48,4 +48,5 @@ export { defaultRetryOptions, calculateNextRetryDelay, calculateResetAt } from "
export { accessoryAttributes } from "./utils/styleAttributes";
export { eventFilterMatches } from "../eventFilterMatches";
export { omit } from "./utils/omit";
export { TracingSDK, type TracingDiagnosticLogLevel } from "./otel";
export { TracingSDK, type TracingDiagnosticLogLevel, recordSpanException } from "./otel";
export { TaskExecutor, type TaskExecutorOptions } from "./workers/taskExecutor";
+1 -1
View File
@@ -67,7 +67,7 @@ export class OtelTaskLogger implements TaskLogger {
severityNumber: SeverityNumber,
properties?: Record<string, unknown>
) {
let attributes: Attributes = { ...flattenAttributes(properties), "log.type": "logger" };
let attributes: Attributes = { ...flattenAttributes(properties) };
const icon = iconStringForSeverity(severityNumber);
if (icon !== undefined) {
+14
View File
@@ -1,2 +1,16 @@
import { Span, SpanStatusCode } from "@opentelemetry/api";
export { TracingSDK, type TracingSDKConfig, type TracingDiagnosticLogLevel } from "./tracingSDK";
export { HttpInstrumentation, FetchInstrumentation } from "./instrumentations";
export function recordSpanException(span: Span, error: unknown) {
if (error instanceof Error) {
span.recordException(error);
} else if (typeof error === "string") {
span.recordException(new Error(error));
} else {
span.recordException(new Error(JSON.stringify(error)));
}
span.setStatus({ code: SpanStatusCode.ERROR });
}
+6
View File
@@ -31,6 +31,8 @@ export const TaskRunErrorCodes = {
TASK_EXECUTION_ABORTED: "TASK_EXECUTION_ABORTED",
TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE: "TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE",
TASK_RUN_CANCELLED: "TASK_RUN_CANCELLED",
TASK_OUTPUT_ERROR: "TASK_OUTPUT_ERROR",
HANDLE_ERROR_ERROR: "HANDLE_ERROR_ERROR",
} as const;
export const TaskRunInternalError = z.object({
@@ -43,7 +45,10 @@ export const TaskRunInternalError = z.object({
"TASK_EXECUTION_ABORTED",
"TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE",
"TASK_RUN_CANCELLED",
"TASK_OUTPUT_ERROR",
"HANDLE_ERROR_ERROR",
]),
message: z.string().optional(),
});
export type TaskRunInternalError = z.infer<typeof TaskRunInternalError>;
@@ -156,6 +161,7 @@ export type TaskRunContext = z.infer<typeof TaskRunContext>;
export const TaskRunExecutionRetry = z.object({
timestamp: z.number(),
delay: z.number(),
error: z.unknown().optional(),
});
export type TaskRunExecutionRetry = z.infer<typeof TaskRunExecutionRetry>;
+17
View File
@@ -0,0 +1,17 @@
import { HandleErrorFnParams, HandleErrorResult } from ".";
import { RetryOptions } from "../schemas";
export interface ProjectConfig {
project: string;
triggerDirectories?: string | string[];
triggerUrl?: string;
retries?: {
enabledInDev?: boolean;
default?: RetryOptions;
};
handleError?: (
payload: any,
error: unknown,
params: HandleErrorFnParams<any>
) => HandleErrorResult;
}
+62
View File
@@ -1 +1,63 @@
import { RetryOptions, TaskMetadataWithFilePath, TaskRunContext } from "../schemas";
import { Prettify } from "./utils";
export * from "./utils";
export * from "./config";
export type InitOutput = Record<string, any> | void | undefined;
export type RunFnParams<TInitOutput extends InitOutput> = Prettify<{
ctx: Context;
init?: TInitOutput;
}>;
export type MiddlewareFnParams = Prettify<{
ctx: Context;
next: () => Promise<void>;
}>;
export type InitFnParams = Prettify<{
ctx: Context;
}>;
export type Context = TaskRunContext;
export type SuccessFnParams<TOutput, TInitOutput extends InitOutput> = RunFnParams<TInitOutput> &
Prettify<{
output: TOutput;
}>;
export type HandleErrorFnParams<TInitOutput extends InitOutput> = RunFnParams<TInitOutput> &
Prettify<{
retry?: RetryOptions;
retryAt?: Date;
retryDelayInMs?: number;
}>;
export type HandleErrorModificationOptions = {
skipRetrying?: boolean | undefined;
retryAt?: Date | undefined;
retryDelayInMs?: number | undefined;
retry?: RetryOptions | undefined;
error?: unknown;
};
export type HandleErrorResult =
| undefined
| void
| HandleErrorModificationOptions
| Promise<undefined | void | HandleErrorModificationOptions>;
export type TaskMetadataWithFunctions = TaskMetadataWithFilePath & {
fns: {
run: (payload: any, params: RunFnParams<any>) => Promise<any>;
init?: (payload: any, params: InitFnParams) => Promise<InitOutput>;
cleanup?: (payload: any, params: RunFnParams<any>) => Promise<void>;
middleware?: (payload: any, params: MiddlewareFnParams) => Promise<void>;
handleError?: (
payload: any,
error: unknown,
params: HandleErrorFnParams<any>
) => HandleErrorResult;
};
};
+4
View File
@@ -2,3 +2,7 @@ export type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T,
Omit<T, K> extends infer O
? { [P in keyof O]: O[P] }
: never;
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
@@ -0,0 +1,315 @@
import { SpanKind } from "@opentelemetry/api";
import { TracingSDK, recordSpanException } from "../otel";
import {
BackgroundWorkerProperties,
Config,
TaskRunContext,
TaskRunErrorCodes,
TaskRunExecution,
TaskRunExecutionResult,
TaskRunExecutionRetry,
} from "../schemas";
import { SemanticInternalAttributes } from "../semanticInternalAttributes";
import { ProjectConfig, TaskMetadataWithFunctions } from "../types";
import { flattenAttributes } from "../utils/flattenAttributes";
import { accessoryAttributes } from "../utils/styleAttributes";
import { calculateNextRetryDelay } from "../utils/retries";
import { taskContextManager } from "../tasks/taskContextManager";
import { TriggerTracer } from "../tracer";
import { ConsoleInterceptor } from "../consoleInterceptor";
import { parseError } from "../errors";
export type TaskExecutorOptions = {
tracingSDK: TracingSDK;
tracer: TriggerTracer;
consoleInterceptor: ConsoleInterceptor;
projectConfig: Config;
importedConfig: ProjectConfig | undefined;
};
export class TaskExecutor {
private _tracingSDK: TracingSDK;
private _tracer: TriggerTracer;
private _consoleInterceptor: ConsoleInterceptor;
private _config: Config;
private _importedConfig: ProjectConfig | undefined;
constructor(
public task: TaskMetadataWithFunctions,
options: TaskExecutorOptions
) {
this._tracingSDK = options.tracingSDK;
this._tracer = options.tracer;
this._consoleInterceptor = options.consoleInterceptor;
this._config = options.projectConfig;
this._importedConfig = options.importedConfig;
}
async execute(
execution: TaskRunExecution,
worker: BackgroundWorkerProperties,
traceContext: Record<string, unknown>
): Promise<TaskRunExecutionResult> {
const parsedPayload = JSON.parse(execution.run.payload);
const ctx = TaskRunContext.parse(execution);
const attemptMessage = `Attempt ${execution.attempt.number}`;
const result = await taskContextManager.runWith(
{
ctx,
payload: parsedPayload,
worker,
},
async () => {
this._tracingSDK.asyncResourceDetector.resolveWithAttributes({
...taskContextManager.attributes,
[SemanticInternalAttributes.SDK_VERSION]: this.task.packageVersion,
[SemanticInternalAttributes.SDK_LANGUAGE]: "typescript",
});
return await this._tracer.startActiveSpan(
attemptMessage,
async (span) => {
return await this._consoleInterceptor.intercept(console, async () => {
const init = await this.#callTaskInit(parsedPayload, ctx);
try {
const output = await this.#callRun(parsedPayload, ctx, init);
try {
span.setAttributes(flattenAttributes(output, SemanticInternalAttributes.OUTPUT));
const serializedOutput = JSON.stringify(output);
return {
ok: true,
id: execution.attempt.id,
output: serializedOutput,
outputType: "application/json",
} satisfies TaskRunExecutionResult;
} catch (stringifyError) {
recordSpanException(span, stringifyError);
return {
ok: false,
id: execution.attempt.id,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.TASK_OUTPUT_ERROR,
message:
stringifyError instanceof Error
? stringifyError.message
: typeof stringifyError === "string"
? stringifyError
: undefined,
},
} satisfies TaskRunExecutionResult;
}
} catch (runError) {
try {
const handleErrorResult = await this.#handleError(
execution,
runError,
parsedPayload,
ctx
);
recordSpanException(span, handleErrorResult.error ?? runError);
return {
id: execution.attempt.id,
ok: false,
error: handleErrorResult.error
? parseError(handleErrorResult.error)
: parseError(runError),
retry:
handleErrorResult.status === "retry" ? handleErrorResult.retry : undefined,
skippedRetrying: handleErrorResult.status === "skipped",
} satisfies TaskRunExecutionResult;
} catch (handleErrorError) {
recordSpanException(span, handleErrorError);
return {
ok: false,
id: execution.attempt.id,
error: {
type: "INTERNAL_ERROR",
code: TaskRunErrorCodes.HANDLE_ERROR_ERROR,
message:
handleErrorError instanceof Error
? handleErrorError.message
: typeof handleErrorError === "string"
? handleErrorError
: undefined,
},
} satisfies TaskRunExecutionResult;
}
} finally {
await this.#callTaskCleanup(parsedPayload, ctx, init);
}
});
},
{
kind: SpanKind.CONSUMER,
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "attempt",
...flattenAttributes(parsedPayload, SemanticInternalAttributes.PAYLOAD),
...accessoryAttributes({
items: [
{
text: ctx.task.filePath,
},
{
text: `${ctx.task.exportName}.run()`,
},
],
style: "codepath",
}),
},
},
this._tracer.extractContext(traceContext)
);
}
);
return result;
}
async #callRun(payload: unknown, ctx: TaskRunContext, init: unknown) {
const runFn = this.task.fns.run;
const middlewareFn = this.task.fns.middleware;
if (!runFn) {
throw new Error("Task does not have a run function");
}
if (!middlewareFn) {
return runFn(payload, { ctx });
}
return middlewareFn(payload, { ctx, next: async () => runFn(payload, { ctx, init }) });
}
async #callTaskInit(payload: unknown, ctx: TaskRunContext) {
const initFn = this.task.fns.init;
if (!initFn) {
return {};
}
return this._tracer.startActiveSpan("init", async (span) => {
return await initFn(payload, { ctx });
});
}
async #callTaskCleanup(payload: unknown, ctx: TaskRunContext, init: unknown) {
const cleanupFn = this.task.fns.cleanup;
if (!cleanupFn) {
return;
}
return this._tracer.startActiveSpan("cleanup", async (span) => {
return await cleanupFn(payload, { ctx, init });
});
}
async #handleError(
execution: TaskRunExecution,
error: unknown,
payload: any,
ctx: TaskRunContext
): Promise<
| { status: "retry"; retry: TaskRunExecutionRetry; error?: unknown }
| { status: "skipped"; error?: unknown } // skipped is different than noop, it means that the task was skipped from retrying, instead of just not retrying
| { status: "noop"; error?: unknown }
> {
const retry = this.task.retry ?? this._config.retries?.default;
if (!retry) {
return { status: "noop" };
}
const delay = calculateNextRetryDelay(retry, execution.attempt.number);
if (
typeof this._config.retries?.enabledInDev === "boolean" &&
!this._config.retries.enabledInDev
) {
return { status: "skipped" };
}
return this._tracer.startActiveSpan(
"handleError()",
async (span) => {
const handleErrorResult = this.task.fns.handleError
? await this.task.fns.handleError(payload, error, {
ctx,
retry,
retryDelayInMs: delay,
retryAt: delay ? new Date(Date.now() + delay) : undefined,
})
: this._importedConfig
? await this._importedConfig.handleError?.(payload, error, {
ctx,
retry,
retryDelayInMs: delay,
retryAt: delay ? new Date(Date.now() + delay) : undefined,
})
: undefined;
// If handleErrorResult
if (!handleErrorResult) {
return typeof delay === "undefined"
? { status: "noop" }
: { status: "retry", retry: { timestamp: Date.now() + delay, delay } };
}
if (handleErrorResult.skipRetrying) {
return { status: "skipped", error: handleErrorResult.error };
}
if (typeof handleErrorResult.retryAt !== "undefined") {
return {
status: "retry",
retry: {
timestamp: handleErrorResult.retryAt.getTime(),
delay: handleErrorResult.retryAt.getTime() - Date.now(),
},
error: handleErrorResult.error,
};
}
if (typeof handleErrorResult.retryDelayInMs === "number") {
return {
status: "retry",
retry: {
timestamp: Date.now() + handleErrorResult.retryDelayInMs,
delay: handleErrorResult.retryDelayInMs,
},
error: handleErrorResult.error,
};
}
if (handleErrorResult.retry && typeof handleErrorResult.retry === "object") {
const delay = calculateNextRetryDelay(handleErrorResult.retry, execution.attempt.number);
return typeof delay === "undefined"
? { status: "noop", error: handleErrorResult.error }
: {
status: "retry",
retry: { timestamp: Date.now() + delay, delay },
error: handleErrorResult.error,
};
}
return { status: "noop", error: handleErrorResult.error };
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "exclamation-circle",
},
}
);
}
}
+1 -11
View File
@@ -1,11 +1 @@
import { RetryOptions } from "./retry";
export interface Config {
project: string;
triggerDirectories?: string | string[];
triggerUrl?: string;
retries?: {
enabledInDev?: boolean;
default?: RetryOptions;
};
}
export type { ProjectConfig as Config } from "@trigger.dev/core/v3";
+13 -27
View File
@@ -1,9 +1,16 @@
import { SpanKind } from "@opentelemetry/api";
import { SemanticAttributes } from "@opentelemetry/semantic-conventions";
import {
HandleErrorFnParams,
HandleErrorResult,
InitFnParams,
InitOutput,
MiddlewareFnParams,
QueueOptions,
RetryOptions,
RunFnParams,
SemanticInternalAttributes,
SuccessFnParams,
TaskRunContext,
accessoryAttributes,
apiClientManager,
@@ -16,34 +23,8 @@ import {
import * as packageJson from "../../package.json";
import { tracer } from "./tracer";
export type InitOutput = Record<string, any> | void | undefined;
export type RunFnParams<TInitOutput extends InitOutput> = Prettify<{
ctx: Context;
init: TInitOutput;
}>;
export type MiddlewareFnParams = Prettify<{
ctx: Context;
next: () => Promise<void>;
}>;
export type InitFnParams = Prettify<{
ctx: Context;
}>;
export type Context = TaskRunContext;
export type SuccessFnParams<TOutput, TInitOutput extends InitOutput> = RunFnParams<TInitOutput> &
Prettify<{
output: TOutput;
}>;
export type ErrorFnParams<TInitOutput extends InitOutput> = RunFnParams<TInitOutput> &
Prettify<{
error: unknown;
}>;
type RequireOne<T, K extends keyof T> = {
[X in Exclude<keyof T, K>]?: T[X];
} & {
@@ -66,10 +47,14 @@ export type TaskOptions<TPayload, TOutput = any, TInitOutput extends InitOutput
};
run: (payload: TPayload, params: RunFnParams<TInitOutput>) => Promise<TOutput>;
init?: (payload: TPayload, params: InitFnParams) => Promise<TInitOutput>;
handleError?: (
payload: TPayload,
error: unknown,
params: HandleErrorFnParams<TInitOutput>
) => HandleErrorResult;
cleanup?: (payload: TPayload, params: RunFnParams<TInitOutput>) => Promise<void>;
middleware?: (payload: TPayload, params: MiddlewareFnParams) => Promise<void>;
onSuccess?: (payload: TPayload, params: SuccessFnParams<TOutput, TInitOutput>) => Promise<void>;
onError?: (payload: TPayload, params: ErrorFnParams<TInitOutput>) => Promise<void>;
};
type InvokeHandle = {
@@ -437,6 +422,7 @@ export function createTask<TInput, TOutput, TInitOutput extends InitOutput>(
init: params.init,
cleanup: params.cleanup,
middleware: params.middleware,
handleError: params.handleError,
},
},
enumerable: false,
+2 -1
View File
@@ -1,4 +1,5 @@
import { InitOutput, TaskOptions, Task, createTask } from "./shared";
import { InitOutput } from "@trigger.dev/core/v3";
import { TaskOptions, Task, createTask } from "./shared";
export function task<TInput, TOutput = any, TInitOutput extends InitOutput = any>(
options: TaskOptions<TInput, TOutput, TInitOutput>
+14 -1
View File
@@ -1,4 +1,4 @@
import { task } from "@trigger.dev/sdk/v3";
import { logger, task } from "@trigger.dev/sdk/v3";
import OpenAI from "openai";
@@ -8,6 +8,9 @@ const openai = new OpenAI({
export const openaiTask = task({
id: "openai-task",
retry: {
maxAttempts: 1,
},
run: async (payload: { prompt: string }) => {
const chatCompletion = await openai.chat.completions.create({
messages: [{ role: "user", content: payload.prompt }],
@@ -16,4 +19,14 @@ export const openaiTask = task({
return chatCompletion.choices[0].message.content;
},
handleError: async (payload, err, { ctx, retryAt }) => {
if (err instanceof OpenAI.APIError) {
logger.log("OpenAI API error", { err });
return {
error: new Error("Custom OpenAI API error"),
retryDelayInMs: 10000,
};
}
},
});
+4
View File
@@ -1,3 +1,4 @@
// @ts-check
/** @type {import('@trigger.dev/sdk/v3').Config} */
export default {
@@ -12,4 +13,7 @@ export default {
randomize: true,
},
},
handleError: async (payload, error, { ctx, retryAt, retryDelayInMs, retry }) => {
return { skipRetrying: true };
},
};