UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

515 lines • 23.7 kB
import { apiClientManager, attemptKey, clock, ExecutorToWorkerMessageCatalog, lifecycleHooks, localsAPI, logger, OTEL_LOG_ATTRIBUTE_COUNT_LIMIT, resourceCatalog, runMetadata, runtime, runTimelineMetrics, taskContext, TaskRunContext, TaskRunErrorCodes, timeout, waitUntil, WorkerManifest, WorkerToExecutorMessageCatalog, traceContext, heartbeats, realtimeStreams, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { ConsoleInterceptor, DevUsageManager, DurableClock, getEnvVar, getNumberEnvVar, logLevels, SharedRuntimeManager, OtelTaskLogger, populateEnv, StandardLifecycleHooksManager, StandardLocalsManager, StandardMetadataManager, StandardResourceCatalog, StandardRunTimelineMetricsManager, StandardWaitUntilManager, TaskExecutor, TracingSDK, usage, UsageTimeoutManager, StandardTraceContextManager, StandardHeartbeatsManager, StandardRealtimeStreamsManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; import { setTimeout } from "node:timers/promises"; import sourceMapSupport from "source-map-support"; import { env } from "std-env"; import { normalizeImportPath } from "../utilities/normalizeImportPath.js"; import { VERSION } from "../version.js"; import { promiseWithResolvers } from "@trigger.dev/core/utils"; sourceMapSupport.install({ handleUncaughtExceptions: false, environment: "node", hookRequire: false, }); process.on("uncaughtException", function (error, origin) { logError("Uncaught exception", { error, origin }); if (error instanceof Error) { process.send && process.send({ type: "EVENT", message: { type: "UNCAUGHT_EXCEPTION", payload: { error: { name: error.name, message: error.message, stack: error.stack }, origin, }, version: "v1", }, }); } else { process.send && process.send({ type: "EVENT", message: { type: "UNCAUGHT_EXCEPTION", payload: { error: { name: "Error", message: typeof error === "string" ? error : JSON.stringify(error), }, origin, }, version: "v1", }, }); } }); process.title = `trigger-dev-run-worker (${getEnvVar("TRIGGER_WORKER_VERSION") ?? "unknown version"})`; const heartbeatIntervalMs = getEnvVar("HEARTBEAT_INTERVAL_MS"); const standardLocalsManager = new StandardLocalsManager(); localsAPI.setGlobalLocalsManager(standardLocalsManager); const standardLifecycleHooksManager = new StandardLifecycleHooksManager(); lifecycleHooks.setGlobalLifecycleHooksManager(standardLifecycleHooksManager); const standardRunTimelineMetricsManager = new StandardRunTimelineMetricsManager(); runTimelineMetrics.setGlobalManager(standardRunTimelineMetricsManager); const devUsageManager = new DevUsageManager(); usage.setGlobalUsageManager(devUsageManager); const usageTimeoutManager = new UsageTimeoutManager(devUsageManager); timeout.setGlobalManager(usageTimeoutManager); // Register listener to send IPC message when max duration is exceeded timeout.registerListener(async (maxDurationInSeconds, elapsedTimeInSeconds) => { log(`[${new Date().toISOString()}] Max duration exceeded: ${maxDurationInSeconds}s, elapsed: ${elapsedTimeInSeconds}s`); await zodIpc.send("MAX_DURATION_EXCEEDED", { maxDurationInSeconds, elapsedTimeInSeconds, }); }); const standardResourceCatalog = new StandardResourceCatalog(); resourceCatalog.setGlobalResourceCatalog(standardResourceCatalog); const standardTraceContextManager = new StandardTraceContextManager(); traceContext.setGlobalManager(standardTraceContextManager); const durableClock = new DurableClock(); clock.setGlobalClock(durableClock); const runMetadataManager = new StandardMetadataManager(apiClientManager.clientOrThrow()); runMetadata.setGlobalManager(runMetadataManager); const standardRealtimeStreamsManager = new StandardRealtimeStreamsManager(apiClientManager.clientOrThrow(), getEnvVar("TRIGGER_STREAM_URL", getEnvVar("TRIGGER_API_URL")) ?? "https://api.trigger.dev", (getEnvVar("TRIGGER_STREAMS_DEBUG") === "1" || getEnvVar("TRIGGER_STREAMS_DEBUG") === "true") ?? false); realtimeStreams.setGlobalManager(standardRealtimeStreamsManager); const waitUntilTimeoutInMs = getNumberEnvVar("TRIGGER_WAIT_UNTIL_TIMEOUT_MS", 60_000); const waitUntilManager = new StandardWaitUntilManager(waitUntilTimeoutInMs); waitUntil.setGlobalManager(waitUntilManager); const triggerLogLevel = getEnvVar("TRIGGER_LOG_LEVEL"); const showInternalLogs = getEnvVar("RUN_WORKER_SHOW_LOGS") === "true"; const standardHeartbeatsManager = new StandardHeartbeatsManager(parseInt(heartbeatIntervalMs ?? "30000", 10)); heartbeats.setGlobalManager(standardHeartbeatsManager); async function importConfig(configPath) { const configModule = await import(normalizeImportPath(configPath)); const config = configModule?.default ?? configModule?.config; return { config, handleError: configModule?.handleError, }; } async function loadWorkerManifest() { const manifestContents = await readFile(env.TRIGGER_WORKER_MANIFEST_PATH, "utf-8"); const raw = JSON.parse(manifestContents); return WorkerManifest.parse(raw); } async function doBootstrap() { return await runTimelineMetrics.measureMetric("trigger.dev/start", "bootstrap", {}, async () => { log("Bootstrapping worker"); const workerManifest = await loadWorkerManifest(); resourceCatalog.registerWorkerManifest(workerManifest); const { config, handleError } = await importConfig(workerManifest.configPath); const tracingSDK = new TracingSDK({ url: env.TRIGGER_OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318", instrumentations: config.telemetry?.instrumentations ?? config.instrumentations ?? [], exporters: config.telemetry?.exporters ?? [], logExporters: config.telemetry?.logExporters ?? [], diagLogLevel: env.TRIGGER_OTEL_LOG_LEVEL ?? "none", forceFlushTimeoutMillis: 30_000, resource: config.telemetry?.resource, }); const otelTracer = tracingSDK.getTracer("trigger-dev-worker", VERSION); const otelLogger = tracingSDK.getLogger("trigger-dev-worker", VERSION); const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger }); const consoleInterceptor = new ConsoleInterceptor(otelLogger, typeof config.enableConsoleLogging === "boolean" ? config.enableConsoleLogging : true, typeof config.disableConsoleInterceptor === "boolean" ? config.disableConsoleInterceptor : false, OTEL_LOG_ATTRIBUTE_COUNT_LIMIT); const configLogLevel = triggerLogLevel ?? config.logLevel ?? "info"; const otelTaskLogger = new OtelTaskLogger({ logger: otelLogger, tracer: tracer, level: logLevels.includes(configLogLevel) ? configLogLevel : "info", maxAttributeCount: OTEL_LOG_ATTRIBUTE_COUNT_LIMIT, }); logger.setGlobalTaskLogger(otelTaskLogger); if (config.init) { lifecycleHooks.registerGlobalInitHook({ id: "config", fn: config.init, }); } if (config.onStart) { lifecycleHooks.registerGlobalStartHook({ id: "config", fn: config.onStart, }); } if (config.onSuccess) { lifecycleHooks.registerGlobalSuccessHook({ id: "config", fn: config.onSuccess, }); } if (config.onFailure) { lifecycleHooks.registerGlobalFailureHook({ id: "config", fn: config.onFailure, }); } if (handleError) { lifecycleHooks.registerGlobalCatchErrorHook({ id: "config", fn: handleError, }); } log("Bootstrapped worker"); return { tracer, tracingSDK, consoleInterceptor, config, workerManifest, }; }); } let bootstrapCache; async function bootstrap() { if (!bootstrapCache) { bootstrapCache = await doBootstrap(); } return bootstrapCache; } let _execution; let _isRunning = false; let _isCancelled = false; let _tracingSDK; let _executionMeasurement; let _cancelController = new AbortController(); let _lastFlushPromise; let _sharedWorkerRuntime; let _lastEnv; let _executionCount = 0; function resetExecutionEnvironment() { _execution = undefined; _isRunning = false; _isCancelled = false; _executionMeasurement = undefined; _cancelController = new AbortController(); standardLocalsManager.reset(); standardLifecycleHooksManager.reset(); standardRunTimelineMetricsManager.reset(); devUsageManager.reset(); usageTimeoutManager.reset(); runMetadataManager.reset(); standardRealtimeStreamsManager.reset(); waitUntilManager.reset(); _sharedWorkerRuntime?.reset(); durableClock.reset(); taskContext.disable(); standardTraceContextManager.reset(); standardHeartbeatsManager.reset(); // Wait for all streams to finish before completing the run waitUntil.register({ requiresResolving: () => standardRealtimeStreamsManager.hasActiveStreams(), promise: (timeoutInMs) => standardRealtimeStreamsManager.waitForAllStreams(timeoutInMs), }); log(`[${new Date().toISOString()}] Reset execution environment`); } const zodIpc = new ZodIpcConnection({ listenSchema: WorkerToExecutorMessageCatalog, emitSchema: ExecutorToWorkerMessageCatalog, process, handlers: { EXECUTE_TASK_RUN: async ({ execution, traceContext, metadata, metrics, env, isWarmStart }, sender) => { if (env) { populateEnv(env, { override: true, previousEnv: _lastEnv, }); _lastEnv = env; } log(`[${new Date().toISOString()}] Received EXECUTE_TASK_RUN`, execution); if (_lastFlushPromise) { const now = performance.now(); await _lastFlushPromise; const duration = performance.now() - now; log(`[${new Date().toISOString()}] Awaited last flush in ${duration}ms`); } resetExecutionEnvironment(); standardTraceContextManager.traceContext = traceContext; standardRunTimelineMetricsManager.registerMetricsFromExecution(metrics, isWarmStart); if (_isRunning) { logError("Worker is already running a task"); await sender.send("TASK_RUN_COMPLETED", { execution, result: { ok: false, id: execution.run.id, error: { type: "INTERNAL_ERROR", code: TaskRunErrorCodes.TASK_ALREADY_RUNNING, }, usage: { durationMs: 0, }, flushedMetadata: await runMetadataManager.stopAndReturnLastFlush(), }, }); return; } const ctx = TaskRunContext.parse(execution); taskContext.setGlobalTaskContext({ ctx, worker: metadata, isWarmStart: isWarmStart ?? false, }); try { const { tracer, tracingSDK, consoleInterceptor, config, workerManifest } = await bootstrap(); _tracingSDK = tracingSDK; const taskManifest = workerManifest.tasks.find((t) => t.id === execution.task.id); if (!taskManifest) { logError(`Could not find task ${execution.task.id}`); await sender.send("TASK_RUN_COMPLETED", { execution, result: { ok: false, id: execution.run.id, error: { type: "INTERNAL_ERROR", code: TaskRunErrorCodes.COULD_NOT_FIND_TASK, message: `Could not find task ${execution.task.id}. Make sure the task is exported and the ID is correct.`, }, usage: { durationMs: 0, }, flushedMetadata: await runMetadataManager.stopAndReturnLastFlush(), }, }); return; } // First attempt to get the task from the resource catalog let task = resourceCatalog.getTask(execution.task.id); if (!task) { log(`Could not find task ${execution.task.id} in resource catalog, importing...`); try { await runTimelineMetrics.measureMetric("trigger.dev/start", "import", { entryPoint: taskManifest.entryPoint, file: taskManifest.filePath, }, async () => { const beforeImport = performance.now(); resourceCatalog.setCurrentFileContext(taskManifest.entryPoint, taskManifest.filePath); // Load init file if it exists if (workerManifest.initEntryPoint) { try { await import(normalizeImportPath(workerManifest.initEntryPoint)); log(`Loaded init file from ${workerManifest.initEntryPoint}`); } catch (err) { logError(`Failed to load init file`, err); throw err; } } await import(normalizeImportPath(taskManifest.entryPoint)); resourceCatalog.clearCurrentFileContext(); const durationMs = performance.now() - beforeImport; log(`Imported task ${execution.task.id} [${taskManifest.entryPoint}] in ${durationMs}ms`); }); } catch (err) { logError(`Failed to import task ${execution.task.id}`, err); await sender.send("TASK_RUN_COMPLETED", { execution, result: { ok: false, id: execution.run.id, error: { type: "INTERNAL_ERROR", code: TaskRunErrorCodes.COULD_NOT_IMPORT_TASK, message: err instanceof Error ? err.message : String(err), stackTrace: err instanceof Error ? err.stack : undefined, }, usage: { durationMs: 0, }, flushedMetadata: await runMetadataManager.stopAndReturnLastFlush(), }, }); return; } // Now try and get the task again task = resourceCatalog.getTask(execution.task.id); } if (!task) { logError(`Could not find task ${execution.task.id}`); await sender.send("TASK_RUN_COMPLETED", { execution, result: { ok: false, id: execution.run.id, error: { type: "INTERNAL_ERROR", code: TaskRunErrorCodes.COULD_NOT_FIND_EXECUTOR, }, usage: { durationMs: 0, }, flushedMetadata: await runMetadataManager.stopAndReturnLastFlush(), }, }); return; } runMetadataManager.runId = execution.run.id; runMetadataManager.runIdIsRoot = typeof execution.run.rootTaskRunId === "undefined"; _executionCount++; const executor = new TaskExecutor(task, { tracer, tracingSDK, consoleInterceptor, retries: config.retries, isWarmStart, executionCount: _executionCount, }); try { _execution = execution; _isRunning = true; standardHeartbeatsManager.startHeartbeat(attemptKey(execution)); runMetadataManager.startPeriodicFlush(getNumberEnvVar("TRIGGER_RUN_METADATA_FLUSH_INTERVAL", 1000)); devUsageManager.setInitialState({ cpuTime: execution.run.durationMs ?? 0, costInCents: execution.run.costInCents ?? 0, }); _executionMeasurement = usage.start(); const timeoutController = timeout.abortAfterTimeout(execution.run.maxDuration); const signal = AbortSignal.any([_cancelController.signal, timeoutController.signal]); const { result } = await executor.execute(execution, ctx, signal); if (_isRunning && !_isCancelled) { const usageSample = usage.stop(_executionMeasurement); return sender.send("TASK_RUN_COMPLETED", { execution, result: { ...result, usage: { durationMs: usageSample.cpuTime, }, flushedMetadata: await runMetadataManager.stopAndReturnLastFlush(), }, }); } } finally { standardHeartbeatsManager.stopHeartbeat(); _execution = undefined; _isRunning = false; log(`[${new Date().toISOString()}] Task run completed`); } } catch (err) { logError("Failed to execute task", err); await sender.send("TASK_RUN_COMPLETED", { execution, result: { ok: false, id: execution.run.id, error: { type: "INTERNAL_ERROR", code: TaskRunErrorCodes.CONFIGURED_INCORRECTLY, message: err instanceof Error ? err.message : String(err), stackTrace: err instanceof Error ? err.stack : undefined, }, usage: { durationMs: 0, }, flushedMetadata: await runMetadataManager.stopAndReturnLastFlush(), }, }); } }, CANCEL: async ({ timeoutInMs }) => { _isCancelled = true; _cancelController.abort("run cancelled"); await callCancelHooks(timeoutInMs); if (_executionMeasurement) { usage.stop(_executionMeasurement); } await flushAll(timeoutInMs); }, FLUSH: async ({ timeoutInMs }) => { await flushAll(timeoutInMs); }, RESOLVE_WAITPOINT: async ({ waitpoint }) => { _sharedWorkerRuntime?.resolveWaitpoints([waitpoint]); }, }, }); async function callCancelHooks(timeoutInMs = 10_000) { const now = performance.now(); try { await Promise.race([lifecycleHooks.callOnCancelHookListeners(), setTimeout(timeoutInMs)]); } finally { const duration = performance.now() - now; log(`Called cancel hooks in ${duration}ms`); } } async function flushAll(timeoutInMs = 10_000) { const now = performance.now(); const { promise, resolve } = promiseWithResolvers(); _lastFlushPromise = promise; const results = await Promise.allSettled([ flushTracingSDK(timeoutInMs), flushMetadata(timeoutInMs), ]); const successfulFlushes = results .filter((result) => result.status === "fulfilled") .map((result) => result.value.flushed); const failedFlushes = ["tracingSDK", "runMetadata"].filter((flushed) => !successfulFlushes.includes(flushed)); if (failedFlushes.length > 0) { logError(`Failed to flush ${failedFlushes.join(", ")}`); } const errorMessages = results .filter((result) => result.status === "rejected") .map((result) => result.reason); if (errorMessages.length > 0) { logError(errorMessages.join("\n")); } for (const flushed of successfulFlushes) { log(`Flushed ${flushed} successfully`); } const duration = performance.now() - now; log(`Flushed all in ${duration}ms`); // Resolve the last flush promise resolve(); } async function flushTracingSDK(timeoutInMs = 10_000) { const now = performance.now(); await Promise.race([_tracingSDK?.flush(), setTimeout(timeoutInMs)]); const duration = performance.now() - now; log(`Flushed tracingSDK in ${duration}ms`); return { flushed: "tracingSDK", durationMs: duration, }; } async function flushMetadata(timeoutInMs = 10_000) { const now = performance.now(); await Promise.race([runMetadataManager.flush(), setTimeout(timeoutInMs)]); const duration = performance.now() - now; log(`Flushed runMetadata in ${duration}ms`); return { flushed: "runMetadata", durationMs: duration, }; } _sharedWorkerRuntime = new SharedRuntimeManager(zodIpc, showInternalLogs); runtime.setGlobalRuntimeManager(_sharedWorkerRuntime); standardHeartbeatsManager.registerListener(async (id) => { await zodIpc.send("TASK_HEARTBEAT", { id }); }); function log(message, ...args) { if (!showInternalLogs) return; console.log(`[${new Date().toISOString()}] ${message}`, args); } function logError(message, error) { if (!showInternalLogs) return; console.error(`[${new Date().toISOString()}] ${message}`, error); } log(`Executor started`); //# sourceMappingURL=dev-run-worker.js.map