fix(kuna): retain failed inventory admission as result

This commit is contained in:
morluto committed 2026-11-11 20:16:24 +08:00
1 parent c4e3e48574
commit 2aa5ba836d
2 files changed
+113 -5

No files matched your search

+27 -5
View File
@@ -24,6 +24,7 @@ import {
type RetainedResourceScope,
} from "../application/AnalysisProvider.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
@@ -2405,10 +2406,18 @@ export class KunaSessionClient implements AnalysisClient {
// The session owns the shared run; each caller only stops waiting.
const run = this.#listInventory(installation);
this.#inventory = run;
void run.then((listed) => {
// A failed listing is retried by the next request.
if (!listed.ok && this.#inventory === run) this.#inventory = undefined;
});
void run.then(
(listed) => {
// A failed listing is retried by the next request.
if (!listed.ok && this.#inventory === run)
this.#inventory = undefined;
},
() => {
// Unexpected rejections remain visible to waiters but cannot poison
// the shared cache or create a second unhandled rejection.
if (this.#inventory === run) this.#inventory = undefined;
},
);
}
return waitFor(this.#inventory, options.signal, "list_procedures");
}
@@ -2427,11 +2436,24 @@ export class KunaSessionClient implements AnalysisClient {
if (this.#reader !== undefined) return ok(this.#reader.project.inventory);
const exported = await this.#exportInventory;
if (exported !== undefined) return ok(exported);
if (this.#session.signal.aborted)
return err(
new AnalysisCancelledError("list_procedures", {
cause: this.#session.signal.reason,
}),
);
let deadline: KunaDeadline;
try {
deadline = kunaFunctionDeadline(installation.configuration);
} catch (cause: unknown) {
if (cause instanceof ConfigurationError) return err(cause);
throw cause;
}
const run = await this.#commands.run({
installation,
arguments: (input) => ["functions", input, "--json"],
operation: "list_procedures",
deadline: kunaFunctionDeadline(installation.configuration),
deadline,
outputBytes: KUNA_INVENTORY_OUTPUT_BYTES,
signal: this.#session.signal,
});
@@ -0,0 +1,86 @@
import { expect, it as test } from "vitest";
import { runWithAnalysisOperationSnapshot } from "../../../../src/domain/analysisOperationSnapshot.js";
import { session } from "./kunaSessionFixture.js";
const it = test.skipIf(process.platform === "win32");
it("retries inventory after a reset exposes an invalid function deadline", async () => {
const snapshot = {
revision: 1,
overrides: { "kuna.function_timeout_ms": 1000 },
} as const;
const fixture = await runWithAnalysisOperationSnapshot(snapshot, () =>
session(
"rea-kuna-deadline-inventory-",
{},
{ REA_KUNA_TIMEOUT_MS: "invalid" },
),
);
const client = runWithAnalysisOperationSnapshot(snapshot, () =>
fixture.open(),
);
try {
const ready = await runWithAnalysisOperationSnapshot(snapshot, () =>
client.execute("health", {}),
);
expect(ready.ok).toBe(true);
const refused = await client.execute("list_procedures", {});
expect(refused).toMatchObject({
ok: false,
error: { _tag: "ConfigurationError" },
});
const retried = await runWithAnalysisOperationSnapshot(snapshot, () =>
client.execute(
"list_procedures",
{},
{ signal: AbortSignal.timeout(3000) },
),
);
expect(retried).toMatchObject({ ok: true });
} finally {
await client.close();
}
});
it.each([
["list_strings", {}],
["procedure_callers", { procedure: "main" }],
["xrefs", { address: "0x1000" }],
] as const)(
"retries %s after a reset exposes an invalid project deadline",
async (operation, parameters) => {
const snapshot = {
revision: 1,
overrides: { "kuna.project_timeout_ms": 1000 },
} as const;
const fixture = await runWithAnalysisOperationSnapshot(snapshot, () =>
session(
"rea-kuna-deadline-query-",
{},
{ REA_KUNA_PROJECT_TIMEOUT_MS: "invalid" },
),
);
const client = runWithAnalysisOperationSnapshot(snapshot, () =>
fixture.open(),
);
try {
const ready = await runWithAnalysisOperationSnapshot(snapshot, () =>
client.execute("health", {}),
);
expect(ready.ok).toBe(true);
const refused = await client.execute(operation, parameters);
expect(refused).toMatchObject({
ok: false,
error: { _tag: "ConfigurationError" },
});
const retried = await runWithAnalysisOperationSnapshot(snapshot, () =>
client.execute(operation, parameters, {
signal: AbortSignal.timeout(3000),
}),
);
expect(retried).toMatchObject({ ok: true });
} finally {
await client.close();
}
},
);