Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* @module extensions/ext-bundler-esbuild/esbuild-bundler.test
*/

import { assertEquals, assertExists, assertRejects, assertStringIncludes } from "@std/assert";
import {
assertEquals,
assertExists,
assertRejects,
assertStringIncludes,
} from "#veryfront/testing/assert.ts";
import { afterEach, beforeEach, describe, it } from "#veryfront/testing/bdd.ts";
import { createRequire } from "node:module";
import type { BuildContext } from "veryfront/extensions/bundler";
Expand Down Expand Up @@ -1479,6 +1484,68 @@ describe("EsbuildBundler service crash recovery", () => {
}
});

it("allows the final budgeted stop loss before the child close event clears tracking", async () => {
const observation = observeEsbuildServices();
const { services } = observation;
const bundler = new EsbuildBundler();
let finalStop: Promise<void> | undefined;
const finalStopStarted = Promise.withResolvers<void>();

try {
await bundler.transform({ code: "export const seed: number = 0;", loader: "ts" });

for (let restart = 1; restart < MAX_SERVICE_RESTARTS; restart++) {
const current = services[services.length - 1]!;
current.child.ref();
current.child.kill("SIGKILL");
await current.close;

await bundler.stop();
const recovered = await bundler.transform({
code: `export const stopReset${restart}: number = ${restart};`,
loader: "ts",
});
assertStringIncludes(recovered.code, `stopReset${restart} = ${restart}`);
Comment thread
kojiwakayama marked this conversation as resolved.
}

const finalBudgeted = services[services.length - 1]!;
finalBudgeted.child.once("exit", () => {
finalStop = bundler.stop();
finalStopStarted.resolve();
});
finalBudgeted.child.ref();
finalBudgeted.child.kill("SIGKILL");

await finalStopStarted.promise;
await finalStop;
await finalBudgeted.close;

const recovered = await bundler.transform({
code: "export const afterFinalStopLoss: number = 3;",
loader: "ts",
});
assertStringIncludes(recovered.code, "afterFinalStopLoss = 3");

const exhausted = services[services.length - 1]!;
exhausted.child.ref();
exhausted.child.kill("SIGKILL");
await exhausted.close;

const stopError = await assertRejects(() => bundler.stop());
assertStringIncludes((stopError as Error).message, "module-wide adapter");
assertStringIncludes((stopError as Error).message, "exited unexpectedly");
} finally {
await finalStop?.catch(() => undefined);
await bundler.stop().catch(() => undefined);
__resetServiceRecoveryForTests();
try {
await bundler.stop();
} finally {
observation.restore();
}
}
});

it("latches exhaustion when stop observes a closed lost service", async () => {
const observation = observeEsbuildServices();
const { services } = observation;
Expand Down
4 changes: 3 additions & 1 deletion extensions/ext-bundler-esbuild/src/esbuild-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,16 +816,18 @@ export class EsbuildBundler implements Bundler {
uninstallServiceLossSpawnGuard();
throw error;
}
let chargedLostService = false;
if (esbuildServiceLost) {
remainingServiceRestarts -= 1;
chargedLostService = true;
uninstallServiceLossSpawnGuard();
}

const m = esbuildModule;
const trackedService = esbuildService;
if (
trackedService && !trackedService.expectedClose && !isLiveService(trackedService) &&
remainingServiceRestarts <= 0
remainingServiceRestarts <= 0 && !chargedLostService
) {
// A dead managed child within the restart budget is a crash, which a
// stop resets anyway; only an exhausted budget still means giving up.
Expand Down