Skip to content

Commit 568da01

Browse files
authored
v3: improve non-zero exit errors (#1179)
* docker provider will optionally enforce machine presets * update task monitor oom message * add oom task to v3 catalog * improve handling of non-zero exit errors * add changeset
1 parent c75e29a commit 568da01

File tree

9 files changed

+122
-21
lines changed

9 files changed

+122
-21
lines changed

.changeset/nervous-planets-sparkle.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
- Improve non-zero exit code error messages
7+
- Detect OOM conditions within worker child processes
8+
- Internal errors can have optional stack traces
9+
- Docker provider can be set to enforce machine presets

apps/docker-provider/src/index.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,27 @@ class DockerTaskOperations implements TaskOperations {
109109

110110
const containerName = this.#getRunContainerName(opts.runId);
111111

112+
const runArgs = [
113+
"run",
114+
"--network=host",
115+
"--detach",
116+
`--env=TRIGGER_ENV_ID=${opts.envId}`,
117+
`--env=TRIGGER_RUN_ID=${opts.runId}`,
118+
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT}`,
119+
`--env=POD_NAME=${containerName}`,
120+
`--env=COORDINATOR_HOST=${COORDINATOR_HOST}`,
121+
`--env=COORDINATOR_PORT=${COORDINATOR_PORT}`,
122+
`--name=${containerName}`,
123+
];
124+
125+
if (process.env.ENFORCE_MACHINE_PRESETS) {
126+
runArgs.push(`--cpus=${opts.machine.cpu}`, `--memory=${opts.machine.memory}G`);
127+
}
128+
129+
runArgs.push(`${opts.image}`);
130+
112131
try {
113-
logger.debug(
114-
await execa("docker", [
115-
"run",
116-
"--network=host",
117-
"--detach",
118-
`--env=TRIGGER_ENV_ID=${opts.envId}`,
119-
`--env=TRIGGER_RUN_ID=${opts.runId}`,
120-
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT}`,
121-
`--env=POD_NAME=${containerName}`,
122-
`--env=COORDINATOR_HOST=${COORDINATOR_HOST}`,
123-
`--env=COORDINATOR_PORT=${COORDINATOR_PORT}`,
124-
`--name=${containerName}`,
125-
`${opts.image}`,
126-
])
127-
);
132+
logger.debug(await execa("docker", runArgs));
128133
} catch (error) {
129134
if (!isExecaChildProcess(error)) {
130135
throw error;

apps/kubernetes-provider/src/taskMonitor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ export class TaskMonitor {
181181
}
182182
break;
183183
case "OOMKilled":
184-
reason = "Out of memory! Try increasing the memory on this task.";
184+
reason =
185+
"Process ran out of memory! Try choosing a machine preset with more memory for this task.";
185186
break;
186187
default:
187188
break;

apps/webapp/app/v3/eventRepository.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,7 @@ export function createExceptionPropertiesFromError(error: TaskRunError): Excepti
11181118
return {
11191119
type: "Internal error",
11201120
message: [error.code, error.message].filter(Boolean).join(": "),
1121+
stacktrace: error.stackTrace,
11211122
};
11221123
}
11231124
case "STRING_ERROR": {

packages/cli-v3/src/workers/common/errors.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export class TaskMetadataParseError extends Error {
2323
}
2424

2525
export class UnexpectedExitError extends Error {
26-
constructor(public code: number) {
26+
constructor(
27+
public code: number,
28+
public signal: NodeJS.Signals | null,
29+
public stderr: string | undefined
30+
) {
2731
super(`Unexpected exit with code ${code}`);
2832

2933
this.name = "UnexpectedExitError";
@@ -61,3 +65,25 @@ export class GracefulExitTimeoutError extends Error {
6165
this.name = "GracefulExitTimeoutError";
6266
}
6367
}
68+
69+
export function getFriendlyErrorMessage(
70+
code: number,
71+
signal: NodeJS.Signals | null,
72+
stderr: string | undefined
73+
) {
74+
const message = (text: string) => {
75+
if (signal) {
76+
return `[${signal}] ${text}`;
77+
} else {
78+
return text;
79+
}
80+
};
81+
82+
if (code === 137 || stderr?.includes("OOMErrorHandler")) {
83+
return message(
84+
"Process ran out of memory! Try choosing a machine preset with more memory for this task."
85+
);
86+
}
87+
88+
return message(`Process exited with code ${code}.`);
89+
}

packages/cli-v3/src/workers/dev/backgroundWorker.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
TaskMetadataParseError,
4646
UncaughtExceptionError,
4747
UnexpectedExitError,
48+
getFriendlyErrorMessage,
4849
} from "../common/errors.js";
4950
import { CliApiClient } from "../../apiClient.js";
5051

@@ -723,6 +724,8 @@ export class BackgroundWorker {
723724
error: {
724725
type: "INTERNAL_ERROR",
725726
code: TaskRunErrorCodes.TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE,
727+
message: getFriendlyErrorMessage(e.code, e.signal, e.stderr),
728+
stackTrace: e.stderr,
726729
},
727730
};
728731
}
@@ -782,6 +785,7 @@ class TaskRunProcess {
782785
private _currentExecution: TaskRunExecution | undefined;
783786
private _isBeingKilled: boolean = false;
784787
private _isBeingCancelled: boolean = false;
788+
private _stderr: Array<string> = [];
785789
/**
786790
* @deprecated use onTaskRunHeartbeat instead
787791
*/
@@ -1009,7 +1013,13 @@ class TaskRunProcess {
10091013
} else if (this._isBeingKilled) {
10101014
rejecter(new CleanupProcessError());
10111015
} else {
1012-
rejecter(new UnexpectedExitError(code ?? -1));
1016+
rejecter(
1017+
new UnexpectedExitError(
1018+
code ?? -1,
1019+
signal,
1020+
this._stderr.length ? this._stderr.join("\n") : undefined
1021+
)
1022+
);
10131023
}
10141024
}
10151025
}
@@ -1048,9 +1058,16 @@ class TaskRunProcess {
10481058
`${this._currentExecution.run.id}.${this._currentExecution.attempt.number}`
10491059
);
10501060

1061+
const errorLine = data.toString();
1062+
10511063
logger.log(
1052-
`${chalkError("○")} ${chalkGrey(prettyPrintDate(new Date()))} ${runId} ${data.toString()}`
1064+
`${chalkError("○")} ${chalkGrey(prettyPrintDate(new Date()))} ${runId} ${errorLine}`
10531065
);
1066+
1067+
if (this._stderr.length > 100) {
1068+
this._stderr.shift();
1069+
}
1070+
this._stderr.push(errorLine);
10541071
}
10551072

10561073
#kill() {

packages/cli-v3/src/workers/prod/backgroundWorker.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
TaskMetadataParseError,
3030
UncaughtExceptionError,
3131
UnexpectedExitError,
32+
getFriendlyErrorMessage,
3233
} from "../common/errors";
3334

3435
type BackgroundWorkerParams = {
@@ -462,6 +463,8 @@ export class ProdBackgroundWorker {
462463
error: {
463464
type: "INTERNAL_ERROR",
464465
code: TaskRunErrorCodes.TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE,
466+
message: getFriendlyErrorMessage(e.code, e.signal, e.stderr),
467+
stackTrace: e.stderr,
465468
},
466469
};
467470
}
@@ -576,6 +579,7 @@ class TaskRunProcess {
576579
private _isBeingKilled: boolean = false;
577580
private _isBeingCancelled: boolean = false;
578581
private _gracefulExitTimeoutElapsed: boolean = false;
582+
private _stderr: Array<string> = [];
579583

580584
/**
581585
* @deprecated use onTaskRunHeartbeat instead
@@ -854,7 +858,13 @@ class TaskRunProcess {
854858
} else if (this._isBeingKilled) {
855859
rejecter(new CleanupProcessError());
856860
} else {
857-
rejecter(new UnexpectedExitError(code ?? -1));
861+
rejecter(
862+
new UnexpectedExitError(
863+
code ?? -1,
864+
signal,
865+
this._stderr.length ? this._stderr.join("\n") : undefined
866+
)
867+
);
858868
}
859869
}
860870
}
@@ -867,7 +877,13 @@ class TaskRunProcess {
867877
}
868878

869879
#handleStdErr(data: Buffer) {
870-
console.error(data.toString());
880+
const text = data.toString();
881+
console.error(text);
882+
883+
if (this._stderr.length > 100) {
884+
this._stderr.shift();
885+
}
886+
this._stderr.push(text);
871887
}
872888

873889
async kill(signal?: number | NodeJS.Signals, timeoutInMs?: number) {

packages/core/src/v3/schemas/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const TaskRunInternalError = z.object({
109109
"TASK_RUN_HEARTBEAT_TIMEOUT",
110110
]),
111111
message: z.string().optional(),
112+
stackTrace: z.string().optional(),
112113
});
113114

114115
export type TaskRunInternalError = z.infer<typeof TaskRunInternalError>;

references/v3-catalog/src/trigger/other.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,28 @@ export const unfriendlyIdTask = task({
8282
console.log("Hello world");
8383
},
8484
});
85+
86+
export const oomTask = task({
87+
id: "oom-task",
88+
machine: {
89+
preset: "micro",
90+
},
91+
run: async () => {
92+
logger.info("running out of memory below this line");
93+
94+
let a = "a";
95+
96+
try {
97+
while (true) {
98+
a += a;
99+
}
100+
} catch (error) {
101+
logger.error(error instanceof Error ? error.message : "Unknown error", { error });
102+
103+
let b = [];
104+
while (true) {
105+
b.push(a.replace(/a/g, "b"));
106+
}
107+
}
108+
},
109+
});

0 commit comments

Comments
 (0)