mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Field-safe SERVER net. A device opening duplicate/rapid sockets (the APK duplicate-socket bug, separate track) currently churns through evictions during the reconnect-throttle's 30s post-restart WARM-UP (only the hard ceiling 20 applies then, so an 8-in-9s burst passes undamped and each new socket evicts the prior). This makes the server absorb it: a thrashing PAIRED device converges to ONE stable connection and stays online. - lib/session-settle.js (decision only; bounded, swept): shouldHold(deviceId, incumbentAlive) — true only when a socket was accepted for this device within SESSION_SETTLE_WINDOW_MS (config, default 2500ms) AND the incumbent is alive. Warm-up-independent. - deviceSocket register gate (just before evictPriorSocket): if a LIVE incumbent exists and we're inside the window, SOFT-REFUSE the new socket (device:throttled reason=session_settle + disconnect) and keep the incumbent; else accept + evict + (re)arm the window. - LIVENESS SAFEGUARD (load-bearing): only hold when the incumbent socket is actually in the /device namespace — a dead/half-open incumbent is replaced, NEVER stranding the device (max hold is the 2.5s window from the incumbent's accept, then any new socket is accepted). - Soft refusal, NEVER a quarantine (reuses patch1's paired-safe philosophy); single-session enforcement intact for a legitimate move; unpaired/abusive flapping still caught by the existing limiters. O(1), no loop impact. Tests (liveness first-class): live incumbent holds + DEAD incumbent replaced (not stranded); storm of 6 sockets converges to ONE, stays online, not quarantined (during warm-up); single- session move past the window replaces cleanly; unit decision + bounded sweep. The evicted-socket-rearm test shrinks its settle window so it still exercises the eviction path. Suite 336/336.
42 lines
1.6 KiB
JavaScript
42 lines
1.6 KiB
JavaScript
'use strict';
|
|
|
|
// #148 patch2 — session-settle DECISION unit tests. The liveness safeguard is the load-bearing
|
|
// one: a dead incumbent must NEVER be held (else we recreate #148 by stranding the device).
|
|
|
|
process.env.SESSION_SETTLE_WINDOW_MS = '2500';
|
|
const { test, beforeEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const ss = require('../lib/session-settle');
|
|
|
|
beforeEach(() => ss.reset());
|
|
|
|
test('HOLD: live incumbent + a socket accepted within the window -> refuse the duplicate', () => {
|
|
ss.accepted('dev', 1000);
|
|
assert.equal(ss.shouldHold('dev', true, 1500), true); // 500ms into the 2500ms window, incumbent alive
|
|
});
|
|
|
|
test('LIVENESS SAFEGUARD (critical): a DEAD incumbent is NEVER held -> accept the new socket', () => {
|
|
ss.accepted('dev', 1000);
|
|
// Within the window, but the caller reports the incumbent is not alive -> must NOT hold,
|
|
// so the new socket is accepted and the corpse evicted (device never stranded offline).
|
|
assert.equal(ss.shouldHold('dev', false, 1500), false);
|
|
});
|
|
|
|
test('window elapsed -> accept (a genuine move to a new socket still works)', () => {
|
|
ss.accepted('dev', 1000);
|
|
assert.equal(ss.shouldHold('dev', true, 1000 + 2500), false); // exactly at the edge
|
|
assert.equal(ss.shouldHold('dev', true, 1000 + 5000), false);
|
|
});
|
|
|
|
test('first connection (no prior accept) -> accept', () => {
|
|
assert.equal(ss.shouldHold('fresh', true, 9999), false);
|
|
});
|
|
|
|
test('bounded: sweep drops entries idle past 4x the window', () => {
|
|
ss.accepted('a', 1000);
|
|
ss.accepted('b', 1000);
|
|
assert.equal(ss._size(), 2);
|
|
ss.sweep(1000 + 2500 * 4 + 1);
|
|
assert.equal(ss._size(), 0);
|
|
});
|