mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
feat(#146) P3.8: soak observability block on /api/status
Expose the new internal states so we can SEE the limiters biting during the alpha soak
instead of grepping logs. /api/status now carries debug: {
flap: {buckets, quarantined},
ota_download: {inFlight, servedThisWindow, shedThisWindow, windowCount},
maintenance: {deleted, ms, at, running}, // last status-log prune
log_coalescer_buffer,
}. Aggregate counts only (no device ids/secrets), cheap in-memory reads. stats()
added to flap-limiter + ota-download-guard (singleton prod state), getMaintenanceStats
from database. Asserted in the booted /api/status test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0f990c2e7e
commit
bfa99771ca
|
|
@ -772,10 +772,13 @@ const { applyTenantDeleteCascade } = require('../lib/tenant-cascade-migration');
|
|||
// table self-heals on next deploy without a restart.
|
||||
// Rides idx_device_status_log_device_ts(device_id, timestamp).
|
||||
let _statusPruneRunning = false;
|
||||
let _lastPrune = { deleted: 0, ms: 0, at: 0 }; // #146 P3.8: soak observability
|
||||
function getMaintenanceStats() { return { ..._lastPrune, running: _statusPruneRunning }; }
|
||||
async function pruneStatusLog(opts = {}) {
|
||||
if (_statusPruneRunning) return 0; // re-entrancy: work runs once
|
||||
if (opts.bandGate && config.maintenanceBandGateEnabled && currentBand() !== 'normal') return 0;
|
||||
_statusPruneRunning = true;
|
||||
const _t0 = Date.now();
|
||||
try {
|
||||
const batch = config.statusLogPruneBatch;
|
||||
const cap = config.statusLogMaxRowsPerDevice;
|
||||
|
|
@ -796,6 +799,7 @@ async function pruneStatusLog(opts = {}) {
|
|||
await yieldTick(); // breathe between devices
|
||||
}
|
||||
if (total > 0) console.log(`[status-log] pruned ${total} row(s) (per-device, newest ${cap}/device + ${config.statusLogRetentionDays}d retention, batches of ${batch})`);
|
||||
_lastPrune = { deleted: total, ms: Date.now() - _t0, at: Math.floor(Date.now() / 1000) };
|
||||
return total;
|
||||
} catch (_) { return 0; } finally { _statusPruneRunning = false; }
|
||||
}
|
||||
|
|
@ -873,4 +877,4 @@ try {
|
|||
const { verifyAndRepairSchema } = require('../lib/schema-check');
|
||||
verifyAndRepairSchema(db);
|
||||
|
||||
module.exports = { db, pruneTelemetry, pruneScreenshots, pruneStatusLog };
|
||||
module.exports = { db, pruneTelemetry, pruneScreenshots, pruneStatusLog, getMaintenanceStats };
|
||||
|
|
|
|||
|
|
@ -88,4 +88,10 @@ function startSweep() {
|
|||
|
||||
function reset() { state.clear(); } // tests
|
||||
function _size() { return state.size; }
|
||||
module.exports = { check, sweep, startSweep, reset, _size };
|
||||
// #146 P3.8: soak observability — bucket count + currently-quarantined count.
|
||||
function stats(now = Date.now()) {
|
||||
let quarantined = 0;
|
||||
for (const [, s] of state) if (now < s.quarantinedUntil) quarantined++;
|
||||
return { buckets: state.size, quarantined };
|
||||
}
|
||||
module.exports = { check, sweep, startSweep, reset, _size, stats };
|
||||
|
|
|
|||
|
|
@ -42,4 +42,10 @@ function admit(state, band, now = Date.now()) {
|
|||
// release() — call when a served response finishes/closes (once).
|
||||
function release(state) { state.inFlight = Math.max(0, state.inFlight - 1); }
|
||||
|
||||
module.exports = { newState, admit, release };
|
||||
// #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, windowCount: _prod.windowCount }; }
|
||||
|
||||
module.exports = { newState, admit, release, prodState, stats };
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ const config = require('../config');
|
|||
const VERSION = require('../version');
|
||||
const { PLATFORM_ROLES } = require('../middleware/auth');
|
||||
const loopLag = require('../services/loop-lag');
|
||||
// #146 P3.8: soak observability — internal limiter/maintenance states.
|
||||
const flapLimiter = require('../lib/flap-limiter');
|
||||
const otaDownloadGuard = require('../lib/ota-download-guard');
|
||||
const logCoalescer = require('../lib/log-coalescer');
|
||||
const { getMaintenanceStats } = require('../db/database');
|
||||
|
||||
// Public status page
|
||||
router.get('/', (req, res) => {
|
||||
|
|
@ -28,6 +33,14 @@ router.get('/', (req, res) => {
|
|||
// #142: current event-loop lag snapshot, so site lag is diagnosable from the
|
||||
// health endpoint independent of any throttling. Cheap (in-memory read).
|
||||
loop_lag: loopLag.getLag(),
|
||||
// #146 P3.8: soak observability — see the limiters biting without grepping logs.
|
||||
// Aggregate counts only (no device ids / secrets); cheap in-memory reads.
|
||||
debug: {
|
||||
flap: flapLimiter.stats(), // { buckets, quarantined }
|
||||
ota_download: otaDownloadGuard.stats(), // { inFlight, servedThisWindow, shedThisWindow, windowCount }
|
||||
maintenance: getMaintenanceStats(), // { deleted, ms, at, running } of the last status-log prune
|
||||
log_coalescer_buffer: logCoalescer._size(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -737,7 +737,7 @@ function logOtaCheck(deviceId, client, latest, available, reason) {
|
|||
// #146 Item C: GLOBAL download admission (lib/ota-download-guard) — concurrency + rate
|
||||
// caps + critical-band shed, NEVER per-IP (SNAT). Single bounded rolling state.
|
||||
const otaDownloadGuard = require('./lib/ota-download-guard');
|
||||
const otaDownloadState = otaDownloadGuard.newState();
|
||||
const otaDownloadState = otaDownloadGuard.prodState(); // #146 P3.8: shared singleton so /api/status can read stats
|
||||
|
||||
app.get('/download/apk', (req, res) => {
|
||||
const apk = apkCache.get();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ test('/api/status exposes a current loop_lag snapshot', async () => {
|
|||
assert.ok(['normal', 'elevated', 'critical'].includes(body.loop_lag.band), 'band is a valid level');
|
||||
assert.equal(typeof body.loop_lag.p99_ms, 'number', 'p99_ms is numeric');
|
||||
assert.equal(typeof body.loop_lag.mean_ms, 'number', 'mean_ms is numeric');
|
||||
// #146 P3.8: soak observability block
|
||||
assert.ok(body.debug, 'debug block present');
|
||||
assert.equal(typeof body.debug.flap.buckets, 'number', 'flap bucket count exposed');
|
||||
assert.equal(typeof body.debug.flap.quarantined, 'number', 'flap quarantine count exposed');
|
||||
assert.equal(typeof body.debug.ota_download.inFlight, 'number', 'download in-flight exposed');
|
||||
assert.ok('maintenance' in body.debug && typeof body.debug.maintenance.ms === 'number', 'last-prune stats exposed');
|
||||
assert.equal(typeof body.debug.log_coalescer_buffer, 'number', 'coalescer buffer size exposed');
|
||||
});
|
||||
|
||||
test('lag samples are persisted AND bounded by retention prune (not unbounded)', async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue