fix(llama-cpp): make runtime cleanup best effort

This commit is contained in:
Vincent Koc
2026-08-04 22:15:34 +08:00
parent b04131670a
commit 48b950d9fc
2 changed files with 37 additions and 24 deletions
@@ -155,7 +155,7 @@ beforeEach(() => {
});
afterEach(async () => {
await inferenceRuntime.dispose();
await inferenceRuntime.dispose().catch(() => undefined);
});
describe("llama.cpp inference provider", () => {
@@ -942,16 +942,20 @@ describe("llama.cpp inference provider", () => {
expect(mocks.llamaDispose).toHaveBeenCalledOnce();
});
it("retains failed cleanup ownership so disposal can be retried", async () => {
it("keeps a failed best-effort cleanup as one disposal attempt", async () => {
await collectTestEvents();
mocks.contextDispose.mockRejectedValueOnce(new Error("context cleanup failed"));
await expect(inferenceRuntime.dispose()).rejects.toThrow("context cleanup failed");
expect([mocks.modelDispose.mock.calls.length, mocks.llamaDispose.mock.calls.length]).toEqual([
0, 0,
]);
await inferenceRuntime.dispose();
expect(mocks.contextDispose).toHaveBeenCalledTimes(2);
const firstDisposal = inferenceRuntime.dispose();
await expect(firstDisposal).rejects.toThrow(
"llama.cpp runtime cleanup failed: Error: context cleanup failed",
);
expect(mocks.modelDispose).toHaveBeenCalledOnce();
expect(mocks.llamaDispose).toHaveBeenCalledOnce();
const repeatedDisposal = inferenceRuntime.dispose();
expect(repeatedDisposal).toBe(firstDisposal);
await expect(repeatedDisposal).rejects.toThrow("context cleanup failed");
expect(mocks.contextDispose).toHaveBeenCalledOnce();
expect(mocks.modelDispose).toHaveBeenCalledOnce();
expect(mocks.llamaDispose).toHaveBeenCalledOnce();
});
+25 -16
View File
@@ -305,33 +305,42 @@ async function serialize(
}
function disposeLlamaCppInferenceRuntime(state: LlamaCppInferenceRuntimeState): Promise<void> {
if (state.lifecycle === "closed") {
return Promise.resolve();
}
if (state.disposePromise) {
return state.disposePromise;
}
state.lifecycle = "closing";
const attempt = serialize(state, async () => {
await disposeLoadedModel(state);
// Plugin services stop once, and node-llama-cpp disposers mark themselves
// disposed before awaiting cleanup. The first disposal attempt is authoritative.
state.disposePromise = serialize(state, async () => {
const errors: unknown[] = [];
const attemptDispose = async (dispose: () => Promise<void>) => {
try {
await dispose();
} catch (error) {
errors.push(error);
}
};
if (state.loadedModel) {
const previous = state.loadedModel;
await attemptDispose(() => previous.context.dispose());
await attemptDispose(() => previous.model.dispose());
if (state.loadedModel === previous) {
state.loadedModel = undefined;
}
}
if (state.llamaInstance) {
const previous = state.llamaInstance;
await previous.dispose();
await attemptDispose(() => previous.dispose());
if (state.llamaInstance === previous) {
state.llamaInstance = undefined;
}
}
if (errors.length > 0) {
throw new AggregateError(errors, `llama.cpp runtime cleanup failed: ${String(errors[0])}`);
}
}).finally(() => {
state.lifecycle = "closed";
});
state.disposePromise = attempt.then(
() => {
state.lifecycle = "closed";
state.disposePromise = undefined;
},
(error: unknown) => {
state.disposePromise = undefined;
throw error;
},
);
return state.disposePromise;
}