Files
Codeman/test/local-model-supervisor.test.ts
T
Codeman maintainer 2cd3cbd6a0 fix(local-model): count what llama-server keeps beside the K/V cache, and bound its checkpoints
The llama.cpp memory rule of the catalogue left out two allocations that the llama.cpp the
runtime bundles (b12111, the runtime's LLAMA_CPP_VERSION) makes for every GGUF row:

- Context checkpoints. For a model whose state cannot be rolled back (the recurrent state of
  the Qwen-family rows, the sliding window of North Mini Code), llama-server keeps host copies
  of that state, two or three per request, up to 32 per slot by default
  (tools/server/server-context.cpp create_checkpoint). For the 27B that is 32 x 157 MB = 5 GB,
  none of which was counted. The runtime now sets LLAMA_ARG_CTX_CHECKPOINTS=8 (read by
  llama-server from its environment like LLAMA_ARG_CACHE_RAM; 8 was llama.cpp's own default
  before it went to 32), and every llamacpp row counts 8 copies of its state.
- Multi-token prediction. The two -mtp tags and qwen3.8:27b-q8_0 carry a prediction layer
  (nextn_predict_layers 1, read from the first bytes of each GGUF; the 9B tags and North Mini
  Code have none). The runtime then always passes --spec-type draft-mtp --spec-draft-n-max 4,
  so llama.cpp keeps 4 rollback copies of the recurrent state (need_n_rs_seq(),
  llama-memory-recurrent.cpp) and the prediction layer's cache in the draft context's cache
  type, f16 by default. The rows counted one state and that layer at q8_0.

What moves (test/local-model-recommend.test.ts, "every memory size", re-pinned):

- 16 GB: limited -> none. MiMo 9B at 65,536 tokens needs 10.2 GB, 0.987 of the 10.4 GB
  estimate. With the budget the Mac mini measured (10.7 GB after the reserve) it is offered,
  0.954, tight; that test is kept. With 4 checkpoints instead of 8 it would be 0.966 on the
  estimate: this size sits on the edge, and only a run on such a Mac settles it.
- 18 GB: the same pick, 0.964 instead of 0.928.
- 36 GB: the same picks, 0.966 (27B on M4/M5 Max) and 0.976 (35B-A3B GGUF on M3).
- 48 GB on a Max chip: the 27B at 262,144 tokens is tight (0.853) instead of good.
- 32 GB with a measured budget of 0.74: North Mini Code at 131,072 tokens (was 262,144), the
  27B at 65,536 on a Max chip (was 131,072).
- Every MLX row, and everything from 64 GB up, is unchanged.

test/local-model-catalog.test.ts recomputes the new rule from the architecture table and holds
the catalogue to LLAMA_ARG_CTX_CHECKPOINTS. The catalogue header, runtime.ts, the types,
docs/local-models.md, CLAUDE.md and architecture-invariants say the same. Read in the source,
not measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 10:26:57 +01:00

910 lines
38 KiB
TypeScript

/**
* @fileoverview Tests for the supervisor of src/local-model/runtime.ts: how Codeman starts,
* watches, restarts and stops its model runtime.
*
* Nothing real runs here. The child process is the fake of test/fixtures/fake-ollama.ts
* (`fake.spawn`): "starting" it makes the fake listen on the port of `OLLAMA_HOST` and write
* the runtime's start-up log, and killing it closes that listener.
*
* Pinned:
* - Start: one non-detached child, `<runtime folder>/ollama serve`, with the allowlisted
* environment; it is ready when `/api/version` answers the pinned version; the device total
* of its log is reported as the measured budget.
* - The port: a listener that is not ours is `port-in-use` and is never signalled; our own
* orphan (the stored pid runs a binary under our runtime folder) is adopted when it holds a
* loaded model at the wanted context, and replaced otherwise.
* - A child that exits by itself is restarted at most 3 times, then given up.
* - Stop: SIGTERM, and nothing is restarted afterwards.
* - Under vitest nothing spawns unless the test injects it.
*
* Port: none fixed (each test reserves a free loopback port).
*/
import { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import { createServer, type Server } from 'node:net';
import type { ChildProcess, SpawnOptions } from 'node:child_process';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { localModelPaths, runtimeBinaryForVersion, type LocalModelPaths } from '../src/local-model/paths.js';
import {
createRuntimeSupervisor,
RUNTIME_MAX_RESTARTS,
type RuntimeStatus,
type RuntimeSupervisor,
type RuntimeSupervisorDeps,
} from '../src/local-model/runtime.js';
import {
createFakeOllama,
FakeRuntimeChild,
freeLoopbackPort,
startFakeOllama,
type FakeOllama,
type FakeOllamaOptions,
} from './fixtures/fake-ollama.js';
const CONTEXT = 131072;
/** A pid of an "earlier server's" runtime. Above every real pid, like the fake's own. */
const ORPHAN_PID = 9_800_000;
let paths: LocalModelPaths;
let port: number;
const fakes: FakeOllama[] = [];
const supervisors: RuntimeSupervisor[] = [];
const servers: Server[] = [];
beforeEach(async () => {
paths = localModelPaths('0.40.15');
port = await freeLoopbackPort();
});
afterEach(async () => {
for (const supervisor of supervisors.splice(0)) await supervisor.stop();
for (const fake of fakes.splice(0)) await fake.close();
for (const server of servers.splice(0)) await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(paths.root, { recursive: true, force: true });
});
/** A fake runtime that does not listen until it is "spawned". */
function fakeRuntime(options: FakeOllamaOptions = {}): FakeOllama {
const fake = createFakeOllama(options);
fakes.push(fake);
return fake;
}
/** The children the fake has spawned so far. */
function children(fake: FakeOllama): FakeRuntimeChild[] {
return fake.state.children;
}
interface Harness {
supervisor: RuntimeSupervisor;
/** Every status `onChange` was called with. */
changes: RuntimeStatus[];
/** Every wait the supervisor asked for. */
waits: number[];
/** Every signal sent to a pid that is not our child. */
kills: [number, NodeJS.Signals][];
}
/** A supervisor whose child is `fake`. Waits are instant unless a test passes its own `sleep`. */
function supervise(fake: FakeOllama, overrides: Partial<RuntimeSupervisorDeps> = {}): Harness {
const changes: RuntimeStatus[] = [];
const waits: number[] = [];
const kills: [number, NodeJS.Signals][] = [];
const supervisor = createRuntimeSupervisor({
spawn: fake.spawn,
fetch,
env: { PATH: '/usr/bin:/bin' },
pin: fake.pin(),
paths,
sleep: async (ms) => {
waits.push(ms);
},
commandOfPid: async () => undefined,
kill: (pid, signal) => {
kills.push([pid, signal]);
},
onChange: (status) => changes.push(status),
startTimeoutMs: 3_000,
budgetWaitMs: 500,
...overrides,
});
supervisors.push(supervisor);
return { supervisor, changes, waits, kills };
}
/** Waits until `check()` is true. Fails the test when it never is. */
async function until(check: () => boolean, what: string, timeoutMs = 3_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!check()) {
if (Date.now() > deadline) throw new Error(`Timed out waiting for: ${what}`);
await new Promise((resolve) => setTimeout(resolve, 5));
}
}
/** The command line `ps` would print for a runtime of ours. */
function ownCommand(): string {
return `${paths.binary} serve`;
}
// ---------- Start ----------
describe('start', () => {
it('starts one child, the runtime binary with "serve", and answers once it is up', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(children(fake)).toHaveLength(1);
const child = children(fake)[0];
expect(child.command).toBe(paths.binary);
expect(child.args).toEqual(['serve']);
expect(result).toEqual({
ok: true,
pid: child.pid,
port,
orphan: 'none',
// 37.4 GiB, read from the child's log line "inference compute".
measuredTotalBytes: 40_157_944_217,
});
expect(supervisor.status()).toEqual({
running: true,
pid: child.pid,
port,
contextLength: CONTEXT,
measuredTotalBytes: 40_157_944_217,
restarts: 0,
gaveUp: false,
});
expect(fake.port).toBe(port);
});
it('starts the child non-detached, with pipes, in the private home', async () => {
const fake = fakeRuntime();
const seen: SpawnOptions[] = [];
const { supervisor } = supervise(fake, {
spawn: (command, args, options) => {
seen.push(options);
return fake.spawn(command, args, options);
},
});
await supervisor.start({ port, contextLength: CONTEXT });
expect(seen).toHaveLength(1);
expect(seen[0].detached).toBe(false);
expect(seen[0].stdio).toEqual(['ignore', 'pipe', 'pipe']);
expect(seen[0].cwd).toBe(paths.homeDir);
expect(seen[0]).not.toHaveProperty('shell');
// The folders it needs exist before it starts.
expect(existsSync(paths.homeDir)).toBe(true);
expect(existsSync(paths.modelsDir)).toBe(true);
});
it('gives the child the allowlisted environment and nothing of the server beyond it', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, {
env: {
PATH: '/usr/bin:/bin',
HOME: '/Users/arkon',
LANG: 'en_US.UTF-8',
CODEMAN_PASSWORD: 'hunter2',
CODEMAN_API_URL: 'http://127.0.0.1:3000',
OLLAMA_ORIGINS: '*',
OLLAMA_HOST: '0.0.0.0:11434',
OLLAMA_KEEP_ALIVE: '-1',
ANTHROPIC_API_KEY: 'sk-ant-secret',
},
});
await supervisor.start({ port, contextLength: 65536 });
expect(children(fake)[0].env).toEqual({
PATH: '/usr/bin:/bin',
LANG: 'en_US.UTF-8',
HOME: paths.homeDir,
OLLAMA_HOST: `127.0.0.1:${port}`,
OLLAMA_MODELS: paths.modelsDir,
OLLAMA_CONTEXT_LENGTH: '65536',
OLLAMA_NO_CLOUD: '1',
OLLAMA_KEEP_ALIVE: '10m',
OLLAMA_NUM_PARALLEL: '1',
OLLAMA_MAX_LOADED_MODELS: '1',
OLLAMA_KV_CACHE_TYPE: 'q8_0',
OLLAMA_FLASH_ATTENTION: '1',
LLAMA_ARG_CACHE_RAM: '1024',
LLAMA_ARG_CTX_CHECKPOINTS: '8',
});
});
it('reads the budget the child logs, whatever the Mac', async () => {
const fake = fakeRuntime({ spawn: { total: '11.8 GiB' } });
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true, measuredTotalBytes: 12_670_153_523 });
expect(supervisor.status().measuredTotalBytes).toBe(12_670_153_523);
});
it('still starts when the child prints no budget line, and reports none', async () => {
const fake = fakeRuntime({ spawn: { logsBudget: false } });
const { supervisor } = supervise(fake, { budgetWaitMs: 60 });
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true, port });
expect(result).not.toHaveProperty('measuredTotalBytes');
expect(supervisor.status()).not.toHaveProperty('measuredTotalBytes');
expect(supervisor.status().running).toBe(true);
});
it('is idempotent: a child that already runs as asked is kept', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const first = await supervisor.start({ port, contextLength: CONTEXT });
const second = await supervisor.start({ port, contextLength: CONTEXT });
expect(second).toEqual(first);
expect(children(fake)).toHaveLength(1);
expect(children(fake)[0].signals).toEqual([]);
});
it('replaces a child that runs with another context length', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const result = await supervisor.start({ port, contextLength: 65536 });
expect(result).toMatchObject({ ok: true, orphan: 'none' });
expect(children(fake)).toHaveLength(2);
// The first got SIGTERM; the second runs with the new context.
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(children(fake)[1].env.OLLAMA_CONTEXT_LENGTH).toBe('65536');
expect(supervisor.status()).toMatchObject({ running: true, pid: children(fake)[1].pid, contextLength: 65536 });
// Replacing on purpose is not a crash.
expect(supervisor.status().restarts).toBe(0);
});
it('runs two starts one after the other, never two children at once', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const [a, b] = await Promise.all([
supervisor.start({ port, contextLength: CONTEXT }),
supervisor.start({ port, contextLength: CONTEXT }),
]);
expect(a).toMatchObject({ ok: true });
expect(b).toEqual(a);
expect(children(fake)).toHaveLength(1);
});
it('refuses a port or a context length that is none, without starting anything', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
for (const bad of [0, -1, 70_000, 1.5, Number.NaN]) {
expect(await supervisor.start({ port: bad, contextLength: CONTEXT }), String(bad)).toMatchObject({
ok: false,
code: 'runtime-start-failed',
});
}
for (const bad of [0, -5, 1.5, Number.NaN]) {
expect(await supervisor.start({ port, contextLength: bad }), String(bad)).toMatchObject({
ok: false,
code: 'runtime-start-failed',
});
}
expect(children(fake)).toHaveLength(0);
});
it('tells its caller every change, and survives a caller that throws', async () => {
const fake = fakeRuntime();
const seen: RuntimeStatus[] = [];
const { supervisor } = supervise(fake, {
onChange: (status) => {
seen.push(status);
throw new Error('the state file is read-only');
},
});
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true });
// The caller stores pid and port from this.
expect(seen.at(-1)).toMatchObject({ running: true, pid: children(fake)[0].pid, port, contextLength: CONTEXT });
await supervisor.stop();
expect(seen.at(-1)).toEqual({ running: false, restarts: 0, gaveUp: false });
});
it('hands out a copy of its status', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const status = supervisor.status();
status.running = false;
status.pid = 1;
expect(supervisor.status()).toMatchObject({ running: true, pid: children(fake)[0].pid });
});
it("writes no state file: storing pid and port is the caller's business", async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
await supervisor.stop();
expect(existsSync(paths.stateFile)).toBe(false);
});
});
// ---------- A start that fails ----------
describe('a start that fails', () => {
it('answers runtime-start-failed when the child never answers, and does not leave it running', async () => {
const fake = fakeRuntime({ spawn: { neverListens: true } });
const { supervisor } = supervise(fake, { startTimeoutMs: 150 });
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
const detail = (result as { detail?: string }).detail ?? '';
expect(detail).toContain('did not answer');
// The last lines of its log are the detail.
expect(detail).toContain('Listening on');
expect(children(fake)).toHaveLength(1);
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
it('answers runtime-start-failed when the child exits at once, with what it printed', async () => {
const fake = fakeRuntime({ spawn: { exitsAtOnce: 2 } });
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
const detail = (result as { detail?: string }).detail ?? '';
expect(detail).toContain('exited with code 2');
expect(detail).toContain('address already in use');
// A start that failed is not a crash to recover from: nothing is started again.
await new Promise((resolve) => setTimeout(resolve, 50));
expect(children(fake)).toHaveLength(1);
expect(supervisor.status()).toMatchObject({ running: false, restarts: 0, gaveUp: false });
});
it('answers runtime-start-failed when a runtime of another version answers', async () => {
const fake = fakeRuntime({ version: '0.39.2' });
const { supervisor } = supervise(fake, { pin: { ...fake.pin(), version: '0.40.15' } });
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
expect((result as { detail?: string }).detail).toContain('0.39.2');
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
});
it('answers runtime-start-failed when the binary cannot be started at all', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, {
spawn: () => {
throw new Error('spawn EACCES');
},
});
expect(await supervisor.start({ port, contextLength: CONTEXT })).toEqual({
ok: false,
code: 'runtime-start-failed',
detail: 'spawn EACCES',
});
});
it('spawns nothing under vitest unless the test injects a spawn', async () => {
// No `spawn` and no `fetch` given: the defaults refuse here.
const supervisor = createRuntimeSupervisor({ paths, startTimeoutMs: 200 });
supervisors.push(supervisor);
const result = await supervisor.start({ port, contextLength: CONTEXT });
// The refusal itself, not a failed attempt to run a binary that is not there.
expect(result).toEqual({
ok: false,
code: 'runtime-start-failed',
detail: 'spawn is not available under vitest: the test must inject its own',
});
expect(supervisor.status().running).toBe(false);
});
it('gives up a start at once when stop is called while it waits', async () => {
const fake = fakeRuntime({ spawn: { neverListens: true } });
const { supervisor } = supervise(fake, {
startTimeoutMs: 20_000,
sleep: () => new Promise((resolve) => setTimeout(resolve, 10)),
});
const starting = supervisor.start({ port, contextLength: CONTEXT });
await until(() => children(fake).length === 1, 'the child to be spawned');
const began = Date.now();
await supervisor.stop();
const result = await starting;
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
// Not after the 20 seconds a start may take.
expect(Date.now() - began).toBeLessThan(5_000);
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(supervisor.status().running).toBe(false);
});
});
// ---------- The port ----------
describe('the port', () => {
/** A runtime that listens on `port` already, as an earlier server's would. */
async function listeningRuntime(options: FakeOllamaOptions = {}): Promise<FakeOllama> {
const other = createFakeOllama(options);
fakes.push(other);
await other.listen(port);
return other;
}
it('answers port-in-use for a runtime that is not ours, and does not touch it', async () => {
// The user's own runtime on our port. No pid of ours is stored.
const theirs = await listeningRuntime();
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT });
// Told apart from any other program on the port: it answers as a runtime, and is not ours.
expect(result).toEqual({
ok: false,
code: 'port-in-use',
detail: `A model runtime that Codeman did not start answers on port ${port} (version 0.40.15)`,
});
expect(children(fake)).toHaveLength(0);
expect(kills).toEqual([]);
expect(theirs.listening).toBe(true);
expect(supervisor.status().running).toBe(false);
});
it('answers port-in-use when the stored pid now runs something else', async () => {
const theirs = await listeningRuntime();
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
// The pid was reused, or it is a runtime outside our folder.
commandOfPid: async () => '/usr/local/bin/ollama serve',
});
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
expect(result).toMatchObject({ ok: false, code: 'port-in-use' });
expect(kills).toEqual([]);
expect(theirs.listening).toBe(true);
expect(children(fake)).toHaveLength(0);
});
it('answers port-in-use for a listener that is no runtime at all', async () => {
// Some other program: it takes the connection and hangs up.
const holder = createServer((socket) => socket.destroy());
servers.push(holder);
await new Promise<void>((resolve) => holder.listen(port, '127.0.0.1', () => resolve()));
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
expect(await supervisor.start({ port, contextLength: CONTEXT })).toEqual({
ok: false,
code: 'port-in-use',
detail: `Another program holds port ${port}`,
});
expect(children(fake)).toHaveLength(0);
});
it('ADOPTS its own orphan when that holds a loaded model at the wanted context', async () => {
const orphan = await listeningRuntime({ installed: true });
orphan.state.loaded = true;
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills, changes } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
});
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
// Kept as it is: nothing spawned, nothing signalled, the loaded model stays loaded.
expect(result).toEqual({ ok: true, pid: ORPHAN_PID, port, orphan: 'adopted' });
expect(children(fake)).toHaveLength(0);
expect(kills).toEqual([]);
expect(orphan.listening).toBe(true);
// Its log is not ours to read, so there is no measured budget.
expect(supervisor.status()).toEqual({
running: true,
pid: ORPHAN_PID,
port,
contextLength: CONTEXT,
restarts: 0,
gaveUp: false,
});
expect(changes.at(-1)).toMatchObject({ running: true, pid: ORPHAN_PID });
// So that the stop after this test finds it gone and does not wait out its grace period.
gone = true;
});
it('stops an adopted orphan by its pid', async () => {
const orphan = await listeningRuntime({ installed: true });
orphan.state.loaded = true;
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
kills.push([pid, signal]);
gone = true;
},
});
await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
await supervisor.stop();
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
/** Starts with an orphan on the port that is ours and goes away when it is signalled. */
async function startOverOrphan(orphanOptions: FakeOllamaOptions, loaded: boolean) {
const orphan = await listeningRuntime(orphanOptions);
orphan.state.loaded = loaded;
let gone = false;
const fake = fakeRuntime();
const harness = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
harness.kills.push([pid, signal]);
gone = true;
void orphan.close();
},
});
const result = await harness.supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
return { orphan, fake, result, ...harness };
}
it('REPLACES its own orphan when that has no model loaded: nothing is lost, and the budget is read again', async () => {
const { orphan, fake, result, kills, supervisor } = await startOverOrphan({ installed: true }, false);
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(orphan.listening).toBe(false);
expect(children(fake)).toHaveLength(1);
expect(result).toEqual({
ok: true,
pid: children(fake)[0].pid,
port,
orphan: 'replaced',
measuredTotalBytes: 40_157_944_217,
});
expect(supervisor.status()).toMatchObject({ running: true, pid: children(fake)[0].pid });
});
it('replaces its own orphan when the model is loaded at another context', async () => {
const { fake, result, kills } = await startOverOrphan({ installed: true, contextLength: 32768 }, true);
expect(result).toMatchObject({ ok: true, orphan: 'replaced' });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(children(fake)[0].env.OLLAMA_CONTEXT_LENGTH).toBe(String(CONTEXT));
});
it('replaces its own orphan when it is another version than the pinned one', async () => {
const { fake, result, kills } = await startOverOrphan({ installed: true, version: '0.39.2' }, true);
expect(result).toMatchObject({ ok: true, orphan: 'replaced' });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
expect(children(fake)).toHaveLength(1);
});
it('replaces its own orphan that no longer answers, and starts on the free port', async () => {
// The stored pid is a runtime of ours, but nothing listens on the port.
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
kills.push([pid, signal]);
gone = true;
},
});
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
expect(result).toMatchObject({ ok: true, orphan: 'replaced' });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
});
it('never signals a pid whose command is not under our runtime folder', async () => {
for (const command of [
'/usr/local/bin/ollama serve',
`${paths.runtimeRoot}-other/ollama serve`,
`/bin/sh -c ${paths.binary} serve`,
undefined,
]) {
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, { commandOfPid: async () => command });
const result = await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID });
expect(result, String(command)).toMatchObject({ ok: true, orphan: 'none' });
expect(kills, String(command)).toEqual([]);
await supervisor.stop();
}
});
it('takes over from an adopted orphan that dies: the next runtime is a child of its own', async () => {
const orphan = await listeningRuntime({ installed: true });
orphan.state.loaded = true;
let gone = false;
const fake = fakeRuntime();
const { supervisor } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
orphanPollMs: 15,
});
expect(await supervisor.start({ port, contextLength: CONTEXT, previousPid: ORPHAN_PID })).toMatchObject({
orphan: 'adopted',
});
// The orphan dies by itself.
await orphan.close();
gone = true;
await until(() => children(fake).length === 1 && supervisor.status().running, 'a child of our own to run');
expect(supervisor.status()).toMatchObject({
running: true,
pid: children(fake)[0].pid,
port,
restarts: 1,
measuredTotalBytes: 40_157_944_217,
});
});
});
// ---------- Restart ----------
describe('a child that exits by itself', () => {
it('is started again, with the same port and context', async () => {
const fake = fakeRuntime();
const { supervisor, waits, changes } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const first = children(fake)[0];
first.crash(1);
await until(() => children(fake).length === 2 && supervisor.status().running, 'the runtime to run again');
const second = children(fake)[1];
expect(second.env.OLLAMA_HOST).toBe(`127.0.0.1:${port}`);
expect(second.env.OLLAMA_CONTEXT_LENGTH).toBe(String(CONTEXT));
expect(supervisor.status()).toMatchObject({ running: true, pid: second.pid, restarts: 1, gaveUp: false });
// It waited a second before starting it again.
expect(waits).toContain(1000);
// The caller was told that it stopped, and that it runs again.
const running = changes.map((status) => status.running);
expect(running.indexOf(false, running.indexOf(true))).toBeGreaterThan(0);
expect(running.at(-1)).toBe(true);
// Nobody signalled the one that crashed.
expect(first.signals).toEqual([]);
});
it('is started again at most 3 times, then given up', async () => {
expect(RUNTIME_MAX_RESTARTS).toBe(3);
const fake = fakeRuntime();
const { supervisor, changes } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
for (let crash = 1; crash <= 3; crash++) {
children(fake).at(-1)?.crash(1);
await until(() => children(fake).length === crash + 1 && supervisor.status().running, `restart number ${crash}`);
expect(supervisor.status()).toMatchObject({ running: true, restarts: crash, gaveUp: false });
}
// The fourth exit is one too many.
children(fake).at(-1)?.crash(1);
await until(() => supervisor.status().gaveUp, 'the supervisor to give up');
expect(supervisor.status()).toEqual({
running: false,
port,
contextLength: CONTEXT,
measuredTotalBytes: 40_157_944_217,
restarts: 3,
gaveUp: true,
});
// The first child and three restarts, and no fifth.
await new Promise((resolve) => setTimeout(resolve, 60));
expect(children(fake)).toHaveLength(4);
expect(changes.at(-1)).toMatchObject({ running: false, gaveUp: true, restarts: 3 });
});
it('counts a restart that does not come up as one of the three', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, { startTimeoutMs: 100 });
await supervisor.start({ port, contextLength: CONTEXT });
// From now on the runtime exits as soon as it is started.
fake.switches.spawn.exitsAtOnce = 1;
children(fake)[0].crash(1);
await until(() => supervisor.status().gaveUp, 'the supervisor to give up');
expect(children(fake)).toHaveLength(4);
expect(supervisor.status()).toMatchObject({ running: false, restarts: 3, gaveUp: true });
});
it('starts counting again after a start that was asked for', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake, { startTimeoutMs: 100 });
await supervisor.start({ port, contextLength: CONTEXT });
fake.switches.spawn.exitsAtOnce = 1;
children(fake)[0].crash(1);
await until(() => supervisor.status().gaveUp, 'the supervisor to give up');
// The person presses Start: it runs again, and the count is back at 0.
fake.switches.spawn.exitsAtOnce = undefined;
const result = await supervisor.start({ port, contextLength: CONTEXT });
expect(result).toMatchObject({ ok: true, orphan: 'none' });
expect(supervisor.status()).toMatchObject({ running: true, restarts: 0, gaveUp: false });
});
it('is not started again once stop was called, also while it waits to restart', async () => {
const fake = fakeRuntime();
let release: () => void = () => undefined;
const { supervisor } = supervise(fake, {
// The wait before a restart does not end by itself; the polls of a start still do.
sleep: (ms) =>
ms === 1000
? new Promise<void>((resolve) => {
release = resolve;
})
: Promise.resolve(),
});
await supervisor.start({ port, contextLength: CONTEXT });
children(fake)[0].crash(1);
await until(() => supervisor.status().restarts === 1, 'the restart to be scheduled');
const stopping = supervisor.stop();
release();
await stopping;
await new Promise((resolve) => setTimeout(resolve, 50));
expect(children(fake)).toHaveLength(1);
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
});
// ---------- Stop ----------
describe('stop', () => {
it('sends the child SIGTERM and resolves once it is gone', async () => {
const fake = fakeRuntime();
const { supervisor, changes } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
const child = children(fake)[0];
await supervisor.stop();
expect(child.signals).toEqual(['SIGTERM']);
expect(child.signalCode).toBe('SIGTERM');
// Gone, and the port is free again.
expect(fake.listening).toBe(false);
await expect(fetch(`http://127.0.0.1:${port}/api/version`)).rejects.toThrow();
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
expect(changes.at(-1)).toEqual({ running: false, restarts: 0, gaveUp: false });
});
it('does not take the stop for a crash: nothing is started again', async () => {
const fake = fakeRuntime();
const { supervisor, waits } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
await supervisor.stop();
await new Promise((resolve) => setTimeout(resolve, 60));
expect(children(fake)).toHaveLength(1);
expect(waits).not.toContain(1000);
});
it('is safe when nothing runs, and twice in a row', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.stop();
await supervisor.start({ port, contextLength: CONTEXT });
await Promise.all([supervisor.stop(), supervisor.stop()]);
await supervisor.stop();
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
expect(supervisor.status().running).toBe(false);
});
it('can be started again after a stop', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
await supervisor.stop();
const again = await supervisor.start({ port, contextLength: CONTEXT });
expect(again).toMatchObject({ ok: true, pid: children(fake)[1].pid, orphan: 'none' });
expect(children(fake)).toHaveLength(2);
});
it('kills the child when the server process exits without its shutdown', async () => {
// The last resort for a forced exit: a listener on the process, for as long as the child lives.
const fake = fakeRuntime();
const before = process.listenerCount('exit');
const { supervisor } = supervise(fake);
await supervisor.start({ port, contextLength: CONTEXT });
expect(process.listenerCount('exit')).toBe(before + 1);
const child = children(fake)[0] as unknown as ChildProcess & FakeRuntimeChild;
// What Node does on `process.exit()`: it calls the listeners.
const added = process.listeners('exit').at(-1) as () => void;
const exited = new Promise<void>((resolve) => child.once('exit', () => resolve()));
added();
await exited;
expect(child.signals).toEqual(['SIGTERM']);
// The listener goes with its child. (This process did not really exit, so the supervisor
// took the exit for a crash and started another child, which has a listener of its own.)
expect(process.listeners('exit')).not.toContain(added);
await supervisor.stop();
expect(process.listenerCount('exit')).toBe(before);
});
});
// ---------- A runtime an earlier server left behind, and the pid that names it ----------
describe('the pid is told the moment the child is spawned', () => {
it('reports it before the child answers, so a server killed during the start leaves a pid', async () => {
// The child never answers: the start waits, and is told to stop.
const fake = fakeRuntime({ spawn: { neverListens: true } });
const { supervisor, changes } = supervise(fake, {
startTimeoutMs: 60_000,
sleep: () => new Promise((resolve) => setTimeout(resolve, 5)),
});
const starting = supervisor.start({ port, contextLength: CONTEXT });
await until(() => children(fake).length === 1, 'the child to be spawned');
await until(() => changes.some((status) => status.pid === children(fake)[0].pid), 'the pid to be reported');
const early = changes.find((status) => status.pid === children(fake)[0].pid);
// Not running yet: it has not answered.
expect(early).toMatchObject({ running: false, pid: children(fake)[0].pid });
await supervisor.stop();
await starting;
// A start that failed leaves no pid behind.
expect(supervisor.status()).toEqual({ running: false, restarts: 0, gaveUp: false });
});
});
describe('stop with the pid the state file kept', () => {
it('ends a runtime of ours that this process never started', async () => {
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID && !gone ? ownCommand() : undefined),
kill: (pid, signal) => {
kills.push([pid, signal]);
gone = true;
},
});
await supervisor.stop({ previousPid: ORPHAN_PID });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
// Nothing was spawned to do it.
expect(children(fake)).toHaveLength(0);
});
it('ends one that runs another version of ours: the folder decides, not the version', async () => {
let gone = false;
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) =>
pid === ORPHAN_PID && !gone ? `${runtimeBinaryForVersion(paths, '0.39.2')} serve` : undefined,
kill: (pid, signal) => {
kills.push([pid, signal]);
gone = true;
},
});
await supervisor.stop({ previousPid: ORPHAN_PID });
expect(kills).toEqual([[ORPHAN_PID, 'SIGTERM']]);
});
it('never signals a pid that runs something else now, nor one that is gone', async () => {
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake, {
commandOfPid: async (pid) => (pid === ORPHAN_PID ? '/usr/local/bin/ollama serve' : undefined),
});
await supervisor.stop({ previousPid: ORPHAN_PID });
await supervisor.stop({ previousPid: ORPHAN_PID + 1 });
await supervisor.stop({ previousPid: 1 });
await supervisor.stop({ previousPid: Number.NaN });
expect(kills).toEqual([]);
});
it('stops its own child first, and the stored pid only when it is somebody else', async () => {
const fake = fakeRuntime();
const { supervisor, kills } = supervise(fake);
const started = await supervisor.start({ port, contextLength: CONTEXT });
expect(started.ok).toBe(true);
await supervisor.stop({ previousPid: children(fake)[0].pid });
expect(children(fake)[0].signals).toEqual(['SIGTERM']);
// The child is gone by then: its pid names nothing of ours any more.
expect(kills).toEqual([]);
});
});
describe('start with the version an installed model was installed with', () => {
it('runs that version from its own folder and expects it to answer as that version', async () => {
const fake = fakeRuntime({ version: '0.39.2' });
const { supervisor } = supervise(fake, { pin: { ...fake.pin(), version: '0.40.15' } });
const result = await supervisor.start({ port, contextLength: CONTEXT, version: '0.39.2' });
expect(result).toMatchObject({ ok: true });
expect(children(fake)[0].command).toBe(runtimeBinaryForVersion(paths, '0.39.2'));
// Asked again the same way: the child is kept. Asked for the pinned version: replaced.
await supervisor.start({ port, contextLength: CONTEXT, version: '0.39.2' });
expect(children(fake)).toHaveLength(1);
});
it('a restart after a crash starts the same version again', async () => {
const fake = fakeRuntime({ version: '0.39.2' });
const { supervisor } = supervise(fake, { pin: { ...fake.pin(), version: '0.40.15' } });
await supervisor.start({ port, contextLength: CONTEXT, version: '0.39.2' });
children(fake)[0].crash();
await until(() => children(fake).length === 2 && supervisor.status().running, 'the restart');
expect(children(fake)[1].command).toBe(runtimeBinaryForVersion(paths, '0.39.2'));
});
it('refuses a version that is no version before anything is spawned', async () => {
const fake = fakeRuntime();
const { supervisor } = supervise(fake);
const result = await supervisor.start({ port, contextLength: CONTEXT, version: '../../bin' });
expect(result).toMatchObject({ ok: false, code: 'runtime-start-failed' });
expect(children(fake)).toHaveLength(0);
});
});