'use strict'; // #146 hardening (Item A) — pruneStatusLog must be per-device, chunked, non-blocking, // band-gated, and re-entrant. The old whole-table ROW_NUMBER sort blocked the loop // 40-48s on the 1.1M-row incident table (the death-spiral amplifier). Deterministic, // in-process. const os = require('node:os'); const path = require('node:path'); const crypto = require('node:crypto'); process.env.DATA_DIR = path.join(os.tmpdir(), 'st-pruneharden-' + crypto.randomBytes(4).toString('hex')); process.env.STATUS_LOG_RETENTION_DAYS = '3'; process.env.STATUS_LOG_MAX_ROWS_PER_DEVICE = '500'; process.env.STATUS_LOG_PRUNE_BATCH = '2000'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const { db, pruneStatusLog } = require('../db/database'); const chunked = require('../lib/chunked-prune'); function seed(deviceId, n, ageSecFn) { const ins = db.prepare('INSERT INTO device_status_log (device_id, status, timestamp) VALUES (?, ?, ?)'); const now = Math.floor(Date.now() / 1000); const tx = db.transaction((count) => { for (let i = 0; i < count; i++) ins.run(deviceId, i % 2 ? 'online' : 'offline', now - ageSecFn(i)); }); tx(n); } const count = (d) => db.prepare('SELECT COUNT(*) c FROM device_status_log' + (d ? ' WHERE device_id = ?' : '')).get(...(d ? [d] : [])).c; test('correctness: keeps newest cap per device, drops older-than-retention, devices independent', async () => { db.exec('DELETE FROM device_status_log'); chunked.__setBandForTest(() => 'normal'); // device A: 800 recent rows -> cap keeps newest 500 seed('A', 800, () => 0); // device B: 300 recent (< cap, all kept) + 50 older-than-retention (dropped) seed('B', 300, () => 0); seed('B', 50, () => 10 * 86400); // device C: 10 rows, all old -> all dropped seed('C', 10, () => 10 * 86400); await pruneStatusLog(); assert.equal(count('A'), 500, 'A capped to newest 500'); assert.equal(count('B'), 300, 'B keeps its 300 recent, drops the 50 old'); assert.equal(count('C'), 0, 'C all older than retention -> gone'); const cutoff = Math.floor(Date.now() / 1000) - 3 * 86400; assert.ok(db.prepare('SELECT MIN(timestamp) m FROM device_status_log').get().m >= cutoff, 'nothing older than retention survives'); }); test('non-blocking: 300k-row single-device backlog trims in many batches, loop stays responsive', async () => { db.exec('DELETE FROM device_status_log'); chunked.__setBandForTest(() => 'normal'); seed('flapper', 300000, () => 0); // all recent -> cap prune must remove ~299500 assert.equal(count('flapper'), 300000, 'seeded 300k'); // Event-loop responsiveness probe: a 10ms ticker; the max gap between ticks is the // worst synchronous block during the prune. A single unbatched DELETE of 300k rows would // freeze it for SECONDS; chunked+yield keeps every gap well under a second. The bar is set to // catch that multi-second freeze while tolerating shared-CI-runner noise (a strict sub-300ms // bar flakes under runner contention/GC — the intent is "not a seconds-long freeze", not a // fixed-latency SLA). let maxGap = 0, last = Date.now(); const ticker = setInterval(() => { const n = Date.now(); maxGap = Math.max(maxGap, n - last); last = n; }, 10); const deleted = await pruneStatusLog(); clearInterval(ticker); assert.equal(count('flapper'), 500, 'trimmed to the cap'); assert.ok(deleted >= 299000, `deleted the backlog (${deleted})`); assert.ok(maxGap < 1500, `no seconds-long freeze — max event-loop gap ${maxGap}ms (an unbatched 300k DELETE would be multiple seconds)`); }); test('band-gate: interval run is a no-op when loaded; startup/normal runs', async () => { db.exec('DELETE FROM device_status_log'); seed('X', 900, () => 0); chunked.__setBandForTest(() => 'critical'); assert.equal(await pruneStatusLog({ bandGate: true }), 0, 'band-gated interval run skips while critical'); assert.equal(count('X'), 900, 'no rows touched while loaded'); // startup path is NOT band-gated even under load assert.ok(await pruneStatusLog({ bandGate: false }) > 0, 'un-gated startup run trims even while critical'); assert.equal(count('X'), 500, 'startup cleared the backlog to cap'); chunked.__setBandForTest(() => 'normal'); }); test('re-entrancy: two concurrent runs -> work happens once', async () => { db.exec('DELETE FROM device_status_log'); chunked.__setBandForTest(() => 'normal'); seed('Y', 5000, () => 0); const [a, b] = await Promise.all([pruneStatusLog(), pruneStatusLog()]); // fired synchronously assert.ok((a > 0) !== (b > 0), 'exactly one run did the work; the other short-circuited to 0'); assert.equal(count('Y'), 500, 'trimmed to cap exactly once (no double-run corruption)'); });