fix(session): report retained file and candidate cleanup
Report cleanup from the owning snapshot and provider-candidate resource scopes without retrying or starting work. Keep status accurate before a client exists and through cleanup retry. Refs #4277, #4274
This commit is contained in:
1 parent
93c620c3e7
commit
839410b663
10 files changed
+110
-9
No files matched your search
@@ -404,13 +404,14 @@ at that bounded check. These are historical snapshots, not live process
|
||||
inventories, and do not claim that no short-lived descendant existed.
|
||||
|
||||
`binary_session.cleanup_pending` reports whether the session retains a provider
|
||||
client whose cleanup has not succeeded. It can be true with `open: false`;
|
||||
target retirement does not establish resource release. Health, activity, and
|
||||
lineage remain available under that client's original `analysis_run.run_id`
|
||||
where the provider supplies them. Successful cleanup clears the retained run;
|
||||
a replacement client receives a fresh run ID. Follow the cleanup error's
|
||||
reported resources and recovery action before reopening; a missing health
|
||||
observation does not establish an idle provider.
|
||||
client, session file resource, or provider-candidate resource whose cleanup has
|
||||
not succeeded. It can be true with `open: false`; target retirement does not
|
||||
establish resource release. Health, activity, and lineage remain available
|
||||
under a retained client's original `analysis_run.run_id` where the provider
|
||||
supplies them. Successful client cleanup clears the retained run; a replacement
|
||||
client receives a fresh run ID. Follow the cleanup error's reported resources
|
||||
and recovery action before reopening; a missing health observation does not
|
||||
establish an idle provider.
|
||||
|
||||
## Progress and cancellation
|
||||
|
||||
|
||||
@@ -290,6 +290,8 @@ export interface AnalysisProviderCandidate extends AnalysisProvider {
|
||||
inspectAvailability(): ProviderAvailability;
|
||||
inspectReadinessSnapshot?(): AnalysisProviderReadinessSnapshot;
|
||||
inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport;
|
||||
/** Report retained candidate-owned cleanup without starting work or retrying it. */
|
||||
hasPendingOwnedResources?(): boolean;
|
||||
/** Release candidate-owned resources that may exist without a client. */
|
||||
closeOwnedResources?(): Promise<Result<null, AnalysisError>>;
|
||||
}
|
||||
@@ -136,6 +136,13 @@ export class AnalysisProviderRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
/** Report retained candidate resources without retrying cleanup. */
|
||||
hasPendingOwnedResources(): boolean {
|
||||
return this.#providers.some(
|
||||
(provider) => provider.hasPendingOwnedResources?.() === true,
|
||||
);
|
||||
}
|
||||
|
||||
/** Close candidate-owned resources even when no target client was opened. */
|
||||
async closeOwnedResources(): Promise<Result<null, AnalysisError>> {
|
||||
const failures: AnalysisError[] = [];
|
||||
|
||||
@@ -594,7 +594,10 @@ export class BinarySession
|
||||
runtimeLineageSnapshots: client?.runtimeLineageSnapshots?.() ?? [],
|
||||
requestActivitySnapshots: client?.requestActivitySnapshots?.() ?? [],
|
||||
providerOperationHealth: client?.operationHealthSnapshot?.() ?? null,
|
||||
cleanupPending: this.#pendingCleanup !== undefined,
|
||||
cleanupPending:
|
||||
this.#pendingCleanup !== undefined ||
|
||||
this.hasPendingWriteResources ||
|
||||
this.#providerRouter.hasPendingOwnedResources(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@ export abstract class BinarySessionRecords {
|
||||
this.#records = records;
|
||||
}
|
||||
|
||||
/** Snapshot staging retained after a failed resource cleanup attempt. */
|
||||
protected get hasPendingWriteResources(): boolean {
|
||||
return this.#writeResources.hasPending;
|
||||
}
|
||||
|
||||
/** Observe changes to the mutable current analysis snapshot resource. */
|
||||
onAnalysisSnapshotChanged(listener: () => void | Promise<void>): () => void {
|
||||
this.#snapshotListeners.add(listener);
|
||||
|
||||
@@ -75,6 +75,11 @@ export class SessionProviderRouter {
|
||||
return this.registry.closeOwnedResources();
|
||||
}
|
||||
|
||||
/** Report retained candidate cleanup without performing cleanup work. */
|
||||
hasPendingOwnedResources(): boolean {
|
||||
return this.registry.hasPendingOwnedResources();
|
||||
}
|
||||
|
||||
/** Resolve target routes before any provider client is created. */
|
||||
async resolve(
|
||||
target: BinaryTarget,
|
||||
|
||||
@@ -248,6 +248,15 @@ export class KunaProjectCache {
|
||||
this.#launcher = options.launcher;
|
||||
}
|
||||
|
||||
/** Report cleanup owners retained after failed process or file cleanup. */
|
||||
get hasPendingCleanup(): boolean {
|
||||
return (
|
||||
this.#retainedProcesses.length > 0 ||
|
||||
this.#retainedFiles.size > 0 ||
|
||||
this.#retainedHashHandles.size > 0
|
||||
);
|
||||
}
|
||||
|
||||
/** Keep an entry from eviction while this process serves it. */
|
||||
pin(key: string): () => void {
|
||||
this.#pins.set(key, (this.#pins.get(key) ?? 0) + 1);
|
||||
|
||||
@@ -298,6 +298,14 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
);
|
||||
}
|
||||
|
||||
/** Report retained file cleanup without probing readiness or retrying owners. */
|
||||
hasPendingOwnedResources(): boolean {
|
||||
return (
|
||||
this.#readinessFileOwners.size > 0 ||
|
||||
this.#cache?.hasPendingCleanup === true
|
||||
);
|
||||
}
|
||||
|
||||
async #closeReadinessOwners() {
|
||||
const failures: { readonly path: string; readonly cause: unknown }[] = [];
|
||||
for (const [owner, path] of this.#readinessFileOwners) {
|
||||
|
||||
@@ -32,6 +32,8 @@ import { readJsonObjectFile } from "../../../src/application/JsonInputFile.js";
|
||||
import { RegularFileChangedError } from "../../../src/filesystem/RegularFile.js";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { RegularFileCleanupFailure } from "../../../src/application/RegularFileRead.js";
|
||||
import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
|
||||
const snapshotFixture = (): AnalysisSnapshot => {
|
||||
const target = snapshotTarget(TARGET);
|
||||
@@ -76,6 +78,50 @@ const snapshotFixture = (): AnalysisSnapshot => {
|
||||
};
|
||||
|
||||
describe("analysis snapshots: persistence", () => {
|
||||
it("reports retained snapshot staging without an active provider and clears it after retry", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-snapshot-cleanup-status-",
|
||||
);
|
||||
const path = join(directory, "analysis.json");
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Snapshot persistence must not create a provider client");
|
||||
});
|
||||
const close = vi
|
||||
.spyOn(OwnedStagedOutput.prototype, "close")
|
||||
.mockRejectedValue(new Error("Controlled staging cleanup failure"));
|
||||
onTestFinished(async () => {
|
||||
close.mockRestore();
|
||||
const closed = await session.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
});
|
||||
|
||||
const written = await session.writeAnalysisSnapshot(
|
||||
snapshotFixture(),
|
||||
path,
|
||||
false,
|
||||
);
|
||||
expect(written).toMatchObject({
|
||||
ok: false,
|
||||
error: { cleanupIncomplete: true, published: { path } },
|
||||
});
|
||||
expect(
|
||||
parseAnalysisSnapshot(JSON.parse(await readFile(path, "utf8"))),
|
||||
).toEqual(snapshotFixture());
|
||||
expect(session.status()).toMatchObject({
|
||||
open: false,
|
||||
cleanup_pending: true,
|
||||
});
|
||||
expect(await session.close()).toMatchObject({
|
||||
ok: false,
|
||||
error: { cleanupIncomplete: true },
|
||||
});
|
||||
expect(session.status()).toMatchObject({ cleanup_pending: true });
|
||||
|
||||
close.mockRestore();
|
||||
expect(await session.close()).toEqual({ ok: true, value: null });
|
||||
expect(session.status()).toMatchObject({ cleanup_pending: false });
|
||||
});
|
||||
|
||||
it("writes canonical private v2 JSON and rejects changed query identity", async () => {
|
||||
const directory = await createTestTempDirectory("rea-snapshot-");
|
||||
const path = join(directory, "analysis.json");
|
||||
|
||||
@@ -126,10 +126,25 @@ it.skipIf(process.platform === "win32")(
|
||||
cleanupResources: [fixture.kuna],
|
||||
});
|
||||
expect(injected.closeCalls).toBe(2);
|
||||
expect(binary.status()).toMatchObject({
|
||||
open: false,
|
||||
cleanup_pending: true,
|
||||
});
|
||||
|
||||
expect(await binary.close()).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
cleanupIncomplete: true,
|
||||
cleanupResources: [fixture.kuna],
|
||||
},
|
||||
});
|
||||
expect(injected.closeCalls).toBe(3);
|
||||
expect(binary.status()).toMatchObject({ cleanup_pending: true });
|
||||
|
||||
injected.releaseCloseFailures = true;
|
||||
expect(await binary.close()).toEqual({ ok: true, value: null });
|
||||
expect(injected.closeCalls).toBe(3);
|
||||
expect(injected.closeCalls).toBe(4);
|
||||
expect(binary.status()).toMatchObject({ cleanup_pending: false });
|
||||
expect((await fakeKunaCalls(fixture.kuna)).map(({ argv }) => argv)).toEqual(
|
||||
[["--version"], ["--version"]],
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user