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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user