From 0bf161f1bcd12804bb375607097e0edf0560fd81 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Nov 2026 10:35:05 +0000 Subject: [PATCH] fix(kuna): resolve the function deadline before taking a run slot An invalid REA_KUNA_TIMEOUT_MS throws once its runtime override is reset. That check now happens before either shared-slot acquisition, so a failed independent decompile cannot leave the provider-wide pool blocked. --- src/config/kunaFunctionRunAdmission.test.ts | 61 +++++++++++++++++++++ src/kuna/KunaSessionClient.ts | 48 ++++++++++++++-- 2 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/config/kunaFunctionRunAdmission.test.ts diff --git a/src/config/kunaFunctionRunAdmission.test.ts b/src/config/kunaFunctionRunAdmission.test.ts new file mode 100644 index 0000000000..4686ae3495 --- /dev/null +++ b/src/config/kunaFunctionRunAdmission.test.ts @@ -0,0 +1,61 @@ +import { setImmediate } from "node:timers/promises"; + +import { expect, it } from "vitest"; + +import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js"; +import type { KunaConfiguration } from "../kuna/KunaConfiguration.js"; +import { KunaRunSlots } from "../kuna/KunaRunSlots.js"; +import { reserveKunaFunctionRun } from "../kuna/KunaSessionClient.js"; + +const configuration = { + command: "/usr/bin/kuna", + cacheRoot: "/tmp/kuna-cache", + cacheMaxBytes: 4 * 1024 ** 3, + projectTimeoutMs: 30 * 60 * 1000, + projectTimeoutSource: "default", + projectTimeoutLaunchValid: true, + functionTimeoutMs: 120_000, + functionTimeoutSource: "default", + functionTimeoutLaunchValid: false, + jobs: 1, +} satisfies KunaConfiguration; + +it("does not take a run slot when the function deadline is no longer valid", async () => { + const slots = new KunaRunSlots(1); + const signal = new AbortController().signal; + await expect( + reserveKunaFunctionRun(slots, signal, configuration), + ).rejects.toThrow( + /REA_KUNA_TIMEOUT_MS is not a valid launch seed and no runtime override is active/, + ); + const released = await slots.acquire(AbortSignal.timeout(50)); + expect(released).toEqual(expect.any(Function)); + released?.(); +}); + +it("releases the function-run slot after the accepted deadline is finished", async () => { + const slots = new KunaRunSlots(1); + const signal = new AbortController().signal; + const reserved = await runWithAnalysisOperationSnapshot( + { + revision: 1, + overrides: { "kuna.function_timeout_ms": 5000 }, + }, + () => reserveKunaFunctionRun(slots, signal, configuration), + ); + expect(reserved?.deadline).toEqual({ + setting: "REA_KUNA_TIMEOUT_MS", + ms: 5000, + source: "runtime", + }); + const next = slots.acquire(AbortSignal.timeout(50)); + let granted = false; + void next.then((release) => { + granted = release !== undefined; + }); + await setImmediate(); + expect(granted).toBe(false); + reserved?.release(); + expect(await next).toEqual(expect.any(Function)); + (await next)?.(); +}); diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 4030313cfb..2a84adb912 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -362,6 +362,24 @@ const procedureSelectorSchema = z.string().min(1); * the background; assembly waits for it. Kuna always runs on a private copy * whose digest matches the selected target. */ +/** + * Resolve the function deadline before taking a shared run slot. A throw from + * an invalid launch seed must not leave the slot acquired. + */ +export const reserveKunaFunctionRun = async ( + slots: KunaRunSlots, + signal: AbortSignal, + configuration: KunaConfiguration, +): Promise< + | { readonly deadline: KunaDeadline; readonly release: KunaRunRelease } + | undefined +> => { + const deadline = kunaFunctionDeadline(configuration); + const release = await slots.acquire(signal); + if (release === undefined) return undefined; + return { deadline, release }; +}; + export class KunaSessionClient implements AnalysisClient { readonly #target: BinaryTarget; readonly #profile: AnalysisProfileCommitment | undefined; @@ -1604,9 +1622,31 @@ export class KunaSessionClient implements AnalysisClient { ): Promise { const prepared = await this.#commands.prepare(); if (!prepared.ok) return prepared; - const admitted = independent - ? await this.#runSlots.acquire(signal) - : await this.#admitSingleFunction(signal); + if (independent) { + const reserved = await reserveKunaFunctionRun( + this.#runSlots, + signal, + installation.configuration, + ); + if (reserved === undefined) + return err( + new AnalysisCancelledError(operation, { cause: signal.reason }), + ); + const run = await this.#commands.run({ + installation, + arguments: (input) => ["decompile", input, address, "--addr"], + operation, + deadline: reserved.deadline, + outputBytes: KUNA_FUNCTION_TEXT_BYTES, + signal, + releaseRun: reserved.release, + }); + return run.ok ? ok(run.value.stdout.text.replace(/\n+$/u, "")) : run; + } + // The shared slot is taken only after the deadline is known. A throw from + // a reset override must not leave that slot acquired. + const deadline = kunaFunctionDeadline(installation.configuration); + const admitted = await this.#admitSingleFunction(signal); if (admitted === EXPORT_READY) return admitted; if (admitted === undefined) return err( @@ -1616,7 +1656,7 @@ export class KunaSessionClient implements AnalysisClient { installation, arguments: (input) => ["decompile", input, address, "--addr"], operation, - deadline: kunaFunctionDeadline(installation.configuration), + deadline, outputBytes: KUNA_FUNCTION_TEXT_BYTES, signal, releaseRun: admitted,