Avoid lifecycle history scans in fleet snapshots

This commit is contained in:
2026-10-05 14:59:08 -04:00
parent 041c024f95
commit 28bd08e1ea
7 changed files with 132 additions and 15 deletions
+32
View File
@@ -116,4 +116,36 @@ describe('persisted Fleet operations', () => {
expect(fleetOperations(now)).toContainEqual(expect.objectContaining({ kind: 'beta', label: 'Observing a match', href: '/betas/1' }));
expect(fleetOperations(now)).toContainEqual(expect.objectContaining({ kind: 'recovery', state: 'attention' }));
});
it('preserves lifecycle selection across cutoff, deletion, supersession, and overlapping warning branches', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const handle = db();
const since = iso(-86_400_000);
handle.transaction(() => {
const insert = handle.prepare(`INSERT INTO server_deployments
(id, server, action, state, stage, started_at, finished_at, server_deleted_at, triggered_by)
VALUES (?, ?, 'restart', ?, 'Stage', ?, ?, ?, 'test')`);
let i = 0;
for (const state of ['queued', 'running', 'waiting', 'awaiting-health', 'done', 'warning', 'cancelled', 'failed', 'interrupted']) {
for (const finished of [null, iso(-86_400_001), since, iso()]) {
for (const deleted of [null, iso()]) {
for (const successor of ['none', 'live', 'deleted']) {
const id = `operation-${i++}`;
insert.run(id, id, state, iso(-i), finished, deleted);
if (successor !== 'none') insert.run(`${id}-next`, id, 'done', iso(-i), null, successor === 'deleted' ? iso() : null);
}
}
}
}
})();
// The former OR query is the compatibility reference for all three branches.
const expected = handle.prepare(`SELECT d.id FROM server_deployments d
WHERE d.server_deleted_at IS NULL AND (d.state IN ('queued', 'running', 'waiting', 'awaiting-health')
OR (d.state IN ('done', 'warning', 'cancelled') AND d.finished_at >= ?)
OR (d.state IN ('failed', 'interrupted', 'warning') AND NOT EXISTS
(SELECT 1 FROM server_deployments n WHERE n.server = d.server AND n.rowid > d.rowid)))`)
.all(since) as { id: string }[];
const { fleetOperations } = await import('../../src/lib/server/fleet/overview.js');
expect(fleetOperations(now).map(row => row.key).sort()).toEqual(expected.map(row => `deployment:${row.id}`).sort());
});
});
@@ -14,6 +14,71 @@ afterEach(async () => {
});
describe('provisioning reads with retained lifecycle history', () => {
it('uses indexed per-server submissions and separate lifecycle indexes for fleet reads', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { latestGameSubmission, latestGameSubmissions } = await import('../../src/lib/server/fleet/lifecycle-evidence.js');
const { observationReader } = await import('../../src/lib/server/fleet/status.js');
const { runningVersions } = await import('../../src/lib/server/integrations/plutonium.js');
const { fleetOperations } = await import('../../src/lib/server/fleet/overview.js');
const handle = db();
const now = Date.parse('2026-10-05T12:00:00Z');
const iso = (offset: number) => new Date(now + offset).toISOString();
handle.transaction(() => {
const server = handle.prepare('INSERT INTO servers(name, komodo_name) VALUES (?, ?)');
for (let i = 0; i < 16; i++) server.run(`server-${i}`, `server-${i}`);
server.run('empty', 'empty');
const insert = handle.prepare(`INSERT INTO server_deployments
(id, server, action, state, stage, started_at, finished_at, submitted_at, server_deleted_at, triggered_by)
VALUES (?, ?, ?, 'done', 'Complete', ?, ?, ?, ?, 'test')`);
for (let i = 0; i < 6_000; i++) {
const at = iso(-100_000_000 - i);
insert.run(`history-${i}`, `server-${i % 16}`, 'restart', at, at, at, null);
}
for (const [i, action] of ['create', 'start', 'restart', 'rebuild'].entries()) {
insert.run(`current-${i}`, `server-${i}`, action, iso(-1000), iso(-1000), iso(-1000), null);
}
insert.run('stop', 'server-0', 'stop', iso(0), iso(0), iso(0), null);
insert.run('destroy', 'server-1', 'destroy', iso(0), iso(0), iso(0), null);
insert.run('deleted', 'server-2', 'restart', iso(0), iso(0), iso(0), iso(0));
insert.run('unsubmitted', 'server-3', 'rebuild', iso(0), null, null, null);
handle.prepare('INSERT INTO restart_rollout(server, requested_at, healthy_at) VALUES (?, ?, ?)')
.run('server-0', iso(-500), iso(0));
handle.prepare('INSERT INTO restart_rollout(server, requested_at) VALUES (?, ?)').run('server-1', iso(-2000));
})();
const submissions = latestGameSubmissions();
expect(submissions.get('empty')).toBeNull();
expect(submissions.get('server-0')).toBe(iso(-500));
for (const i of [1, 2, 3]) expect(submissions.get(`server-${i}`)).toBe(iso(-1000));
for (const [server, at] of submissions) expect(at).toBe(latestGameSubmission(server));
// Capture real snapshot queries so plan checks catch regressions at call sites.
for (const read of [observationReader, runningVersions]) {
const prepare = vi.spyOn(handle, 'prepare');
read();
const queries = prepare.mock.calls.map(([sql]) => sql).filter(sql => sql.includes('submitted_at'));
prepare.mockRestore();
expect(queries).toHaveLength(1);
const plan = handle.prepare(`EXPLAIN QUERY PLAN ${queries[0]}`).all() as { detail: string }[];
const details = plan.map(row => row.detail).join('\n');
expect(details).toContain('SEARCH server_deployments USING COVERING INDEX idx_server_deployments_submission');
expect(details).not.toMatch(/SCAN server_deployments\b/);
}
const prepare = vi.spyOn(handle, 'prepare');
fleetOperations(now);
const queries = prepare.mock.calls.map(([sql]) => sql).filter(sql => sql.includes('server_deployments'));
prepare.mockRestore();
expect(queries).toHaveLength(1);
const statement = handle.prepare(queries[0]);
expect(statement.columns().map(column => column.name)).toEqual(['id', 'server', 'action', 'state', 'stage', 'error', 'started_at']);
const plan = handle.prepare(`EXPLAIN QUERY PLAN ${queries[0]}`).all(iso(-86_400_000)) as { detail: string }[];
const details = plan.map(row => row.detail).join('\n');
expect(details).toContain('SEARCH server_deployments USING INDEX idx_server_deployments_state');
expect(details).toContain('SEARCH server_deployments USING INDEX idx_server_deployments_finished_at');
expect(details).toContain('SEARCH server_deployments USING INTEGER PRIMARY KEY (rowid=?)');
expect(details).toContain('SCAN server_deployments USING COVERING INDEX idx_server_deployments_server');
expect(details).not.toMatch(/SCAN server_deployments(?:\n|$)/);
});
it('indexes the server and queue reads used by collection matching and health polling', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { latestGameSubmission } = await import('../../src/lib/server/fleet/lifecycle-evidence.js');
@@ -70,7 +135,8 @@ describe('provisioning reads with retained lifecycle history', () => {
expect(handle.prepare('SELECT message FROM server_deployment_events').all()).toEqual([{ message: 'Creation complete' }]);
const indexes = handle.pragma('index_list(server_deployments)') as { name: string }[];
expect(indexes.map(index => index.name)).toEqual(expect.arrayContaining([
'idx_server_deployments_server', 'idx_server_deployments_state', 'idx_server_deployments_started_at'
'idx_server_deployments_server', 'idx_server_deployments_state', 'idx_server_deployments_started_at',
'idx_server_deployments_submission', 'idx_server_deployments_finished_at'
]));
expect(handle.pragma('foreign_key_check')).toEqual([]);
closeDb();