Skip to content

Commit 82251e2

Browse files
authored
Merge pull request #1817 from triggerdotdev/revamped-lifecycle-hooks
v4: New lifecycle hooks
2 parents 2ef60ce + 6d5d10a commit 82251e2

File tree

38 files changed

+4675
-538
lines changed

38 files changed

+4675
-538
lines changed

.changeset/weak-jobs-hide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"trigger.dev": patch
4+
"@trigger.dev/core": patch
5+
---
6+
7+
v4: New lifecycle hooks

.cursor/rules/executing-commands.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ But often, when running tests, it's better to `cd` into the directory and then r
1313

1414
```
1515
cd apps/webapp
16-
pnpm run test
16+
pnpm run test --run
1717
```
1818

1919
This way you can run for a single file easily:
2020

2121
```
2222
cd internal-packages/run-engine
23-
pnpm run test ./src/engine/tests/ttl.test.ts
23+
pnpm run test ./src/engine/tests/ttl.test.ts --run
2424
```

apps/webapp/app/components/runs/v3/RunIcon.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
4444
}
4545

4646
if (!name) return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
47+
if (tablerIcons.has(name)) {
48+
return <TablerIcon name={name} className={className} />;
49+
}
4750

4851
switch (name) {
4952
case "task":
@@ -73,6 +76,28 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
7376
return <InformationCircleIcon className={cn(className, "text-rose-500")} />;
7477
case "fatal":
7578
return <HandRaisedIcon className={cn(className, "text-rose-800")} />;
79+
case "task-middleware":
80+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
81+
case "task-fn-run":
82+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
83+
case "task-hook-init":
84+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
85+
case "task-hook-onStart":
86+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
87+
case "task-hook-onSuccess":
88+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
89+
case "task-hook-onFailure":
90+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
91+
case "task-hook-onComplete":
92+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
93+
case "task-hook-onWait":
94+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
95+
case "task-hook-onResume":
96+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
97+
case "task-hook-catchError":
98+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
99+
case "task-hook-cleanup":
100+
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;
76101
}
77102

78103
return <InformationCircleIcon className={cn(className, "text-text-dimmed")} />;

internal-packages/run-engine/src/engine/errors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export function runStatusFromError(error: TaskRunError): TaskRunStatus {
1313
//e.g. a bug
1414
switch (error.code) {
1515
case "RECURSIVE_WAIT_DEADLOCK":
16+
case "TASK_INPUT_ERROR":
17+
case "TASK_OUTPUT_ERROR":
18+
case "TASK_MIDDLEWARE_ERROR":
1619
return "COMPLETED_WITH_ERRORS";
1720
case "TASK_RUN_CANCELLED":
1821
return "CANCELED";
@@ -41,8 +44,6 @@ export function runStatusFromError(error: TaskRunError): TaskRunStatus {
4144
case "TASK_RUN_STALLED_EXECUTING_WITH_WAITPOINTS":
4245
case "TASK_HAS_N0_EXECUTION_SNAPSHOT":
4346
case "GRACEFUL_EXIT_TIMEOUT":
44-
case "TASK_INPUT_ERROR":
45-
case "TASK_OUTPUT_ERROR":
4647
case "POD_EVICTED":
4748
case "POD_UNKNOWN_ERROR":
4849
case "TASK_EXECUTION_ABORTED":

packages/cli-v3/src/build/bundle.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export type BundleResult = {
4747
runControllerEntryPoint: string | undefined;
4848
indexWorkerEntryPoint: string | undefined;
4949
indexControllerEntryPoint: string | undefined;
50+
initEntryPoint: string | undefined;
5051
stop: (() => Promise<void>) | undefined;
5152
};
5253

@@ -229,11 +230,26 @@ export async function getBundleResultFromBuild(
229230
let runControllerEntryPoint: string | undefined;
230231
let indexWorkerEntryPoint: string | undefined;
231232
let indexControllerEntryPoint: string | undefined;
233+
let initEntryPoint: string | undefined;
232234

233235
const configEntryPoint = resolvedConfig.configFile
234236
? relative(resolvedConfig.workingDir, resolvedConfig.configFile)
235237
: "trigger.config.ts";
236238

239+
// Check if the entry point is an init.ts file at the root of a trigger directory
240+
function isInitEntryPoint(entryPoint: string): boolean {
241+
const normalizedEntryPoint = entryPoint.replace(/\\/g, "/"); // Normalize path separators
242+
const initFileNames = ["init.ts", "init.mts", "init.cts", "init.js", "init.mjs", "init.cjs"];
243+
244+
// Check if it's directly in one of the trigger directories
245+
return resolvedConfig.dirs.some((dir) => {
246+
const normalizedDir = dir.replace(/\\/g, "/");
247+
return initFileNames.some(
248+
(fileName) => normalizedEntryPoint === `${normalizedDir}/${fileName}`
249+
);
250+
});
251+
}
252+
237253
for (const [outputPath, outputMeta] of Object.entries(result.metafile.outputs)) {
238254
if (outputPath.endsWith(".mjs")) {
239255
const $outputPath = resolve(workingDir, outputPath);
@@ -254,6 +270,8 @@ export async function getBundleResultFromBuild(
254270
indexControllerEntryPoint = $outputPath;
255271
} else if (isIndexWorkerForTarget(outputMeta.entryPoint, target)) {
256272
indexWorkerEntryPoint = $outputPath;
273+
} else if (isInitEntryPoint(outputMeta.entryPoint)) {
274+
initEntryPoint = $outputPath;
257275
} else {
258276
if (
259277
!outputMeta.entryPoint.startsWith("..") &&
@@ -280,6 +298,7 @@ export async function getBundleResultFromBuild(
280298
runControllerEntryPoint,
281299
indexWorkerEntryPoint,
282300
indexControllerEntryPoint,
301+
initEntryPoint,
283302
contentHash: hasher.digest("hex"),
284303
};
285304
}
@@ -357,6 +376,7 @@ export async function createBuildManifestFromBundle({
357376
runControllerEntryPoint: bundle.runControllerEntryPoint ?? getRunControllerForTarget(target),
358377
runWorkerEntryPoint: bundle.runWorkerEntryPoint ?? getRunWorkerForTarget(target),
359378
loaderEntryPoint: bundle.loaderEntryPoint,
379+
initEntryPoint: bundle.initEntryPoint,
360380
configPath: bundle.configPath,
361381
customConditions: resolvedConfig.build.conditions ?? [],
362382
deploy: {

packages/cli-v3/src/entryPoints/dev-index-worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ await sendMessageInCatalog(
141141
controllerEntryPoint: buildManifest.runControllerEntryPoint,
142142
loaderEntryPoint: buildManifest.loaderEntryPoint,
143143
customConditions: buildManifest.customConditions,
144+
initEntryPoint: buildManifest.initEntryPoint,
144145
},
145146
importErrors,
146147
},

packages/cli-v3/src/entryPoints/dev-run-worker.ts

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
import type { Tracer } from "@opentelemetry/api";
22
import type { Logger } from "@opentelemetry/api-logs";
33
import {
4+
AnyOnCatchErrorHookFunction,
5+
AnyOnFailureHookFunction,
6+
AnyOnInitHookFunction,
7+
AnyOnStartHookFunction,
8+
AnyOnSuccessHookFunction,
49
apiClientManager,
510
clock,
611
ExecutorToWorkerMessageCatalog,
712
type HandleErrorFunction,
13+
lifecycleHooks,
14+
localsAPI,
815
logger,
916
LogLevel,
17+
resourceCatalog,
1018
runMetadata,
1119
runtime,
12-
resourceCatalog,
20+
runTimelineMetrics,
1321
TaskRunErrorCodes,
1422
TaskRunExecution,
1523
timeout,
1624
TriggerConfig,
1725
waitUntil,
1826
WorkerManifest,
1927
WorkerToExecutorMessageCatalog,
20-
runTimelineMetrics,
2128
} from "@trigger.dev/core/v3";
2229
import { TriggerTracer } from "@trigger.dev/core/v3/tracer";
2330
import {
@@ -29,15 +36,17 @@ import {
2936
logLevels,
3037
ManagedRuntimeManager,
3138
OtelTaskLogger,
39+
StandardLifecycleHooksManager,
40+
StandardLocalsManager,
3241
StandardMetadataManager,
3342
StandardResourceCatalog,
43+
StandardRunTimelineMetricsManager,
3444
StandardWaitUntilManager,
3545
TaskExecutor,
3646
TracingDiagnosticLogLevel,
3747
TracingSDK,
3848
usage,
3949
UsageTimeoutManager,
40-
StandardRunTimelineMetricsManager,
4150
} from "@trigger.dev/core/v3/workers";
4251
import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc";
4352
import { readFile } from "node:fs/promises";
@@ -89,10 +98,16 @@ process.on("uncaughtException", function (error, origin) {
8998

9099
const heartbeatIntervalMs = getEnvVar("HEARTBEAT_INTERVAL_MS");
91100

101+
const standardLocalsManager = new StandardLocalsManager();
102+
localsAPI.setGlobalLocalsManager(standardLocalsManager);
103+
92104
const standardRunTimelineMetricsManager = new StandardRunTimelineMetricsManager();
93105
runTimelineMetrics.setGlobalManager(standardRunTimelineMetricsManager);
94106
standardRunTimelineMetricsManager.seedMetricsFromEnvironment();
95107

108+
const standardLifecycleHooksManager = new StandardLifecycleHooksManager();
109+
lifecycleHooks.setGlobalLifecycleHooksManager(standardLifecycleHooksManager);
110+
96111
const devUsageManager = new DevUsageManager();
97112
usage.setGlobalUsageManager(devUsageManager);
98113
timeout.setGlobalManager(new UsageTimeoutManager(devUsageManager));
@@ -170,12 +185,46 @@ async function bootstrap() {
170185

171186
logger.setGlobalTaskLogger(otelTaskLogger);
172187

188+
if (config.init) {
189+
lifecycleHooks.registerGlobalInitHook({
190+
id: "config",
191+
fn: config.init as AnyOnInitHookFunction,
192+
});
193+
}
194+
195+
if (config.onStart) {
196+
lifecycleHooks.registerGlobalStartHook({
197+
id: "config",
198+
fn: config.onStart as AnyOnStartHookFunction,
199+
});
200+
}
201+
202+
if (config.onSuccess) {
203+
lifecycleHooks.registerGlobalSuccessHook({
204+
id: "config",
205+
fn: config.onSuccess as AnyOnSuccessHookFunction,
206+
});
207+
}
208+
209+
if (config.onFailure) {
210+
lifecycleHooks.registerGlobalFailureHook({
211+
id: "config",
212+
fn: config.onFailure as AnyOnFailureHookFunction,
213+
});
214+
}
215+
216+
if (handleError) {
217+
lifecycleHooks.registerGlobalCatchErrorHook({
218+
id: "config",
219+
fn: handleError as AnyOnCatchErrorHookFunction,
220+
});
221+
}
222+
173223
return {
174224
tracer,
175225
tracingSDK,
176226
consoleInterceptor,
177227
config,
178-
handleErrorFn: handleError,
179228
workerManifest,
180229
};
181230
}
@@ -217,7 +266,7 @@ const zodIpc = new ZodIpcConnection({
217266
}
218267

219268
try {
220-
const { tracer, tracingSDK, consoleInterceptor, config, handleErrorFn, workerManifest } =
269+
const { tracer, tracingSDK, consoleInterceptor, config, workerManifest } =
221270
await bootstrap();
222271

223272
_tracingSDK = tracingSDK;
@@ -257,6 +306,18 @@ const zodIpc = new ZodIpcConnection({
257306
async () => {
258307
const beforeImport = performance.now();
259308
resourceCatalog.setCurrentFileContext(taskManifest.entryPoint, taskManifest.filePath);
309+
310+
// Load init file if it exists
311+
if (workerManifest.initEntryPoint) {
312+
try {
313+
await import(normalizeImportPath(workerManifest.initEntryPoint));
314+
log(`Loaded init file from ${workerManifest.initEntryPoint}`);
315+
} catch (err) {
316+
logError(`Failed to load init file`, err);
317+
throw err;
318+
}
319+
}
320+
260321
await import(normalizeImportPath(taskManifest.entryPoint));
261322
resourceCatalog.clearCurrentFileContext();
262323
const durationMs = performance.now() - beforeImport;
@@ -321,8 +382,7 @@ const zodIpc = new ZodIpcConnection({
321382
tracer,
322383
tracingSDK,
323384
consoleInterceptor,
324-
config,
325-
handleErrorFn,
385+
retries: config.retries,
326386
});
327387

328388
try {
@@ -340,42 +400,7 @@ const zodIpc = new ZodIpcConnection({
340400
? timeout.abortAfterTimeout(execution.run.maxDuration)
341401
: undefined;
342402

343-
signal?.addEventListener("abort", async (e) => {
344-
if (_isRunning) {
345-
_isRunning = false;
346-
_execution = undefined;
347-
348-
const usageSample = usage.stop(measurement);
349-
350-
await sender.send("TASK_RUN_COMPLETED", {
351-
execution,
352-
result: {
353-
ok: false,
354-
id: execution.run.id,
355-
error: {
356-
type: "INTERNAL_ERROR",
357-
code: TaskRunErrorCodes.MAX_DURATION_EXCEEDED,
358-
message:
359-
signal.reason instanceof Error
360-
? signal.reason.message
361-
: String(signal.reason),
362-
},
363-
usage: {
364-
durationMs: usageSample.cpuTime,
365-
},
366-
metadata: runMetadataManager.stopAndReturnLastFlush(),
367-
},
368-
});
369-
}
370-
});
371-
372-
const { result } = await executor.execute(
373-
execution,
374-
metadata,
375-
traceContext,
376-
measurement,
377-
signal
378-
);
403+
const { result } = await executor.execute(execution, metadata, traceContext, signal);
379404

380405
const usageSample = usage.stop(measurement);
381406

packages/cli-v3/src/entryPoints/managed-index-worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ await sendMessageInCatalog(
141141
controllerEntryPoint: buildManifest.runControllerEntryPoint,
142142
loaderEntryPoint: buildManifest.loaderEntryPoint,
143143
customConditions: buildManifest.customConditions,
144+
initEntryPoint: buildManifest.initEntryPoint,
144145
},
145146
importErrors,
146147
},

0 commit comments

Comments
 (0)