Skip to content

Commit 278c410

Browse files
committed
fix(cli): stop dev runs crashing when a rebuild removes an in-use build dir
A dev run could crash at boot with a raw MODULE_NOT_FOUND on dev-run-worker.mjs, or hang until timeout, when a rebuild had cleaned up the build directory it was launched against. Verify the worker entry before forking (retryable fail instead of a raw crash), make the watchdog lock-aware so it cannot delete a live session build tree, and fail a run bound to a superseded worker version with a clear message.
1 parent 80cbc46 commit 278c410

4 files changed

Lines changed: 68 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Fixes intermittent `trigger dev` run crashes where a run could fail at boot with a cryptic `Cannot find module .../dev-run-worker.mjs` after a rebuild had cleaned up the build directory the run was launched against. Dev runs now retry cleanly instead of hard-crashing when their build directory is missing, the dev watchdog no longer removes the build tree of a still-running session, and a run assigned to a worker version that was superseded by a rebuild now fails fast with a clear message instead of silently hanging until it times out.

packages/cli-v3/src/dev/devSupervisor.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { tryCatch } from "@trigger.dev/core/utils";
2+
import { TaskRunErrorCodes } from "@trigger.dev/core/v3";
23
import type {
34
BuildManifest,
45
CreateBackgroundWorkerRequestBody,
6+
DequeuedMessage,
57
DevConfigResponseBody,
68
WorkerManifest,
79
} from "@trigger.dev/core/v3";
@@ -224,6 +226,7 @@ class DevSupervisor implements WorkerRuntime {
224226

225227
this.activeRunsPath = join(triggerDir, `active-runs${suffix}.json`);
226228
this.watchdogPidPath = join(triggerDir, `watchdog${suffix}.pid`);
229+
const lockFilePath = join(triggerDir, safeBranch ? `dev.${safeBranch}.lock` : "dev.lock");
227230

228231
// Write empty active-runs file
229232
this.#updateActiveRunsFile();
@@ -249,6 +252,7 @@ class DevSupervisor implements WorkerRuntime {
249252
WATCHDOG_ACTIVE_RUNS: this.activeRunsPath,
250253
WATCHDOG_PID_FILE: this.watchdogPidPath,
251254
WATCHDOG_TMP_DIR: getTmpRoot(this.options.config.workingDir, this.options.branch),
255+
WATCHDOG_LOCK_FILE: lockFilePath,
252256
},
253257
});
254258

@@ -478,7 +482,9 @@ class DevSupervisor implements WorkerRuntime {
478482
}
479483
);
480484

481-
//todo call the API to crash the run with a good message
485+
this.#failRunWithMissingWorker(message).catch((error) => {
486+
logger.debug("[DevSupervisor] Failed to fail run with missing worker", { error });
487+
});
482488
continue;
483489
}
484490

@@ -588,6 +594,33 @@ class DevSupervisor implements WorkerRuntime {
588594
}
589595
}
590596

597+
async #failRunWithMissingWorker(message: DequeuedMessage) {
598+
const start = await this.options.client.dev.startRunAttempt(
599+
message.run.friendlyId,
600+
message.snapshot.friendlyId
601+
);
602+
603+
if (!start.success) {
604+
return;
605+
}
606+
607+
const { run, snapshot, execution } = start.data;
608+
609+
await this.options.client.dev.completeRunAttempt(run.friendlyId, snapshot.friendlyId, {
610+
completion: {
611+
id: execution.run.id,
612+
ok: false,
613+
retry: undefined,
614+
error: {
615+
type: "INTERNAL_ERROR",
616+
code: TaskRunErrorCodes.COULD_NOT_FIND_EXECUTOR,
617+
message:
618+
"This run was assigned to a background worker version that is no longer available in the dev session because it was superseded by a rebuild. Trigger the run again to use the current version.",
619+
},
620+
},
621+
});
622+
}
623+
591624
async #startPresenceConnection() {
592625
try {
593626
const eventSource = this.options.client.dev.presenceConnection();

packages/cli-v3/src/dev/devWatchdog.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const apiKey = process.env.WATCHDOG_API_KEY!;
3434
const activeRunsPath = process.env.WATCHDOG_ACTIVE_RUNS!;
3535
const pidFilePath = process.env.WATCHDOG_PID_FILE!;
3636
const tmpDir = process.env.WATCHDOG_TMP_DIR;
37+
const lockFilePath = process.env.WATCHDOG_LOCK_FILE;
3738

3839
if (!parentPid || !apiUrl || !apiKey || !activeRunsPath || !pidFilePath) {
3940
process.exit(1);
@@ -77,8 +78,25 @@ function cleanup() {
7778
} catch {}
7879
}
7980

81+
function tmpDirOwnedByLiveSession(): boolean {
82+
if (!lockFilePath) return false;
83+
try {
84+
const pid = Number(readFileSync(lockFilePath, "utf8").trim());
85+
if (!pid || pid === parentPid) return false;
86+
try {
87+
process.kill(pid, 0);
88+
return true;
89+
} catch {
90+
return false;
91+
}
92+
} catch {
93+
return false;
94+
}
95+
}
96+
8097
function cleanupTmpDir() {
8198
if (!tmpDir) return;
99+
if (tmpDirOwnedByLiveSession()) return;
82100
try {
83101
rmSync(tmpDir, { recursive: true, force: true });
84102
} catch {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
isOOMRunError,
1515
SuspendedProcessError,
1616
} from "@trigger.dev/core/v3";
17+
import { UnexpectedExitError } from "@trigger.dev/core/v3/errors";
1718
import { type WorkloadRunAttemptStartResponseBody } from "@trigger.dev/core/v3/workers";
1819
import { setTimeout as sleep } from "timers/promises";
1920
import type { CliApiClient } from "../apiClient.js";
@@ -22,6 +23,7 @@ import { assertExhaustive } from "../utilities/assertExhaustive.js";
2223
import { logger } from "../utilities/logger.js";
2324
import { sanitizeEnvVars } from "../utilities/sanitizeEnvVars.js";
2425
import { join } from "node:path";
26+
import { existsSync } from "node:fs";
2527
import type { BackgroundWorker } from "../dev/backgroundWorker.js";
2628
import { eventBus } from "../utilities/eventBus.js";
2729
import type { TaskRunProcessPool } from "../dev/taskRunProcessPool.js";
@@ -600,6 +602,15 @@ export class DevRunController {
600602
throw new Error(`No worker manifest for Dev ${run.friendlyId}`);
601603
}
602604

605+
const workerEntryPoint = this.opts.worker.manifest.workerEntryPoint;
606+
if (!existsSync(workerEntryPoint)) {
607+
throw new UnexpectedExitError(
608+
1,
609+
null,
610+
`Dev worker build directory was removed before the run could start, likely cleaned up by a concurrent rebuild. Missing worker entry: ${workerEntryPoint}`
611+
);
612+
}
613+
603614
this.snapshotPoller.start();
604615

605616
logger.debug("getProcess", {

0 commit comments

Comments
 (0)