igneum/tools/reliability/fake-worker.mjs
igneum-josh f39e240d2c App engine: watchdog per card and per node, WORKER FAULT and mismatched= read from the miner; public GPU bench table and the reliability test harness
Review round 4 X21 and Josh's levers 5 and 6 (4 October 2026 evening):
- src/watchdog.rs: CardWatch (no status line for 90 s, or hash rate 0 for 60 s while the node is synced: one
  restart, then the card is marked faulted with the reason in the UI and the log; the miner's own worker restarts
  suspend both rules until ready, so there are no double restarts; exit 43 counts as a watchdog restart) and
  NodeWatch (our node silent for 120 s is restarted in-process through the restart kind the remote jobs use, with a
  growing delay). State machines with no clock; 11 unit tests replay recorded STATUS and WORKER FAULT lines.
- engine.rs reads STATUS mismatched=, faults= and the WORKER FAULT lines onto the card (faults, mismatched, message);
  a faulted card is not restarted until the user changes its settings or resumes mining; other cards keep mining.
- site/miners.html (build.mjs, scrubbed like /bench, rows from site/miner-bench.json): card, generator version, best
  MH/s, MH per watt where measured, miner version, date, source, measured by the team or reported by the fleet. In the
  navigation beside the engineering log on every page and in the sitemap. One line says there is no other miner to
  compare with.
- tools/reliability: fake-worker.mjs (the serve protocol, misbehaving on command), run.mjs (miner guards on a private
  test network, ports 29950+) and app-run.mjs (the app watchdog end to end, ports 29960+).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 20:20:49 +01:00

74 lines
3.3 KiB
JavaScript
Executable file

#!/usr/bin/env node
// A GPU worker stand-in that speaks the miner's serve protocol (ready / job / done / found / prepare / prepared /
// quit) and misbehaves on command, so the miner's fault guards and the app's watchdog can be measured on a Mac with
// no faulty GPU at hand. It never hashes: every `done` is invented, every `found` is wrong on purpose.
//
// FAKE_WORKER_CTL=<file> the file's first word is the mode, re-read before every job:
// ok every job: `done` after 100 ms with the job's nonce count as hashes (about 167 MH/s at 2^24 nonces)
// slow `done` after 5,000 ms (a self-heal build inside a job, a game on the GPU)
// fast `done` after 0.3 ms with the full count: a runtime answering success while running nothing
// (the 4 October 2026 gfx1036 fault)
// zero `done` after 100 ms with 0 hashes: the worker completes jobs and produces nothing
// silent never answers a job (the queue fills and nothing comes back)
// badfound a `found` with a wrong hash before every `done`: a worker on a wrong program
// preparefail every `prepare` is answered `prepare-failed`; jobs run as in ok
// exit the process exits with code 7 at the next job
// The mode is read from argv `--mode <m>` when no control file is set. `--serve` is accepted and ignored.
// Lines on stderr are prefixed `fake-worker:` and never match a miner pattern.
import { readFileSync, existsSync, writeSync } from 'node:fs';
import { createInterface } from 'node:readline';
const ctl = process.env.FAKE_WORKER_CTL;
const argMode = (() => { const i = process.argv.indexOf('--mode'); return i >= 0 ? process.argv[i + 1] : 'ok'; })();
function mode() {
if (ctl && existsSync(ctl)) {
const m = readFileSync(ctl, 'utf8').trim().split(/\s+/)[0];
if (m) return m;
}
return argMode;
}
// synchronous writes: a piped stdout is asynchronous in Node and process.exit would drop pending lines
const out = (s) => { writeSync(1, s + '\n'); };
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
out('ready fake Fake_GPU dataset-log2 28 batch 16777216 prepare 1');
const queue = [];
let running = false;
async function pump() {
if (running) return;
running = true;
while (queue.length) {
const f = queue.shift();
const [id, , , nonceStart, nonces] = f;
const m = mode();
if (m === 'exit') process.exit(7);
if (m === 'silent') { await sleep(1_000_000); continue; }
const t0 = performance.now();
let hashes = Number(nonces);
let ms = 100;
if (m === 'slow') ms = 5000;
else if (m === 'fast') ms = 0.3;
else if (m === 'zero') hashes = 0;
await sleep(ms);
if (m === 'badfound') out(`found ${id} ${BigInt(nonceStart) + 17n} 0000000000000001`);
out(`done ${id} ${hashes} ${(performance.now() - t0).toFixed(2)}`);
}
running = false;
}
const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
const f = line.trim().split(/\s+/);
switch (f[0]) {
case 'job': queue.push(f.slice(1)); pump(); break;
case 'prepare':
if (mode() === 'preparefail') out(`prepare-failed ${f[1]} ${f[2]} fake worker refuses`);
else setTimeout(() => out(`prepared ${f[1]} ${f[2]} 50 fake resident 2 programs 2 datasets`), 50);
break;
case 'quit': process.exit(0);
default: break;
}
});
rl.on('close', () => process.exit(0));