State-file age is time since the last transition, not staleness, and a preview's state is the one being decided

This commit is contained in:
Gmer4Lfe
2026-08-22 00:52:38 -04:00
parent 8454d3d0a2
commit 0f228020ef
2 changed files with 79 additions and 24 deletions
+44 -24
View File
@@ -24,9 +24,15 @@
// A missing state file renders as unknown, never as NORMAL — claiming healthy for a
// fallback process that is not running would be the worst possible error on this page.
//
// State age is shown next to state, always. Every value in the state file was written by a
// daemon that may not be running: this host displayed NORMAL from a file five days stale
// with no process alive, and nothing on the page said so.
// Liveness is the PROCESS, never the state file's age. fallback.sh writes that file only on a
// transition — the steady NORMAL path writes nothing — so an untouched file means nothing has
// changed, which is the healthy case. An earlier version of this card read that age as
// staleness and coloured a perfectly good five-day-quiet host red. The row is "Last change"
// and carries no verdict; "is anything watching" is answered by _daemonRow reading /proc.
//
// A running preview's state is shown in place of the live value, marked "preview". A dry run
// redirects its writes to its own copy, so reading the live file while one runs reported a
// healthy preview as UNKNOWN on a host that has never run fallback for real.
//
// Stopping the test is a different button from stopping the daemon, deliberately. The test
// holds an iptables rule that only its own EXIT trap removes, so the two cannot share a
@@ -423,26 +429,24 @@ function _daemonRow(p, checkInterval) {
return ['good', mode + ' · PID ' + p.pid, 'up'];
}
// State freshness. The daemon rewrites this file every check interval, so anything past a few
// intervals is a wedged process or a dead one — and the state value it left behind is a claim
// about the past, not the present.
function _freshness(node, checkInterval) {
const a = node.state_age;
// A dry run redirects its state writes to a per-PID copy so the live file is never touched.
// That is the point of it — but it means the live file goes on ageing while a preview runs,
// and "daemon: dry run" beside "state age: 5d — stale" reads as a fault when it is the
// designed behaviour. Say which it is rather than leaving the operator to chase it.
// Time since the last STATE CHANGE — not staleness, and not a heartbeat.
//
// fallback.sh writes this file only on a transition: the steady NORMAL path logs and writes
// nothing. So a file untouched for five days means "nothing has changed in five days", which on
// this page is the healthy case, and an earlier version of this card labelled exactly that as
// "stale" and coloured it red. Liveness is the PROCESS, which _daemonRow reads directly and is
// the only thing that can answer "is anything watching right now".
function _lastChange(node) {
const dry = node.proc && node.proc.running && node.proc.mode === 'dry-run';
const a = dry ? (node.preview?.age ?? null) : node.state_age;
if (a === null || a === undefined) {
if (dry) return ['dim', 'none — dry run does not write live state'];
if (dry) return ['dim', ''];
return node.is_me
? ['bad', 'no state file — fallback has never run here']
? ['dim', 'never — no state file yet']
: ['dim', 'not readable from here'];
}
const limit = Math.max(120, (checkInterval || 30) * 4);
if (a > limit && dry) return ['dim', _age(a) + ' — not refreshed: dry run writes elsewhere'];
return [a > limit ? 'bad' : 'good', _age(a) + (a > limit ? ' — stale' : '')];
return ['dim', _age(a)];
}
function _leg(ok, label) {
@@ -459,7 +463,11 @@ function _nodeCard(node, data) {
const slot = node.slot;
const [dCls, dTxt, dKind] = _daemonRow(node.proc, data.check_interval);
const [fCls, fTxt] = _freshness(node, data.check_interval);
const [fCls, fTxt] = _lastChange(node);
// A preview running beside an absent or unchanging live state is what is actually being
// decided right now, so it is what the badge shows — labelled, never merged into live state.
const dryRun = node.proc && node.proc.running && node.proc.mode === 'dry-run';
const shown = (dryRun && node.preview && node.preview.state) ? node.preview.state : state;
const test = node.proc_test || {};
const reach = node.reach || {};
@@ -503,8 +511,8 @@ function _nodeCard(node, data) {
<span class="vv-fb-hnm">${vvEscHtml(node.hostname)}</span>
${node.is_me ? '<span class="vv-fb-usbadge">US</span>' : ''}
<span style="flex:1"></span>
${(node.is_me && fCls === 'good') ? _ptStatus(st) : ''}
${_stateBadge(state)}
${(node.is_me && node.proc && node.proc.running && node.proc.mode === 'live') ? _ptStatus(st) : ''}
${_stateBadge(shown)}${dryRun ? '<span class="vv-fb-leg" style="margin-left:4px;">preview</span>' : ''}
</div>
<div class="vv-fb-legs">
@@ -516,7 +524,7 @@ function _nodeCard(node, data) {
<div class="vv-fb-stats">
<b>Daemon</b> <span class="vv-fb-sv ${dCls}">${dTxt}</span>
<b>State age</b> <span class="vv-fb-sv ${fCls}">${fTxt}</span>
<b>Last change</b><span class="vv-fb-sv ${fCls}">${fTxt}</span>
<b>Covers</b> ${covTarget}
<b>Running</b> <span class="vv-fb-sv">${node.running_count ?? 0} containers</span>
${state === 'FALLBACK'
@@ -552,9 +560,21 @@ function _verdict(data) {
if (inFb.length)
return void (el.innerHTML = `<span style="color:#ffb74d;font-weight:600;">${inFb.map(n=>n.id).join(', ')} in FALLBACK</span> — covering for a partner right now.`);
let s = `<span style="color:#4caf50;">Armed</span> · ${live.length}/${nodes.length} node${nodes.length!==1?'s':''} running the daemon`;
if (dry.length) s += ` · <span style="color:#4a9eff;">${dry.length} in dry run</span>`;
if (dead.length) s += ` · <span style="color:#ef5350;">${dead.map(n=>n.id).join(', ')} not running — nothing would detect an outage there</span>`;
// Count every running daemon, then say what KIND. Splitting live from dry-run and reporting
// only the live count read as "0/2 nodes running the daemon · 2 in dry run" — two clauses
// contradicting each other about the same two processes.
const up = live.length + dry.length;
let s = up === nodes.length
? `<span style="color:#4caf50;">Armed</span> · all ${nodes.length} node${nodes.length!==1?'s':''} running`
: `<span style="color:#4caf50;">Armed</span> · ${up}/${nodes.length} node${nodes.length!==1?'s':''} running`;
if (dry.length && !live.length)
s += ` — <span style="color:#4a9eff;">all previewing (dry run), nothing would actually fail over</span>`;
else if (dry.length)
s += ` · <span style="color:#4a9eff;">${dry.map(n=>n.id).join(', ')} previewing (dry run)</span>`;
if (dead.length)
s += ` · <span style="color:#ef5350;">${dead.map(n=>n.id).join(', ')} not running — nothing would detect an outage there</span>`;
el.innerHTML = s;
}
function _setToggles(data) {