Fix retention query performance and cleanup scheduling

This commit is contained in:
2026-10-06 10:39:32 -04:00
parent 9fd448339d
commit 88a950692a
8 changed files with 104 additions and 8 deletions
+47
View File
@@ -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);