Fix retention query performance and cleanup scheduling
This commit is contained in:
@@ -22,6 +22,53 @@ it('previews eligible campaigns, respects latest-N and explicit holds, and treat
|
||||
a.saveFleetSettings({ ...a.fleetSettings(),deployKeepDays: 0,historyKeepCount: 0 });
|
||||
expect(a.pruneHistory('campaign')).toBe(0);
|
||||
});
|
||||
it('seeks expired campaigns and uses indexed dependency checks even with failure retention enabled', async () => {
|
||||
const a = await api();
|
||||
const now = new Date().toISOString();
|
||||
const insert = a.db.prepare("INSERT INTO deployments(id,action,started_at,finished_at) VALUES(?,'drift',?,?)");
|
||||
a.db.transaction(() => {
|
||||
for (let n=0;n<8000;n++) insert.run(`recent-${n}`,now,now);
|
||||
})();
|
||||
a.saveFleetSettings({...a.fleetSettings(),deployKeepDays:7,failureKeepDays:7});
|
||||
const prepare = vi.spyOn(a.db,'prepare');
|
||||
let sql: string;
|
||||
try {
|
||||
expect(a.pruneHistory('campaign')).toBe(0);
|
||||
sql = prepare.mock.calls.find(([sql]) => sql.startsWith('SELECT o.rowid AS rid'))![0];
|
||||
} finally { prepare.mockRestore(); }
|
||||
const plan = a.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all({category:'campaign',cutoff:old,failureCutoff:old}) as {detail:string}[];
|
||||
const details = plan.map(row=>row.detail).join('\n');
|
||||
expect(details).toMatch(/SEARCH o USING (?:COVERING )?INDEX idx_deployments_started \(started_at<\?\)/);
|
||||
for (const index of ['idx_deployments_rolled_back_by','idx_watchdog_actions_campaign','idx_provisioning_sources_source']) {
|
||||
expect(details).toContain(index);
|
||||
}
|
||||
expect(details).not.toMatch(/SCAN [odas]\b/);
|
||||
});
|
||||
it('selects resolved failures separately while preserving age, protection and batch limits', async () => {
|
||||
const a = await api();
|
||||
const now = new Date().toISOString();
|
||||
for (const id of ['normal','unresolved','resolved','held','new-resolution','changed']) {
|
||||
a.db.prepare("INSERT INTO deployments(id,action,started_at,finished_at) VALUES(?,'drift',?,?)").run(id,old,old);
|
||||
if (id !== 'normal') a.db.prepare("INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES(?,'s',1,'failed')").run(id);
|
||||
}
|
||||
for (const id of ['resolved','held','new-resolution','changed']) {
|
||||
a.setRetentionControl({category:'campaign',id,resolve:true,protected:id==='held'});
|
||||
if (id !== 'new-resolution') a.db.prepare("UPDATE retention_controls SET resolved_at=? WHERE record_id=?").run(old,id);
|
||||
}
|
||||
a.db.prepare("UPDATE deployments SET started_at=? WHERE id='changed'").run(now);
|
||||
// Failure retention works independently of normal campaign retention.
|
||||
a.saveFleetSettings({...a.fleetSettings(),deployKeepDays:0,failureKeepDays:7});
|
||||
expect(a.retentionPreview().find(p=>p.category==='campaign')?.records).toEqual([{id:'resolved'}]);
|
||||
expect(a.pruneHistory('campaign')).toBe(1);
|
||||
a.saveFleetSettings({...a.fleetSettings(),deployKeepDays:7});
|
||||
const insert = a.db.prepare("INSERT INTO deployments(id,action,started_at) VALUES(?,'drift',?)");
|
||||
a.db.transaction(() => { for (let n=0;n<600;n++) insert.run(`old-${n}`,old); })();
|
||||
expect(a.pruneHistory('campaign')).toBe(500);
|
||||
expect(a.pruneHistory('campaign')).toBe(101);
|
||||
expect(a.db.prepare('SELECT id FROM deployments ORDER BY id').all()).toEqual([
|
||||
{id:'changed'},{id:'held'},{id:'new-resolution'},{id:'unresolved'}
|
||||
]);
|
||||
});
|
||||
it('expires file bodies without silently turning rollback into file deletion', async () => {
|
||||
const a = await api();
|
||||
a.db.prepare("INSERT INTO deployments(id,action,started_at,finished_at) VALUES('p','promote',?,?)").run(old,old);
|
||||
|
||||
@@ -16,6 +16,16 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe('sweep', () => {
|
||||
it('leaves history cleanup to the retention requester', async () => {
|
||||
const retention = await import('../../src/lib/server/retention.js');
|
||||
const prune = vi.spyOn(retention,'pruneHistory');
|
||||
try {
|
||||
const { sweep } = await import('../../src/lib/server/pipeline/sweep.js');
|
||||
await sweep();
|
||||
expect(prune).not.toHaveBeenCalled();
|
||||
} finally { prune.mockRestore(); }
|
||||
});
|
||||
|
||||
it('emits server state only when state, players, or error changes', async () => {
|
||||
const { sweep } = await import('../../src/lib/server/pipeline/sweep.js');
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
|
||||
@@ -6,6 +6,7 @@ vi.mock('../../src/lib/server/pipeline/beta-runner.js', () => ({ betaTick: vi.fn
|
||||
vi.mock('../../src/lib/server/maintenance/index.js', () => ({ runMaintenance: vi.fn() }));
|
||||
vi.mock('../../src/lib/server/game-files/index.js', () => ({ cleanupControllerLibraries: vi.fn(async () => {}) }));
|
||||
vi.mock('../../src/lib/server/provisioning/worker.js', () => ({ processProvisioning: vi.fn(async () => {}) }));
|
||||
vi.mock('../../src/lib/server/retention.js', () => ({ pruneHistory: vi.fn(), pruneSampleHistory: vi.fn() }));
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
@@ -31,6 +32,30 @@ async function setup() {
|
||||
}
|
||||
|
||||
describe('independent provisioning scheduling', () => {
|
||||
it('runs retention at most every five minutes across ticks and explicit kicks', async () => {
|
||||
const s = await setup();
|
||||
const { pruneHistory, pruneSampleHistory } = await import('../../src/lib/server/retention.js');
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
s.startWorker();
|
||||
expect(pruneHistory).toHaveBeenCalledTimes(1);
|
||||
expect(pruneSampleHistory).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(285_000);
|
||||
await s.kick();
|
||||
expect(pruneHistory).toHaveBeenCalledTimes(1);
|
||||
expect(pruneSampleHistory).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
expect(pruneHistory).toHaveBeenCalledTimes(2);
|
||||
expect(pruneSampleHistory).toHaveBeenCalledTimes(2);
|
||||
expect(s.process.mock.calls.length).toBeGreaterThan(2);
|
||||
} finally {
|
||||
s.stopWorker(); s.background.resolve();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
vi.useRealTimers();
|
||||
await vi.waitFor(() => expect(s.workerActivity()).toEqual([]));
|
||||
}
|
||||
});
|
||||
|
||||
it('runs controller library cleanup on worker ticks without overlapping a slow cleanup', async () => {
|
||||
const s = await setup();
|
||||
const { cleanupControllerLibraries } = await import('../../src/lib/server/game-files/index.js');
|
||||
|
||||
Reference in New Issue
Block a user