mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
The debug block exposed only gauges (buckets, quarantined, inFlight) — state, not work.
A real flapping Firestick reads as flap.buckets:36, quarantined:0, indistinguishable
from healthy. Add lightweight in-memory throughput counters (total + last-completed
rolling window) so the server tells the flapper/flood story itself.
- lib/rolling-counter.js: shared bounded scalar counter (total, curWindow, lastWindow,
windowStart); rolls lazily on bump AND read (no timer), idle decays to 0.
DEBUG_STATS_WINDOW_MS default 60000.
- flap-limiter: refused{Total,LastWindow} (every allow:false), quarantineStarts{Total,
LastWindow} (a quarantine event stays visible after the gauge decays).
- ota-breaker: stats() rateBackoff{Total,LastWindow}.
- ota-download-guard: servedTotal/shedTotal alongside the per-window values.
- database: maintenance sweepsTotal (confirm the prune is firing, not stalled).
- routes/status: debug block gains ota_breaker + the new fields (aggregate-only, cheap).
Tests: rolling-counter window-roll + idle decay; each counter increments on the right
event; booted /api/status asserts the new fields present + numeric. Suite 285/285.
Fallout doc: observability section lists the fields + what each tells a soak-watcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
2.9 KiB
JavaScript
53 lines
2.9 KiB
JavaScript
'use strict';
|
|
// #146 Item C — GLOBAL admission control for /download/apk. NOT per-IP: the fleet SNATs
|
|
// to one IP, so per-IP would collapse the fleet into one bucket. Concurrency + rate caps
|
|
// + critical-band shed protect the loop and IO from a download flood; a per-window
|
|
// aggregate makes the flood visible. Pure + testable; mutates the passed rolling state.
|
|
|
|
const config = require('../config');
|
|
|
|
// newState() — the single bounded rolling counter the endpoint keeps. served/shed are
|
|
// per-window (reset each window); servedTotal/shedTotal are running totals (#146 obs).
|
|
function newState() { return { inFlight: 0, windowStart: 0, windowCount: 0, served: 0, shed: 0, servedTotal: 0, shedTotal: 0 }; }
|
|
|
|
// admit(state, band, now) -> { allow, status?, retryAfter?, summary? }
|
|
// summary (when a window just rolled) = { served, shed } to log, else null.
|
|
// NEVER takes an IP — admission is global by construction.
|
|
//
|
|
// #146 P1.2: caps are BAND-AWARE — a LOAD-TIME backstop, not a healthy-state limiter.
|
|
// - normal : serve FREELY (no concurrency/rate cap) so a coordinated rollout of the
|
|
// whole fleet isn't staggered while the server is perfectly healthy.
|
|
// - elevated : the configured concurrency/rate caps engage (early backpressure).
|
|
// - critical : shed (503) — the real protection.
|
|
// Kill switch: OTA_DOWNLOAD_GUARD_ENABLED=false disables everything (always allow).
|
|
function admit(state, band, now = Date.now()) {
|
|
let summary = null;
|
|
if (now - state.windowStart >= config.otaDownloadWindowMs) {
|
|
if (state.served || state.shed) summary = { served: state.served, shed: state.shed, inFlight: state.inFlight };
|
|
state.windowStart = now; state.windowCount = 0; state.served = 0; state.shed = 0;
|
|
}
|
|
|
|
if (config.otaDownloadGuardEnabled) {
|
|
if (band === 'critical') { state.shed++; state.shedTotal++; return { allow: false, status: 503, retryAfter: 30, summary }; }
|
|
if (band === 'elevated') {
|
|
const overGlobal = state.inFlight >= config.otaDownloadMaxConcurrent || state.windowCount >= config.otaDownloadMaxPerWindow;
|
|
if (overGlobal) { state.shed++; state.shedTotal++; return { allow: false, status: 503, retryAfter: 10, summary }; }
|
|
}
|
|
// band === 'normal': no cap — serve freely.
|
|
}
|
|
|
|
state.inFlight++; state.windowCount++; state.served++; state.servedTotal++;
|
|
return { allow: true, summary };
|
|
}
|
|
|
|
// release() — call when a served response finishes/closes (once).
|
|
function release(state) { state.inFlight = Math.max(0, state.inFlight - 1); }
|
|
|
|
// #146 P3.8: the production singleton state + a stats view for /api/status. Tests use
|
|
// newState() for isolation.
|
|
const _prod = newState();
|
|
function prodState() { return _prod; }
|
|
function stats() { return { inFlight: _prod.inFlight, servedThisWindow: _prod.served, shedThisWindow: _prod.shed, servedTotal: _prod.servedTotal, shedTotal: _prod.shedTotal }; }
|
|
|
|
module.exports = { newState, admit, release, prodState, stats };
|