mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Don't let telemetry/logging cook the loop under a storm.
- lib/log-coalescer.js: dedup+count high-frequency lines, flush ONE summarized line per
key per window ("[loop-lag] band=critical (x47 in 30s)"). Bounded buffer (auto-flush
at MAX_KEYS). Applied to the loop-lag "still loaded" line (band CHANGES stay immediate),
the per-request OTA check line, and "Device reconnected".
- loop-lag: event_loop_lag rows are BUFFERED and batch-inserted on a flush interval
(was a synchronous INSERT per sample); the buffer is bounded (drop-oldest). Its
retention prune now rides the Item-A chunkedDelete so this table can never repeat the
status_log bloat-then-freeze. /api/status still reads in-memory current (real-time
band unaffected).
- Bounded the previously un-evicted per-device Maps: content-ack limiter gets an idle
sweep (started in server.js); status-log-writer.lastWritten is capped (drop-oldest;
it only suppresses a redundant consecutive row, so eviction is safe).
Tests: N identical lines -> one counted line; single line verbatim; coalescer buffer
bounded under a distinct-key flood; content-ack Map swept of idle buckets.
loop-lag-integration updated for the batched-insert cadence. Suite 266/266.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
3.9 KiB
JavaScript
82 lines
3.9 KiB
JavaScript
// #143 — content-ack flood control (the single control on the content-ack path).
|
|
//
|
|
// Folds three concerns into ONE per-device limiter so there are no competing
|
|
// limiters on this path (reconnect-throttle.js is left untouched):
|
|
// 1. #142 dedup — drop an exact (content_id, status) repeat within the dedup
|
|
// window. Legit repeat suppression; does NOT consume rate budget.
|
|
// 2. #143 per-device RATE budget — cap TOTAL non-duplicate acks per device per
|
|
// window regardless of differing content_id. This is what dedup misses: a
|
|
// device cycling 2-4 ids makes each ack look unique, so dedup never fires,
|
|
// but aggregate volume still floods the loop. Over budget -> shed silently.
|
|
// 3. #143 global pressure valve — when loop-lag (services/loop-lag.js) reports
|
|
// the CRITICAL band, shed non-essential acks even for a device within its own
|
|
// budget. Reuses the existing band + hysteresis; never fires below critical.
|
|
//
|
|
// Per-device, in-memory, resets on restart (like lastPlayLogAt / pair-lockout).
|
|
// Fixed window (counter reset per window) — simple and makes "log once per window"
|
|
// natural. `band` is injected so this is testable without the loop-lag monitor.
|
|
|
|
const config = require('../config');
|
|
|
|
// deviceId -> { winStart, count, shedNotified, dup: Map(content|status -> ts) }
|
|
const state = new Map();
|
|
|
|
// Returns one of:
|
|
// { action: 'pass' } -> caller logs + emits
|
|
// { action: 'dedup' } -> drop (exact repeat)
|
|
// { action: 'shed-rate', logStart, observed, budget } -> drop (over per-device budget)
|
|
// { action: 'shed-valve' } -> drop (global critical-lag valve)
|
|
function check(deviceId, contentId, status, band = 'normal', now = Date.now()) {
|
|
let s = state.get(deviceId);
|
|
if (!s) { s = { winStart: now, count: 0, shedNotified: false, dup: new Map() }; state.set(deviceId, s); }
|
|
|
|
// Roll the fixed rate window.
|
|
if (now - s.winStart >= config.contentAckRateWindowMs) {
|
|
s.winStart = now;
|
|
s.count = 0;
|
|
s.shedNotified = false;
|
|
// Bound the dedup map: drop entries older than the dedup window.
|
|
for (const [k, t] of s.dup) if (now - t >= config.contentAckDedupMs) s.dup.delete(k);
|
|
}
|
|
|
|
// 1) Dedup — exact (content, status) repeat within the dedup window. Does NOT
|
|
// consume rate budget (it's a legit repeat we simply suppress).
|
|
const key = `${contentId}|${status}`;
|
|
if (now - (s.dup.get(key) || 0) < config.contentAckDedupMs) return { action: 'dedup' };
|
|
s.dup.set(key, now);
|
|
|
|
// 2) Per-device rate budget — always applies, counts all non-duplicate acks.
|
|
s.count++;
|
|
if (s.count > config.contentAckMaxPerWindow) {
|
|
const logStart = !s.shedNotified; // log ONCE per device per window when shedding starts
|
|
s.shedNotified = true;
|
|
return { action: 'shed-rate', logStart, observed: s.count, budget: config.contentAckMaxPerWindow };
|
|
}
|
|
|
|
// 3) Global valve — extra shedding only under critical lag; a within-budget device
|
|
// in a non-critical band is never touched here.
|
|
if (band === 'critical') return { action: 'shed-valve' };
|
|
|
|
return { action: 'pass' };
|
|
}
|
|
|
|
// #146 Item E: evict idle per-device buckets so this Map can't grow unbounded over
|
|
// churned device_ids (a SNAT flood minting provisioning ids inflated every un-swept
|
|
// per-device Map). Keyed by winStart age.
|
|
function sweep(now = Date.now()) {
|
|
let n = 0;
|
|
const idle = config.contentAckRateWindowMs * 4;
|
|
for (const [k, s] of state) if (now - s.winStart > idle) { state.delete(k); n++; }
|
|
return n;
|
|
}
|
|
let sweepTimer = null;
|
|
function startSweep() {
|
|
if (sweepTimer) return sweepTimer;
|
|
sweepTimer = setInterval(() => sweep(), config.contentAckRateWindowMs * 4);
|
|
if (sweepTimer.unref) sweepTimer.unref();
|
|
return sweepTimer;
|
|
}
|
|
|
|
function reset() { state.clear(); } // tests
|
|
module.exports = { check, reset, sweep, startSweep, _size: () => state.size };
|