mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
The death-spiral amplifier: pruneStatusLog ran a whole-table ROW_NUMBER() sort, 40-48s synchronous on the 1.1M-row incident table, freezing boot -> healthcheck fail -> restart loop. - lib/chunked-prune.js: shared chunkedDelete (rowid IN (SELECT ... LIMIT ?) since better-sqlite3 has no DELETE...LIMIT) — bounded batch + setImmediate yield between batches, optional band-gate. Core invariant: no sync op blocks >~50ms ever. - pruneStatusLog: rewritten per-device via a loose index-scan seek (WHERE device_id > ? ORDER BY device_id LIMIT 1 — O(log n) each), retention + newest-cap trimmed in bounded batches, async, re-entrancy-guarded, band-gated on the interval / un-gated + fire-and-forget at startup so a bloated table self-heals on deploy WITHOUT freezing boot. - heartbeat.js: maintenance moved off the interval body into async band-gated re-entrant runMaintenance(); play_logs + provisioning prunes chunked; offline-marking stays synchronous. - pruneTelemetry: bounded single statement (OFFSET 6000 LIMIT batch), stays sync. - idx_devices_provisioning so the provisioning prune batch subquery is an index range. Tests: correctness (per-device cap + retention, independent devices), 300k-row backlog trims in many batches with max event-loop gap <250ms, band-gate no-op while critical + startup runs regardless, re-entrancy (concurrent -> once). Existing prune tests updated to await. Suite 247/247. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
2.4 KiB
JavaScript
49 lines
2.4 KiB
JavaScript
'use strict';
|
|
// #146 hardening — bounded, yielding maintenance so NO sweep blocks the event loop.
|
|
//
|
|
// The #146 death spiral's amplifier was pruneStatusLog running a whole-table
|
|
// ROW_NUMBER() sort — 40-48s synchronous on a 1.1M-row table, freezing boot into a
|
|
// restart loop. This helper is the discipline every table-growth sweep now rides:
|
|
// - delete in bounded batches (config.statusLogPruneBatch rows max per statement),
|
|
// - `await setImmediate` between batches so the loop breathes (core invariant: no
|
|
// sync op blocks >~50ms, ever, regardless of table size),
|
|
// - optional band-gate: skip an INTERVAL run entirely when loop-lag is not normal
|
|
// (never add maintenance pressure while already loaded); startup runs un-gated so
|
|
// it can clear an existing backlog and self-heal without a restart.
|
|
//
|
|
// better-sqlite3's bundled SQLite is NOT built with SQLITE_ENABLE_UPDATE_DELETE_LIMIT,
|
|
// so `DELETE ... LIMIT` is a syntax error. We delete by `rowid IN (SELECT rowid ...
|
|
// LIMIT ?)`, which is portable and rides whatever index the inner SELECT uses.
|
|
|
|
const config = require('../config');
|
|
|
|
const yieldTick = () => new Promise((resolve) => setImmediate(resolve));
|
|
|
|
// Lazy + defensive band read — avoids a load-time cycle (loop-lag requires db, db's
|
|
// prune requires this). Returns 'normal' if the monitor isn't wired yet (e.g. tests).
|
|
let _getBand = null;
|
|
function currentBand() {
|
|
if (_getBand === null) {
|
|
try { _getBand = require('../services/loop-lag').getBand; } catch { _getBand = () => 'normal'; }
|
|
}
|
|
try { return _getBand(); } catch { return 'normal'; }
|
|
}
|
|
|
|
// Run `runBatch(limit)` (a synchronous DELETE returning rows-deleted) to completion in
|
|
// bounded batches, yielding between each. Stops when a batch deletes < limit (drained).
|
|
// Returns { skipped, deleted, batches }.
|
|
async function chunkedDelete(runBatch, opts = {}) {
|
|
const batch = opts.batch || config.statusLogPruneBatch;
|
|
if (opts.bandGate && currentBand() !== 'normal') return { skipped: true, deleted: 0, batches: 0 };
|
|
let total = 0, batches = 0, n;
|
|
do {
|
|
n = runBatch(batch);
|
|
total += n;
|
|
batches += 1;
|
|
if (n > 0) await yieldTick();
|
|
} while (n >= batch); // a short (or zero) batch means the predicate is drained
|
|
return { skipped: false, deleted: total, batches };
|
|
}
|
|
|
|
module.exports = { chunkedDelete, yieldTick, currentBand, __setBandForTest: (fn) => { _getBand = fn; } };
|