Use database fleet registry and gate RCON polling on container readiness
This commit is contained in:
@@ -4,6 +4,8 @@ import { extractUdpPort, extractContainerState } from '../../src/lib/server/inte
|
||||
describe('container readiness', () => {
|
||||
it.each(['starting', 'unhealthy'])('does not accept a running container whose health is %s', (health) => {
|
||||
expect(extractContainerState({ State: { Status: 'running', Running: true, Health: { Status: health } } })).toBe(health);
|
||||
expect(extractContainerState({ state: 'running', health })).toBe(health);
|
||||
expect(extractContainerState({ state: 'running', health: { status: health } })).toBe(health);
|
||||
});
|
||||
it('accepts healthy running containers and preserves stopped states', () => {
|
||||
expect(extractContainerState({ State: { Status: 'running', Running: true, Health: { Status: 'healthy' } } })).toBe('running');
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
vi.mock('../../src/lib/server/integrations/komodo.js', () => ({
|
||||
komodoInspectHealthy: vi.fn(), komodoRestart: vi.fn()
|
||||
}));
|
||||
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({
|
||||
rconStatusResult: vi.fn(), rconGetPlayerCount: vi.fn()
|
||||
}));
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
fixture = await newFixture(['unregistered-folder']);
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); });
|
||||
|
||||
async function setup() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare("INSERT INTO servers (name, komodo_name, last_error) VALUES ('managed', 'managed', 'timeout:old process')").run();
|
||||
const { komodoInspectHealthy } = await import('../../src/lib/server/integrations/komodo.js');
|
||||
const { rconStatusResult } = await import('../../src/lib/server/integrations/rcon.js');
|
||||
const { fanoutStatus } = await import('../../src/lib/server/pipeline/worker.js');
|
||||
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
|
||||
return { db: db(), inspect: vi.mocked(komodoInspectHealthy), rcon: vi.mocked(rconStatusResult), fanoutStatus, getServer };
|
||||
}
|
||||
|
||||
describe('database fleet and container readiness', () => {
|
||||
it('lists registered servers without folders and ignores unregistered folders and tombstones', async () => {
|
||||
const { db } = await setup();
|
||||
const { allServers, listServers } = await import('../../src/lib/server/fleet/servers.js');
|
||||
expect(await allServers()).toEqual(['managed']);
|
||||
db.prepare("UPDATE servers SET destroyed_at = '2026-01-01' WHERE name = 'managed'").run();
|
||||
expect(listServers()).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips startup RCON, clears old errors, and resumes polling only after Komodo is ready', async () => {
|
||||
const { inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
const { GET } = await import('../../src/routes/api/servers/+server.js');
|
||||
inspect.mockResolvedValue({ ok: false, state: 'starting', reason: 'container state: starting' });
|
||||
await fanoutStatus();
|
||||
await fanoutStatus();
|
||||
expect(rcon).not.toHaveBeenCalled();
|
||||
expect(getServer('managed')).toMatchObject({ container_state: 'starting', last_error: null });
|
||||
expect((await (await GET()).json()).servers[0]).toMatchObject({ health: 'starting', last_error: null, last_players: null });
|
||||
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'timeout', detail: 'no reply' });
|
||||
await fanoutStatus();
|
||||
expect(rcon).toHaveBeenCalledTimes(1);
|
||||
expect(getServer('managed')).toMatchObject({ container_state: 'running', last_error: 'timeout:no reply' });
|
||||
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect((await (await GET()).json()).servers[0]).toMatchObject({ health: 'online', last_error: null });
|
||||
});
|
||||
|
||||
it.each(['unhealthy', 'exited', undefined])('does not poll RCON with container state %s', async (state) => {
|
||||
const { inspect, rcon, fanoutStatus } = await setup();
|
||||
inspect.mockResolvedValue({ ok: false, state, reason: 'not ready' });
|
||||
await fanoutStatus();
|
||||
expect(rcon).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('waits for an EC restart request to settle before trusting the old healthy container', async () => {
|
||||
const { db, inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
db.prepare("INSERT INTO restart_rollout (server, requested_at) VALUES ('managed', ?)").run(new Date().toISOString());
|
||||
await fanoutStatus();
|
||||
expect(inspect).not.toHaveBeenCalled();
|
||||
expect(rcon).not.toHaveBeenCalled();
|
||||
expect(getServer('managed')?.container_state).toBe('restarting');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user