mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -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>
42 lines
2.1 KiB
JavaScript
42 lines
2.1 KiB
JavaScript
'use strict';
|
|
|
|
// #142 (cut 2) — provisioning-row cleanup window correctness. The sweep deletes
|
|
// UNCLAIMED provisioning devices older than 24h (it previously used 365*86400 — a
|
|
// year — contradicting its own comment). Imported devices (user_id set) and
|
|
// non-provisioning devices are preserved. Deterministic, in-process (no server).
|
|
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const crypto = require('node:crypto');
|
|
process.env.DATA_DIR = path.join(os.tmpdir(), 'st-provclean-' + crypto.randomBytes(4).toString('hex'));
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { db } = require('../db/database');
|
|
const { pruneProvisioningDevices } = require('../services/heartbeat');
|
|
|
|
test('sweeps unclaimed provisioning devices older than 24h, keeps the rest', async () => {
|
|
db.pragma('foreign_keys = OFF'); // seed user_id without a real users row
|
|
db.exec('DELETE FROM devices');
|
|
const ins = db.prepare("INSERT INTO devices (id, status, user_id, created_at) VALUES (?, ?, ?, strftime('%s','now') - ?)");
|
|
ins.run('old-unclaimed', 'provisioning', null, 25 * 3600); // >24h, unclaimed -> SWEPT
|
|
ins.run('new-unclaimed', 'provisioning', null, 1 * 3600); // <24h, unclaimed -> kept
|
|
ins.run('old-imported', 'provisioning', 'u-imported', 25 * 3600); // >24h but imported (user_id) -> kept
|
|
ins.run('old-online', 'online', null, 25 * 3600); // >24h but not provisioning -> kept
|
|
db.pragma('foreign_keys = ON');
|
|
|
|
assert.equal(db.prepare('SELECT COUNT(*) c FROM devices').get().c, 4, 'seeded 4');
|
|
|
|
const deleted = await pruneProvisioningDevices();
|
|
assert.equal(deleted, 1, 'only the >24h unclaimed provisioning device is swept');
|
|
|
|
const ids = db.prepare('SELECT id FROM devices ORDER BY id').all().map(r => r.id);
|
|
assert.deepEqual(ids, ['new-unclaimed', 'old-imported', 'old-online']);
|
|
// regression guard: a 25h-old row sits well inside the OLD 365-day window, so this
|
|
// would have survived before the fix.
|
|
});
|
|
|
|
test('idempotent: a second sweep with nothing stale deletes nothing', async () => {
|
|
assert.equal(await pruneProvisioningDevices(), 0);
|
|
});
|