mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
fix(sourcemaps): bind cleanup status to remaining codec owners
This commit is contained in:
@@ -154,6 +154,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
readonly #runId = `rea-source-map-${randomUUID()}`;
|
||||
#runtime: SourceMapCodecRuntime | undefined;
|
||||
#supervisor: ProviderProcessSupervisor | undefined;
|
||||
#cleanupAnchor: SourceMapCleanupFailure | undefined;
|
||||
#outcome: CodecOutcome | undefined;
|
||||
#drainStop: CodecPhysicalObservation | undefined;
|
||||
readonly #onInterruption: () => void;
|
||||
@@ -462,6 +463,14 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
);
|
||||
}
|
||||
|
||||
/** Current ownership, independent of historical cleanup diagnostics. */
|
||||
get cleanupResources(): readonly string[] {
|
||||
return [
|
||||
...(this.#supervisor === undefined ? [] : [this.#runId]),
|
||||
...(this.#runtime === undefined ? [] : [this.#runtime.path]),
|
||||
];
|
||||
}
|
||||
|
||||
async close(
|
||||
previousError: string | null,
|
||||
): Promise<ProviderCleanupError | undefined> {
|
||||
@@ -483,21 +492,20 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
failures.push(message(cause));
|
||||
}
|
||||
}
|
||||
return failures.length === 0
|
||||
? undefined
|
||||
: new SourceMapCleanupFailure(
|
||||
this,
|
||||
[
|
||||
this.#runId,
|
||||
...(this.#runtime === undefined ? [] : [this.#runtime.path]),
|
||||
],
|
||||
{
|
||||
map_path: this.path,
|
||||
cleanup_failures: failures,
|
||||
previous_error: previousError,
|
||||
codec_pid: this.#supervisor?.launch.process.pid ?? null,
|
||||
},
|
||||
);
|
||||
if (failures.length === 0) {
|
||||
this.#cleanupAnchor?.resolveCleanup();
|
||||
this.#cleanupAnchor = undefined;
|
||||
return undefined;
|
||||
}
|
||||
const failure = new SourceMapCleanupFailure(this, this.cleanupResources, {
|
||||
map_path: this.path,
|
||||
cleanup_failures: failures,
|
||||
previous_error: previousError,
|
||||
codec_pid: this.#supervisor?.launch.process.pid ?? null,
|
||||
}).bindCleanupResourceOwner(this);
|
||||
if (this.#cleanupAnchor === undefined) this.#cleanupAnchor = failure;
|
||||
else failure.bindCleanupDependencies([this.#cleanupAnchor], this);
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
const message = (cause: unknown): string =>
|
||||
|
||||
@@ -2,12 +2,14 @@ import { access } from "node:fs/promises";
|
||||
import { expect, it } from "vitest";
|
||||
import { SourceMapDecoder } from "./SourceMapDecoder.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
SourceMapCleanupFailure,
|
||||
type SourceMapDecoderDependencies,
|
||||
} from "./SourceMapCommand.js";
|
||||
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
|
||||
import { spawnOwnedProviderProcess } from "../../process/ProviderProcess.js";
|
||||
import { cleanupOwnedProcessGroup } from "../../process/ProcessOwnership.js";
|
||||
import {
|
||||
webSourceLocationArgs,
|
||||
webSourceLocationFixture,
|
||||
@@ -69,6 +71,7 @@ it("retains failed cleanup ownership and retries it before another decode", asyn
|
||||
if (!(failed.error instanceof SourceMapCleanupFailure))
|
||||
throw new Error("expected source-map cleanup ownership");
|
||||
expect(failed.error.cleanupOwner).toBeDefined();
|
||||
expect(failed.error.cleanupResources).toEqual([root.path]);
|
||||
expect(failed.error.diagnostics).toMatchObject({
|
||||
cleanup_failures: ["injected root cleanup failure"],
|
||||
});
|
||||
@@ -87,7 +90,119 @@ it("retains failed cleanup ownership and retries it before another decode", asyn
|
||||
expect(retried.error._tag).toBe("AnalysisInputError");
|
||||
expect(cleanupAttempts).toBe(2);
|
||||
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(failed.error.cleanupIncomplete).toBe(false);
|
||||
expect(failed.error.cleanupResources).toEqual([]);
|
||||
expect(projectAnalysisError(failed.error).code).not.toBe(
|
||||
"cleanup_incomplete",
|
||||
);
|
||||
expect(failed.error.diagnostics).toMatchObject({
|
||||
cleanup_failures: ["injected root cleanup failure"],
|
||||
});
|
||||
} finally {
|
||||
await decoder.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("settles every source-map cleanup failure when final close releases the exact runtime", async () => {
|
||||
const root = await PrivateRuntimeRoot.create({
|
||||
prefix: "rea-source-map-final-cleanup-test-",
|
||||
});
|
||||
let attempts = 0;
|
||||
const decoder = new SourceMapDecoder({
|
||||
prepareOwnership: async () => {},
|
||||
createRuntime: async () => ({
|
||||
path: root.path,
|
||||
close: async () => {
|
||||
attempts += 1;
|
||||
if (attempts <= 2)
|
||||
throw new Error(`runtime cleanup attempt ${String(attempts)} failed`);
|
||||
await root.close();
|
||||
},
|
||||
}),
|
||||
launcher: async () => {
|
||||
throw new Error("controlled launch failure");
|
||||
},
|
||||
});
|
||||
try {
|
||||
const initial = await decoder.trace(input);
|
||||
if (initial.ok) throw new Error("expected initial cleanup failure");
|
||||
const retry = await decoder.trace(input);
|
||||
if (retry.ok) throw new Error("expected retry cleanup failure");
|
||||
for (const failure of [initial.error, retry.error]) {
|
||||
expect(failure.cleanupIncomplete).toBe(true);
|
||||
expect(failure.cleanupResources).toEqual([root.path]);
|
||||
}
|
||||
await expect(access(root.path)).resolves.toBeUndefined();
|
||||
await decoder.close();
|
||||
expect(attempts).toBe(3);
|
||||
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
for (const failure of [initial.error, retry.error]) {
|
||||
expect(failure.cleanupIncomplete).toBe(false);
|
||||
expect(failure.cleanupResources).toEqual([]);
|
||||
expect(projectAnalysisError(failure).code).not.toBe("cleanup_incomplete");
|
||||
expect(failure.diagnostics?.cleanup_failures).toEqual([
|
||||
expect.stringContaining("runtime cleanup attempt"),
|
||||
]);
|
||||
}
|
||||
} finally {
|
||||
await decoder.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("attributes cleanup resources to the remaining process and directory owners", async () => {
|
||||
const root = await PrivateRuntimeRoot.create({
|
||||
prefix: "rea-source-map-mixed-cleanup-test-",
|
||||
});
|
||||
let denyProcessRelease = true;
|
||||
let rootAttempts = 0;
|
||||
let runId: string | undefined;
|
||||
const decoder = new SourceMapDecoder({
|
||||
prepareOwnership: async () => {},
|
||||
createRuntime: async () => ({
|
||||
path: root.path,
|
||||
close: async () => {
|
||||
rootAttempts += 1;
|
||||
if (rootAttempts === 1)
|
||||
throw new Error("controlled directory cleanup failure");
|
||||
await root.close();
|
||||
},
|
||||
}),
|
||||
launcher: async (options) => {
|
||||
runId = options.runId;
|
||||
const spawned = await spawnOwnedProviderProcess({
|
||||
...options,
|
||||
arguments: ["-e", "process.exit(0)"],
|
||||
});
|
||||
return {
|
||||
...spawned,
|
||||
cleanup: async () =>
|
||||
denyProcessRelease
|
||||
? { cleaned: false, reason: "controlled process cleanup failure" }
|
||||
: spawned.cleanup === undefined
|
||||
? cleanupOwnedProcessGroup(spawned.ownership)
|
||||
: spawned.cleanup(),
|
||||
};
|
||||
},
|
||||
});
|
||||
try {
|
||||
const initial = await decoder.trace(input);
|
||||
if (initial.ok) throw new Error("expected process cleanup failure");
|
||||
expect(initial.error.cleanupResources).toEqual([runId, root.path]);
|
||||
expect(rootAttempts).toBe(0);
|
||||
denyProcessRelease = false;
|
||||
await expect(decoder.close()).rejects.toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
});
|
||||
expect(rootAttempts).toBe(1);
|
||||
expect(initial.error.cleanupIncomplete).toBe(true);
|
||||
expect(initial.error.cleanupResources).toEqual([root.path]);
|
||||
await expect(access(root.path)).resolves.toBeUndefined();
|
||||
await decoder.close();
|
||||
expect(initial.error.cleanupIncomplete).toBe(false);
|
||||
expect(initial.error.cleanupResources).toEqual([]);
|
||||
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
denyProcessRelease = false;
|
||||
await decoder.close();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user