Initial import of verae-fleet from zapier monorepo

This commit is contained in:
George Lambert 2026-09-11 14:37:13 -04:00
commit eaa16a145d
42 changed files with 3086 additions and 0 deletions

86
src/agent.js Normal file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Remote host agent: spawn/kill fleet workers so the control plane can spread replicas.
*
* FLEET_AGENT_PORT=3851 node src/agent.js
*
* Bind loopback or a private interface. Do not publish NATS.
*/
import http from 'node:http';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const WORKER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'worker.js');
const PORT = Number(process.env.FLEET_AGENT_PORT || 3851);
const BIND = process.env.FLEET_AGENT_BIND || '127.0.0.1';
const children = new Map();
function json(res, code, obj) {
res.writeHead(code, { 'content-type': 'application/json' });
res.end(JSON.stringify(obj));
}
async function readBody(req) {
const chunks = [];
for await (const c of req) chunks.push(c);
const raw = Buffer.concat(chunks).toString('utf8');
return raw ? JSON.parse(raw) : {};
}
export function startAgent({ port = PORT, bind = BIND } = {}) {
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${bind}:${port}`);
try {
if (req.method === 'GET' && url.pathname === '/health') {
return json(res, 200, {
ok: true,
role: 'fleet-agent',
workers: [...children.keys()],
});
}
if (req.method === 'POST' && url.pathname === '/spawn') {
const body = await readBody(req);
const instance = body.instance;
if (!instance) return json(res, 400, { error: 'instance required' });
if (children.has(instance)) {
return json(res, 200, { pid: children.get(instance).pid, instance, existing: true });
}
const child = spawn(process.execPath, [WORKER], {
env: { ...process.env, ...(body.env || {}) },
stdio: ['ignore', 'pipe', 'pipe'],
});
children.set(instance, child);
child.on('exit', () => children.delete(instance));
return json(res, 200, { pid: child.pid, instance });
}
if (req.method === 'POST' && url.pathname === '/kill') {
const body = await readBody(req);
const child = children.get(body.instance);
if (child) {
child.kill('SIGTERM');
children.delete(body.instance);
}
return json(res, 200, { killed: Boolean(child), instance: body.instance });
}
json(res, 404, { error: 'not found' });
} catch (err) {
json(res, 400, { error: err.message });
}
});
server.listen(port, bind, () => {
process.stdout.write(`fleet-agent http://${bind}:${port}/\n`);
});
return {
server,
async close() {
for (const c of children.values()) c.kill('SIGTERM');
children.clear();
await new Promise((r) => server.close(r));
},
};
}
if (process.argv[1] && path.basename(process.argv[1]) === 'agent.js') {
startAgent();
}

29
src/classify.js Normal file
View file

@ -0,0 +1,29 @@
/**
* Row health: operational | degraded | down
* @module classify
*/
/**
* @param {{
* min?: number,
* available?: number,
* running?: number,
* paused?: number,
* managed?: boolean,
* instances?: Array<{ pid?: number|null, paused?: boolean, healthy?: boolean }>
* }} sv
* @returns {'operational'|'degraded'|'down'}
*/
export function classifyService(sv) {
const min = sv.min ?? 0;
const available = sv.available ?? 0;
const running = sv.running ?? 0;
const paused = sv.paused ?? 0;
const inst = sv.instances || [];
const unhealthy = inst.filter((i) => i.pid && i.healthy === false && !i.paused).length;
if (min === 0 && running === 0) return 'operational';
if (available === 0 && min > 0) return 'down';
if (available >= min && paused === 0 && unhealthy === 0) return 'operational';
return 'degraded';
}

145
src/cli.js Normal file
View file

@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* verae-fleet catalog, monitor, restart, pause.
*
* Local (no daemon): list
* Daemon: serve | monitor
* Against daemon: status | start | stop | pause | resume | restart
*/
import http from 'node:http';
import { loadFleet, listServices } from './load.js';
import { Supervisor } from './supervisor.js';
import { Monitor } from './monitor.js';
import { startControlServer } from './server.js';
const loaded = loadFleet();
const [cmd = 'help', target] = process.argv.slice(2);
const BASE = process.env.FLEET_URL || `http://127.0.0.1:${loaded.control.port}`;
function print(obj) {
process.stdout.write(`${typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2)}\n`);
}
function help() {
print(`verae-fleet
list services, config files, replica min/max
serve start floors + monitor + UI ${BASE}/
monitor same without printing extra
status GET daemon status
start <service> turn on, spawn up to min
stop <service|instance> turn off (service disable) or stop one replica
pause <service|instance> pause (does not count toward tree-node floor)
resume <service|instance>
restart <instance> kill + respawn
reconcile force floor check
Central spec: fleet.json
Machines: machines.json (add hosts to spread replicas)
Per service: services/<id>.json
Remote SSH: machines.json user + host + identityFile (path only)
ssh-check [id] test SSH login (default ns1)
`);
}
function api(method, pathname) {
return new Promise((resolve, reject) => {
const u = new URL(pathname, BASE);
const req = http.request(
{ hostname: u.hostname, port: u.port, path: u.pathname, method, timeout: 8000 },
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try {
resolve({ status: res.statusCode, body: JSON.parse(raw) });
} catch {
resolve({ status: res.statusCode, body: raw });
}
});
},
);
req.on('error', reject);
req.end();
});
}
async function main() {
if (cmd === 'help' || cmd === '-h' || cmd === '--help') return help();
if (cmd === 'list') {
for (const r of listServices(loaded)) {
print(
`${r.id.padEnd(22)} min=${String(r.min).padStart(2)} max=${String(r.max).padStart(2)} floor=${r.keepFloor ? 'yes' : 'no '} managed=${r.managed ? 'yes' : 'no '} ${r.configPath}`,
);
}
print(`\ncentral replica spec: ${loaded.fleetPath}`);
print(`tree-node floor: min=${loaded.services['tree-node'].min} keepFloor=${loaded.services['tree-node'].keepFloor}`);
print('\nmachines:');
for (const m of loaded.machines) {
const ssh = m.kind === 'ssh' ? `${m.user}@${m.host} key=${m.identityFile}` : m.host;
print(` ${m.id.padEnd(12)} ${m.kind.padEnd(6)} ${m.enabled ? 'on ' : 'off'} cap=${m.capacity} ${ssh}`);
}
return;
}
if (cmd === 'ssh-check') {
const { sshCheck } = await import('./ssh.js');
const id = target || 'ns1';
const m = loaded.machines.find((x) => x.id === id);
if (!m) throw new Error(`unknown machine ${id}`);
print(await sshCheck(m));
return;
}
if (cmd === 'serve' || cmd === 'monitor') {
const sup = new Supervisor({ loaded });
for (const id of Object.keys(loaded.services)) {
const s = loaded.services[id];
if (s.managed && s.enabled && s.min > 0) await sup.startService(id);
}
const mon = new Monitor(sup);
await mon.tick();
mon.start();
startControlServer(sup, mon);
print(`fleet ${cmd} pid=${process.pid}\nUI ${BASE}/\nSIGINT stops all managed replicas`);
process.on('SIGINT', async () => {
mon.stop();
await sup.stopAll();
process.exit(0);
});
process.on('SIGTERM', async () => {
mon.stop();
await sup.stopAll();
process.exit(0);
});
return;
}
const instance = target && /-\d+$/.test(target);
const map = {
status: ['GET', '/api/status'],
reconcile: ['POST', '/api/reconcile'],
};
if (map[cmd] && !target) {
const [m, p] = map[cmd];
const r = await api(m, p);
print(r.body);
if (r.status >= 400) process.exitCode = 1;
return;
}
if (!target) {
help();
process.exitCode = 1;
return;
}
const pathName = instance
? `/api/instances/${target}/${cmd}`
: `/api/services/${target}/${cmd}`;
const r = await api('POST', pathName);
print(r.body);
if (r.status >= 400) process.exitCode = 1;
}
main().catch((err) => {
print({ error: err.message, hint: 'Is `verae-fleet serve` running?' });
process.exitCode = 1;
});

118
src/health.js Normal file
View file

@ -0,0 +1,118 @@
/**
* Probe a replica: HTTP /health or TCP connect.
* @module health
*/
import http from 'node:http';
import net from 'node:net';
import fs from 'node:fs';
import path from 'node:path';
/**
* @param {number|{ host?: string, port: number, path?: string, timeoutMs?: number }} portOrOpts
*/
export function httpProbe(portOrOpts, pathname = '/health', timeoutMs = 800) {
const opts =
typeof portOrOpts === 'object'
? portOrOpts
: { port: portOrOpts, path: pathname, timeoutMs };
const host = opts.host || '127.0.0.1';
const port = opts.port;
const pathName = opts.path || pathname;
const tmo = opts.timeoutMs || timeoutMs;
return new Promise((resolve) => {
const req = http.get(
{ host, port, path: pathName, timeout: tmo },
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
let body = {};
try {
body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
} catch {
body = {};
}
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300 && body.ok !== false,
statusCode: res.statusCode,
paused: Boolean(body.paused),
body,
});
});
},
);
req.on('error', (err) => resolve({ ok: false, error: err.message, paused: false }));
req.on('timeout', () => {
req.destroy();
resolve({ ok: false, error: 'timeout', paused: false });
});
});
}
export function tcpProbe(host, port, timeoutMs = 400) {
return new Promise((resolve) => {
const sock = net.connect({ host, port });
const done = (ok, error) => {
try {
sock.destroy();
} catch {
/* ignore */
}
resolve({ ok, error, paused: false });
};
sock.setTimeout(timeoutMs);
sock.on('connect', () => done(true));
sock.on('error', (err) => done(false, err.message));
sock.on('timeout', () => done(false, 'timeout'));
});
}
/**
* @param {number|{ host?: string, port: number, path?: string, body?: object }} portOrOpts
* @param {string} [pathname]
*/
export function postControl(portOrOpts, pathname = '/') {
const opts = typeof portOrOpts === 'object' ? portOrOpts : { port: portOrOpts, path: pathname };
const host = opts.host || '127.0.0.1';
const port = opts.port;
const pathName = opts.path || pathname;
const body = opts.body ? JSON.stringify(opts.body) : '';
return new Promise((resolve, reject) => {
const req = http.request(
{
host,
port,
path: pathName,
method: 'POST',
timeout: opts.timeoutMs || 2000,
headers: body ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) } : {},
},
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
} catch {
resolve({});
}
});
},
);
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
export function heartbeatFresh(stateDir, maxAgeMs) {
const file = path.join(stateDir, 'heartbeat.json');
try {
const st = fs.statSync(file);
const body = JSON.parse(fs.readFileSync(file, 'utf8'));
return { ok: Date.now() - st.mtimeMs < maxAgeMs && body.ok !== false, body, ageMs: Date.now() - st.mtimeMs };
} catch (err) {
return { ok: false, error: err.message };
}
}

94
src/load.js Normal file
View file

@ -0,0 +1,94 @@
/**
* Load central fleet.json + per-service JSON configs.
* @module load
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadMachines } from './machines.js';
const HERE = path.dirname(fileURLToPath(import.meta.url));
export const FLEET_ROOT = path.resolve(HERE, '..');
export function readJson(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
/**
* @param {string} [root]
*/
export function loadFleet(root = FLEET_ROOT, opts = {}) {
const fleetPath = path.join(root, 'fleet.json');
const fleet = readJson(fleetPath);
const dir = path.join(root, 'services');
const services = {};
const unmanaged = [];
for (const name of fs.readdirSync(dir).sort()) {
if (!name.endsWith('.json')) continue;
const spec = readJson(path.join(dir, name));
const configPath = path.join('services', name);
if (spec.id === '_unmanaged') {
for (const e of spec.entries || []) {
unmanaged.push({ ...e, managed: false, configPath });
}
continue;
}
const central = fleet.services?.[spec.id] || {};
services[spec.id] = {
...spec,
configPath,
min: central.min ?? 0,
max: central.max ?? 1,
keepFloor: central.keepFloor !== false,
enabled: central.enabled !== false,
managed: central.managed === false ? false : spec.managed !== false,
};
}
const overlay =
opts.overlay === false ? null : path.join(root, 'data', 'runtime', 'machines-overlay.json');
const machines = loadMachines(root, overlay);
return {
root,
fleetPath: path.relative(root, fleetPath) || 'fleet.json',
machinesPath: 'machines.json',
machinesOverlay: overlay,
control: fleet.control,
nats: fleet.nats,
machines,
services,
unmanaged,
};
}
export function listServices(loaded) {
const rows = Object.values(loaded.services).map((s) => ({
id: s.id,
title: s.title,
kind: s.kind,
managed: s.managed,
enabled: s.enabled,
min: s.min,
max: s.max,
keepFloor: s.keepFloor,
configPath: s.configPath,
package: s.package,
runtime: s.runtime,
}));
for (const u of loaded.unmanaged) {
rows.push({
id: u.id,
title: u.id,
kind: 'unmanaged',
managed: false,
enabled: false,
min: 0,
max: 0,
keepFloor: false,
configPath: u.configPath,
package: u.package,
runtime: u.runtime,
});
}
return rows;
}

107
src/machines.js Normal file
View file

@ -0,0 +1,107 @@
/**
* Host catalog: where replicas may run. Least-loaded placement.
* @module machines
*/
import fs from 'node:fs';
import path from 'node:path';
export function defaultMachines() {
return [
{
id: 'local',
title: 'Control plane (this host)',
kind: 'local',
host: '127.0.0.1',
enabled: true,
capacity: 32,
roles: ['*'],
},
];
}
export function loadMachines(root, overlayPath) {
const file = path.join(root, 'machines.json');
let list = defaultMachines();
if (fs.existsSync(file)) {
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
if (Array.isArray(raw.machines) && raw.machines.length) list = raw.machines;
}
if (overlayPath && fs.existsSync(overlayPath)) {
const extra = JSON.parse(fs.readFileSync(overlayPath, 'utf8'));
const incoming = Array.isArray(extra) ? extra : extra.machines || [];
for (const m of incoming) upsertMachine(list, m);
}
const secretsPath = path.join(root, 'machines.secrets.json');
if (fs.existsSync(secretsPath)) {
const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
const byId = secrets.machines || secrets;
for (const m of list) {
const extra = byId[m.id];
if (extra && typeof extra === 'object') Object.assign(m, extra);
}
}
return list.map(normalizeMachine);
}
export function normalizeMachine(m) {
const kind = m.kind === 'agent' || m.kind === 'ssh' ? m.kind : 'local';
return {
id: String(m.id || '').trim(),
title: m.title || m.id,
kind,
host: m.host || '127.0.0.1',
user: m.user || (kind === 'ssh' ? 'marchon' : ''),
sshPort: Number(m.sshPort || 22),
identityFile: m.identityFile || (kind === 'ssh' ? '~/.ssh/id_ed25519' : ''),
remoteDir: m.remoteDir || '~/verae-fleet-runtime',
agentPort: Number(m.agentPort || 3851),
enabled: m.enabled !== false,
capacity: Number(m.capacity || 8),
roles: Array.isArray(m.roles) && m.roles.length ? m.roles : ['*'],
notes: m.notes || '',
};
}
export function upsertMachine(list, spec) {
const m = normalizeMachine(spec);
if (!m.id) throw new Error('machine id required');
const i = list.findIndex((x) => x.id === m.id);
if (i >= 0) list[i] = { ...list[i], ...m };
else list.push(m);
return m;
}
export function canHost(machine, serviceId, role) {
if (!machine?.enabled) return false;
const roles = machine.roles || ['*'];
if (roles.includes('*')) return true;
return roles.includes(serviceId) || (role && roles.includes(role));
}
/**
* Pick the enabled machine with the most free capacity (fewest running / capacity).
* @param {object[]} machines
* @param {Array<{ machine?: string }>} instances
* @param {string} serviceId
* @param {string} [role]
*/
export function pickMachine(machines, instances, serviceId, role, exclude = []) {
const skip = new Set(exclude);
const eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id));
if (!eligible.length) return null;
const scored = eligible.map((m) => {
const running = instances.filter((i) => i.machine === m.id && i.pid).length;
const free = m.capacity - running;
const load = m.capacity <= 0 ? 1 : running / m.capacity;
return { m, running, free, load };
});
scored.sort((a, b) => a.load - b.load || a.running - b.running || a.m.id.localeCompare(b.m.id));
const best = scored.find((s) => s.free > 0);
return best ? best.m : null;
}
export function saveOverlay(overlayPath, machines) {
fs.mkdirSync(path.dirname(overlayPath), { recursive: true });
fs.writeFileSync(overlayPath, JSON.stringify({ machines }, null, 2));
}

38
src/monitor.js Normal file
View file

@ -0,0 +1,38 @@
/**
* Periodic probe + reconcile so replica floors (especially tree-node) stay met.
* @module monitor
*/
export class Monitor {
/**
* @param {import('./supervisor.js').Supervisor} supervisor
* @param {{ intervalMs?: number }} [opts]
*/
constructor(supervisor, opts = {}) {
this.supervisor = supervisor;
this.intervalMs = opts.intervalMs ?? supervisor.loaded.control?.probeIntervalMs ?? 750;
this.timer = null;
this.last = null;
}
async tick() {
const actions = await this.supervisor.reconcile();
this.last = { t: new Date().toISOString(), actions, status: this.supervisor.status() };
return this.last;
}
start() {
if (this.timer) return;
this.timer = setInterval(() => {
this.tick().catch((err) => {
this.supervisor.log('monitor-error', { error: err.message });
});
}, this.intervalMs);
this.timer.unref?.();
}
stop() {
if (this.timer) clearInterval(this.timer);
this.timer = null;
}
}

57
src/rtt.js Normal file
View file

@ -0,0 +1,57 @@
/**
* Ring buffer of message-processing RTTs (ms) for planning percentiles.
* @module rtt
*/
export class RttWindow {
/**
* @param {number} [cap=400]
*/
constructor(cap = 400) {
this.cap = cap;
/** @type {number[]} */
this.samples = [];
}
add(ms) {
const n = Number(ms);
if (!Number.isFinite(n) || n < 0) return;
this.samples.push(n);
if (this.samples.length > this.cap) this.samples.splice(0, this.samples.length - this.cap);
}
stats() {
return summarizeRtt(this.samples);
}
}
/**
* @param {number[]} samples
*/
export function summarizeRtt(samples) {
const xs = samples.filter((n) => Number.isFinite(n) && n >= 0).slice().sort((a, b) => a - b);
if (!xs.length) {
return { count: 0, minMs: null, avgMs: null, p50Ms: null, p90Ms: null };
}
const sum = xs.reduce((a, b) => a + b, 0);
return {
count: xs.length,
minMs: round1(xs[0]),
avgMs: round1(sum / xs.length),
p50Ms: round1(percentile(xs, 0.5)),
p90Ms: round1(percentile(xs, 0.9)),
};
}
function percentile(sorted, p) {
if (sorted.length === 1) return sorted[0];
const idx = (sorted.length - 1) * p;
const lo = Math.floor(idx);
const hi = Math.ceil(idx);
if (lo === hi) return sorted[lo];
return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
}
function round1(n) {
return Math.round(n * 10) / 10;
}

145
src/server.js Normal file
View file

@ -0,0 +1,145 @@
/**
* Operator HTTP: list, status, pause/resume/stop/start/restart.
* Binds 127.0.0.1 only.
* @module server
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { listServices } from './load.js';
import { ACTIONS, Simulator } from '../../verae-zapier-simulator/src/pipeline.js';
const PUBLIC = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'public');
function json(res, code, obj) {
res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' });
res.end(JSON.stringify(obj, null, 2));
}
/**
* @param {import('./supervisor.js').Supervisor} sup
* @param {import('./monitor.js').Monitor} [mon]
*/
export function startControlServer(sup, mon) {
const bind = sup.loaded.control?.bind || '127.0.0.1';
const port = Number(sup.loaded.control?.port || 3850);
const sim = new Simulator();
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${bind}:${port}`);
try {
if (req.method === 'GET' && url.pathname === '/health') {
return json(res, 200, { ok: true, role: 'operator-console' });
}
if (req.method === 'GET' && url.pathname === '/api/sim/actions') {
return json(res, 200, { actions: ACTIONS, state: sim.snapshot() });
}
if (req.method === 'POST' && url.pathname === '/api/sim/reset') {
sim.reset();
return json(res, 200, { ok: true, state: sim.snapshot() });
}
if (req.method === 'POST' && url.pathname === '/api/sim/run') {
const chunks = [];
for await (const c of req) chunks.push(c);
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
const result = await sim.run(body);
return json(res, 200, result);
}
if (req.method === 'GET' && url.pathname === '/api/services') {
return json(res, 200, { services: listServices(sup.loaded), fleetPath: sup.loaded.fleetPath });
}
if (req.method === 'GET' && url.pathname === '/api/status') {
return json(res, 200, { ...sup.status(), monitor: mon?.last || null });
}
if (req.method === 'GET' && url.pathname === '/api/machines') {
return json(res, 200, { machines: sup.status().machines });
}
if (req.method === 'POST' && url.pathname === '/api/machines') {
const chunks = [];
for await (const c of req) chunks.push(c);
const spec = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
const m = sup.addMachine(spec);
return json(res, 200, { machine: m, machines: sup.status().machines });
}
const mach = url.pathname.match(/^\/api\/machines\/([^/]+)\/(enable|disable|check)$/);
if (mach && req.method === 'POST') {
const [, id, op] = mach;
if (op === 'check') {
const check = await sup.checkMachine(id);
return json(res, check.ok ? 200 : 400, { check, machines: sup.status().machines });
}
const m = sup.setMachineEnabled(id, op === 'enable');
return json(res, 200, { machine: m, machines: sup.status().machines });
}
if (req.method === 'POST' && url.pathname === '/api/reconcile') {
const actions = await sup.reconcile();
return json(res, 200, { actions, status: sup.status() });
}
const svc = url.pathname.match(/^\/api\/services\/([^/]+)\/(start|stop|pause|resume)$/);
if (svc && req.method === 'POST') {
const [, id, op] = svc;
const fn = {
start: () => sup.startService(id),
stop: () => sup.stopService(id),
pause: () => sup.pauseService(id),
resume: () => sup.resumeService(id),
}[op];
const result = await fn();
return json(res, 200, { op, id, result, status: sup.status().services[id] });
}
const inst = url.pathname.match(/^\/api\/instances\/([^/]+)\/(pause|resume|restart|stop|unhealthy)$/);
if (inst && req.method === 'POST') {
const [, id, op] = inst;
const fn = {
pause: () => sup.pauseInstance(id),
resume: () => sup.resumeInstance(id),
restart: () => sup.restartInstance(id),
stop: () => sup.stopInstance(id, { replace: true }),
unhealthy: () => sup.markUnhealthy(id),
}[op];
const result = await fn();
return json(res, 200, { op, id, result: result?.id || result, status: sup.status() });
}
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
const dest = path.join(PUBLIC, 'index.html');
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
fs.createReadStream(dest).pipe(res);
return;
}
if (req.method === 'GET' && url.pathname.startsWith('/docs/')) {
const dest = path.normalize(path.join(PUBLIC, '..', url.pathname));
const root = path.normalize(path.join(PUBLIC, '..', 'docs'));
if (
!(dest === root || dest.startsWith(root + path.sep)) ||
!fs.existsSync(dest) ||
!fs.statSync(dest).isFile()
) {
return json(res, 404, { error: 'not found' });
}
const ext = path.extname(dest).toLowerCase();
const types = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.md': 'text/markdown; charset=utf-8',
'.html': 'text/html; charset=utf-8',
};
res.writeHead(200, { 'content-type': types[ext] || 'application/octet-stream' });
fs.createReadStream(dest).pipe(res);
return;
}
json(res, 404, { error: 'not found' });
} catch (err) {
json(res, 400, { error: err.message });
}
});
server.listen(port, bind, () => {
process.stdout.write(`fleet control http://${bind}:${port}/\n`);
});
return server;
}

170
src/ssh.js Normal file
View file

@ -0,0 +1,170 @@
/**
* SSH control of remote fleet workers. Uses username + host + identity file path.
* Never logs or stores private-key material.
* @module ssh
*/
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
export function expandHome(p) {
if (!p) return p;
if (p === '~') return os.homedir();
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
return p;
}
export function shellQuote(s) {
return `'${String(s).replace(/'/g, `'\"'\"'`)}'`;
}
export function sshTarget(machine) {
return `${machine.user || 'marchon'}@${machine.host}`;
}
export function sshBaseArgs(machine) {
const ident = expandHome(machine.identityFile || '');
const args = [
'-n',
'-o', 'BatchMode=yes',
'-o', 'StrictHostKeyChecking=accept-new',
'-o', 'IdentitiesOnly=yes',
'-o', 'ConnectTimeout=8',
'-p', String(machine.sshPort || 22),
];
if (ident) {
if (!fs.existsSync(ident)) throw new Error(`identityFile not found: ${ident}`);
args.push('-i', ident);
}
return args;
}
export function scpBaseArgs(machine) {
const ident = expandHome(machine.identityFile || '');
const args = [
'-o', 'BatchMode=yes',
'-o', 'StrictHostKeyChecking=accept-new',
'-o', 'IdentitiesOnly=yes',
'-o', 'ConnectTimeout=8',
'-P', String(machine.sshPort || 22),
];
if (ident) args.push('-i', ident);
return args;
}
export function sshExec(machine, remoteCommand, { timeoutMs = 20000 } = {}) {
return new Promise((resolve, reject) => {
const args = [...sshBaseArgs(machine), sshTarget(machine), remoteCommand];
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
const out = [];
const err = [];
child.stdout.on('data', (c) => out.push(c));
child.stderr.on('data', (c) => err.push(c));
const t = setTimeout(() => {
child.kill('SIGKILL');
reject(new Error(`ssh timeout: ${machine.id || machine.host}`));
}, timeoutMs);
child.on('error', (e) => {
clearTimeout(t);
reject(e);
});
child.on('close', (code) => {
clearTimeout(t);
const stdout = Buffer.concat(out).toString('utf8').trim();
const stderr = Buffer.concat(err).toString('utf8').trim();
if (code !== 0) {
reject(new Error(stderr || stdout || `ssh exit ${code}`));
return;
}
resolve({ stdout, stderr, code });
});
});
}
export function scpTo(machine, localFiles, remoteDir) {
return new Promise((resolve, reject) => {
const dest = `${sshTarget(machine)}:${remoteDir}/`;
const args = [...scpBaseArgs(machine), ...localFiles, dest];
const child = spawn('scp', args, { stdio: ['ignore', 'pipe', 'pipe'] });
const err = [];
child.stderr.on('data', (c) => err.push(c));
const t = setTimeout(() => {
child.kill('SIGKILL');
reject(new Error('scp timeout'));
}, 30000);
child.on('close', (code) => {
clearTimeout(t);
if (code !== 0) reject(new Error(Buffer.concat(err).toString('utf8') || `scp exit ${code}`));
else resolve();
});
child.on('error', (e) => {
clearTimeout(t);
reject(e);
});
});
}
const synced = new Set();
export async function ensureRemoteRuntime(machine) {
const remoteDir = machine.remoteDir || '~/verae-fleet-runtime';
const key = `${machine.id}:${remoteDir}`;
if (synced.has(key)) return remoteDir;
await sshExec(machine, `mkdir -p ${remoteDir}/src ${remoteDir}/data`);
await scpTo(machine, [path.join(HERE, 'worker.js'), path.join(HERE, 'rtt.js')], `${remoteDir}/src`);
synced.add(key);
return remoteDir;
}
export async function sshSpawnWorker(machine, { instance, env }) {
const remoteDir = await ensureRemoteRuntime(machine);
const fleetEnv = Object.entries(env)
.filter(([k]) => k.startsWith('FLEET_'))
.map(([k, v]) => `${k}=${shellQuote(v)}`)
.join(' ');
const inner = `node src/worker.js </dev/null >data/${instance}/worker.log 2>&1 & echo $!`;
const cmd = `mkdir -p ${remoteDir}/data/${instance} && cd ${remoteDir} && ${fleetEnv} bash -c ${shellQuote(inner)}`;
const { stdout } = await sshExec(machine, cmd);
const pid = Number(String(stdout).split('\n').pop());
if (!Number.isFinite(pid)) throw new Error(`remote spawn produced no pid: ${stdout}`);
return { pid, remoteDir };
}
export async function sshKillWorker(machine, pid, instance) {
const cmd = pid
? `kill -TERM ${Number(pid)} 2>/dev/null || true`
: `pkill -f ${shellQuote(`FLEET_INSTANCE=${instance}`)} 2>/dev/null || true`;
await sshExec(machine, cmd);
}
export async function sshHttp(machine, method, urlPath, { port, body } = {}) {
const url = `http://127.0.0.1:${port}${urlPath}`;
let cmd = `curl -sS -m 3 -X ${method}`;
if (body) {
cmd += ` -H 'content-type: application/json' -d ${shellQuote(JSON.stringify(body))}`;
}
cmd += ` ${shellQuote(url)}`;
const { stdout } = await sshExec(machine, cmd, { timeoutMs: 8000 });
try {
return JSON.parse(stdout || '{}');
} catch {
return { raw: stdout };
}
}
export async function sshCheck(machine) {
const { stdout } = await sshExec(machine, 'echo OK; whoami; hostname; command -v node; node -v');
const lines = stdout.split('\n');
return {
ok: lines[0] === 'OK',
user: lines[1],
hostname: lines[2],
node: lines[3] ? `${lines[3]} ${lines[4] || ''}`.trim() : null,
identityFile: expandHome(machine.identityFile || ''),
};
}

510
src/supervisor.js Normal file
View file

@ -0,0 +1,510 @@
/**
* Spawn, pause, resume, stop, restart replicas. Keep replica floor (tree-node min).
* @module supervisor
*/
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadFleet } from './load.js';
import { httpProbe, postControl } from './health.js';
import { classifyService } from './classify.js';
import { pickMachine, upsertMachine, saveOverlay } from './machines.js';
import { summarizeRtt } from './rtt.js';
import { sshSpawnWorker, sshKillWorker, sshHttp, sshCheck } from './ssh.js';
const WORKER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'worker.js');
export class Supervisor {
/**
* @param {object} [opts]
* @param {ReturnType<typeof loadFleet>} [opts.loaded]
* @param {string} [opts.stateRoot]
*/
constructor(opts = {}) {
this.loaded = opts.loaded || loadFleet();
this.stateRoot = opts.stateRoot || path.join(this.loaded.root, 'data', 'runtime');
fs.mkdirSync(this.stateRoot, { recursive: true });
/** @type {Map<string, object>} */
this.instances = new Map();
this.events = [];
this._seq = 0;
if (!Array.isArray(this.loaded.machines) || !this.loaded.machines.length) {
this.loaded.machines = [
{ id: 'local', title: 'local', kind: 'local', host: '127.0.0.1', enabled: true, capacity: 32, roles: ['*'] },
];
}
}
machines() {
return this.loaded.machines;
}
addMachine(spec) {
const m = upsertMachine(this.loaded.machines, spec);
if (this.loaded.machinesOverlay) saveOverlay(this.loaded.machinesOverlay, this.loaded.machines);
this.log('machine-add', { id: m.id, host: m.host, kind: m.kind });
return m;
}
setMachineEnabled(id, enabled) {
const m = this.loaded.machines.find((x) => x.id === id);
if (!m) throw new Error(`unknown machine ${id}`);
m.enabled = Boolean(enabled);
if (this.loaded.machinesOverlay) saveOverlay(this.loaded.machinesOverlay, this.loaded.machines);
return m;
}
async checkMachine(id) {
const m = this.machines().find((x) => x.id === id);
if (!m) throw new Error(`unknown machine ${id}`);
if (m.kind === 'ssh') return sshCheck(m);
if (m.kind === 'agent') {
const r = await httpProbe({ host: m.host, port: m.agentPort, path: '/health', timeoutMs: 2000 });
return { ok: r.ok, kind: 'agent', body: r.body };
}
return { ok: true, kind: 'local', host: m.host };
}
machineOf(rec) {
return this.machines().find((x) => x.id === rec.machine);
}
async controlInstance(rec, pathname, body) {
if (rec.kind === 'ssh') {
const m = this.machineOf(rec);
if (!m) throw new Error(`machine ${rec.machine} missing`);
return sshHttp(m, 'POST', pathname, { port: rec.healthPort, body });
}
return postControl({
host: rec.host || '127.0.0.1',
port: rec.healthPort,
path: pathname,
body,
});
}
log(type, detail) {
const ev = { t: new Date().toISOString(), type, ...detail };
this.events.push(ev);
if (this.events.length > 500) this.events.splice(0, this.events.length - 400);
return ev;
}
spec(serviceId) {
const s = this.loaded.services[serviceId];
if (!s) throw new Error(`unknown service ${serviceId}`);
return s;
}
nextIndex(serviceId) {
const used = new Set(
[...this.instances.values()].filter((i) => i.service === serviceId).map((i) => i.index),
);
const max = this.spec(serviceId).max ?? 8;
for (let i = 0; i < max; i += 1) if (!used.has(i)) return i;
return null;
}
/**
* @param {string} serviceId
* @param {{ index?: number }} [opts]
*/
async startOne(serviceId, opts = {}) {
const spec = this.spec(serviceId);
if (!spec.managed) {
this.log('skip', { service: serviceId, reason: 'unmanaged' });
return null;
}
if (!spec.enabled) {
this.log('skip', { service: serviceId, reason: 'disabled' });
return null;
}
const index = opts.index ?? this.nextIndex(serviceId);
if (index == null) {
this.log('skip', { service: serviceId, reason: 'at-max' });
return null;
}
const instance = `${serviceId}-${index}`;
if (this.instances.has(instance)) return this.instances.get(instance);
const machine =
opts.machine ||
pickMachine(
this.machines(),
[...this.instances.values()],
serviceId,
spec.role || serviceId,
opts.excludeMachines || [],
);
if (!machine) {
this.log('skip', { service: serviceId, reason: 'no-machine-capacity' });
return null;
}
const healthPort = (spec.ports?.healthBase || 19000) + index;
const stateDir = path.join(this.stateRoot, instance);
fs.mkdirSync(stateDir, { recursive: true });
const env = {
...process.env,
FLEET_ROLE: spec.role || serviceId,
FLEET_SERVICE: serviceId,
FLEET_INSTANCE: instance,
FLEET_MACHINE: machine.id,
FLEET_HEALTH_PORT: String(healthPort),
FLEET_HEALTH_BIND: machine.kind === 'local' ? '127.0.0.1' : '127.0.0.1',
FLEET_STATE_DIR: machine.kind === 'ssh' ? `${machine.remoteDir || '~/verae-fleet-runtime'}/data/${instance}` : stateDir,
FLEET_MESSAGE_HZ: process.env.FLEET_MESSAGE_HZ || '8',
};
let child = null;
let pid = null;
if (machine.kind === 'ssh') {
try {
const spawned = await sshSpawnWorker(machine, { instance, env });
pid = spawned.pid;
} catch (err) {
this.log('skip', { service: serviceId, reason: 'ssh-spawn-failed', machine: machine.id, error: err.message });
const exclude = [...(opts.excludeMachines || []), machine.id];
if (exclude.length < this.machines().length) {
return this.startOne(serviceId, { ...opts, excludeMachines: exclude });
}
return null;
}
} else if (machine.kind === 'agent') {
try {
const spawned = await postControl({
host: machine.host,
port: machine.agentPort,
path: '/spawn',
body: { env, instance },
timeoutMs: 4000,
});
pid = spawned.pid || null;
} catch (err) {
this.log('skip', { service: serviceId, reason: 'agent-spawn-failed', machine: machine.id, error: err.message });
return null;
}
} else {
child = spawn(process.execPath, [WORKER], { env, stdio: ['ignore', 'pipe', 'pipe'] });
pid = child.pid;
}
const rec = {
id: instance,
service: serviceId,
index,
machine: machine.id,
host: machine.host,
pid,
healthPort,
stateDir,
child,
kind: machine.kind,
_machine: machine,
paused: false,
startedAt: Date.now(),
restarts: 0,
lastError: null,
rttSamples: [],
};
if (child) {
child.stderr?.on('data', () => {});
child.on('exit', (code, signal) => {
rec.exitCode = code;
rec.signal = signal;
rec.pid = null;
this.log('exit', { instance, service: serviceId, code, signal });
});
}
this.instances.set(instance, rec);
this.log('start', { instance, service: serviceId, machine: machine.id, pid: rec.pid, healthPort });
const h = await waitForHealth(rec, 8000);
rec.healthy = h.ok;
rec.lastProbe = h;
return rec;
}
async stopInstance(instanceId, { replace = true } = {}) {
const rec = this.instances.get(instanceId);
if (!rec) return null;
rec.stopping = true;
if (rec.kind === 'ssh') {
const m = this.machines().find((x) => x.id === rec.machine);
if (m) {
try {
await sshKillWorker(m, rec.pid, instanceId);
} catch {
/* ignore */
}
}
} else if (rec.kind === 'agent') {
const m = this.machines().find((x) => x.id === rec.machine);
if (m) {
try {
await postControl({ host: m.host, port: m.agentPort, path: '/kill', body: { instance: instanceId } });
} catch {
/* ignore */
}
}
} else if (rec.child && rec.pid) {
rec.child.kill('SIGTERM');
await waitExit(rec.child, 2000);
}
this.instances.delete(instanceId);
this.log('stop', { instance: instanceId, service: rec.service });
if (replace) await this.reconcile(rec.service);
return rec;
}
async pauseInstance(instanceId, { replace = true } = {}) {
const rec = this.instances.get(instanceId);
if (!rec) throw new Error(`no instance ${instanceId}`);
await this.controlInstance(rec, '/pause');
rec.paused = true;
this.log('pause', { instance: instanceId, service: rec.service });
if (replace) await this.reconcile(rec.service);
return rec;
}
async resumeInstance(instanceId) {
const rec = this.instances.get(instanceId);
if (!rec) throw new Error(`no instance ${instanceId}`);
await this.controlInstance(rec, '/resume');
rec.paused = false;
this.log('resume', { instance: instanceId, service: rec.service });
return rec;
}
async restartInstance(instanceId) {
const rec = this.instances.get(instanceId);
if (!rec) throw new Error(`no instance ${instanceId}`);
const { service, index, machine } = rec;
await this.stopInstance(instanceId, { replace: false });
const next = await this.startOne(service, { index, machine: this.machines().find((m) => m.id === machine) });
if (next) next.restarts = (rec.restarts || 0) + 1;
this.log('restart', { instance: instanceId, service });
return next;
}
async markUnhealthy(instanceId) {
const rec = this.instances.get(instanceId);
if (!rec) throw new Error(`no instance ${instanceId}`);
await this.controlInstance(rec, '/unhealthy');
return rec;
}
async probe(rec) {
const spec = this.spec(rec.service);
const pathName = spec.health?.path || '/health';
let r;
if (rec.kind === 'ssh') {
const m = this.machineOf(rec);
try {
const body = await sshHttp(m, 'GET', pathName, { port: rec.healthPort });
r = { ok: body.ok !== false, paused: Boolean(body.paused), body, statusCode: body.ok === false ? 503 : 200 };
} catch (err) {
r = { ok: false, paused: false, error: err.message };
}
} else {
r = await httpProbe({
host: rec.host || '127.0.0.1',
port: rec.healthPort,
path: pathName,
timeoutMs: spec.health?.timeoutMs || 800,
});
}
rec.paused = Boolean(r.paused);
rec.lastProbe = r;
rec.healthy = r.ok;
if (r.ok && !rec.paused) {
try {
const msg = await this.controlInstance(rec, '/message');
if (Number.isFinite(msg.rttMs)) {
rec.rttSamples = rec.rttSamples || [];
rec.rttSamples.push(msg.rttMs);
if (rec.rttSamples.length > 200) rec.rttSamples.splice(0, rec.rttSamples.length - 200);
}
rec.rtt = msg.rtt || r.body?.rtt || summarizeRtt(rec.rttSamples);
} catch {
rec.rtt = r.body?.rtt || summarizeRtt(rec.rttSamples || []);
}
}
return r;
}
available(serviceId) {
return [...this.instances.values()].filter(
(i) => i.service === serviceId && i.pid && !i.paused && i.healthy !== false,
);
}
/**
* Ensure running healthy unpaused copies >= min when keepFloor.
* @param {string} [only]
*/
async reconcile(only) {
const actions = [];
const ids = only ? [only] : Object.keys(this.loaded.services);
for (const serviceId of ids) {
const spec = this.loaded.services[serviceId];
if (!spec?.managed || !spec.enabled) continue;
for (const rec of [...this.instances.values()].filter((i) => i.service === serviceId)) {
if (!rec.pid && !rec.stopping) {
this.instances.delete(rec.id);
continue;
}
if (rec.pid) await this.probe(rec);
if (rec.pid && rec.healthy === false && !rec.paused) {
this.log('unhealthy', { instance: rec.id, service: serviceId });
await this.restartInstance(rec.id);
actions.push({ op: 'restart', instance: rec.id });
}
}
const avail = this.available(serviceId).length;
const want = spec.keepFloor ? spec.min : Math.min(spec.min, spec.max);
if (avail < want) {
const need = want - avail;
for (let n = 0; n < need; n += 1) {
const started = await this.startOne(serviceId);
if (started) actions.push({ op: 'start', instance: started.id });
}
}
}
return actions;
}
async startService(serviceId) {
const spec = this.spec(serviceId);
spec.enabled = true;
const have = [...this.instances.values()].filter((i) => i.service === serviceId && i.pid).length;
const want = spec.min || 1;
const started = [];
for (let i = have; i < want; i += 1) {
const rec = await this.startOne(serviceId);
if (rec) started.push(rec.id);
}
return started;
}
async stopService(serviceId, { holdFloor = false } = {}) {
const spec = this.spec(serviceId);
if (!holdFloor) spec.enabled = false;
const ids = [...this.instances.values()].filter((i) => i.service === serviceId).map((i) => i.id);
for (const id of ids) await this.stopInstance(id, { replace: false });
if (holdFloor) await this.reconcile(serviceId);
return ids;
}
async pauseService(serviceId) {
const ids = [...this.instances.values()].filter((i) => i.service === serviceId && i.pid).map((i) => i.id);
for (const id of ids) await this.pauseInstance(id, { replace: false });
await this.reconcile(serviceId);
return ids;
}
async resumeService(serviceId) {
const ids = [...this.instances.values()].filter((i) => i.service === serviceId).map((i) => i.id);
for (const id of ids) await this.resumeInstance(id);
return ids;
}
async stopAll() {
for (const id of [...this.instances.keys()]) {
await this.stopInstance(id, { replace: false });
}
}
status() {
const services = {};
for (const spec of Object.values(this.loaded.services)) {
const inst = [...this.instances.values()].filter((i) => i.service === spec.id);
const available = inst.filter((i) => i.pid && !i.paused && i.healthy !== false).length;
services[spec.id] = {
id: spec.id,
title: spec.title,
configPath: spec.configPath,
managed: spec.managed,
enabled: spec.enabled,
min: spec.min,
max: spec.max,
keepFloor: spec.keepFloor,
running: inst.filter((i) => i.pid).length,
paused: inst.filter((i) => i.paused).length,
available,
belowFloor: spec.keepFloor && spec.enabled && spec.managed && available < spec.min,
instances: inst.map((i) => ({
id: i.id,
pid: i.pid,
machine: i.machine || 'local',
host: i.host || '127.0.0.1',
healthPort: i.healthPort,
paused: i.paused,
healthy: i.healthy,
restarts: i.restarts,
rtt: i.rtt || summarizeRtt(i.rttSamples || []),
})),
};
const allSamples = inst.flatMap((i) => i.rttSamples || []);
services[spec.id].rtt = summarizeRtt(allSamples);
services[spec.id].health = classifyService(services[spec.id]);
}
const machines = this.machines().map((m) => {
const inst = [...this.instances.values()].filter((i) => i.machine === m.id);
const samples = inst.flatMap((i) => i.rttSamples || []);
return {
...m,
running: inst.filter((i) => i.pid).length,
available: inst.filter((i) => i.pid && !i.paused && i.healthy !== false).length,
rtt: summarizeRtt(samples),
};
});
return {
control: this.loaded.control,
nats: this.loaded.nats,
machines,
services,
events: this.events.slice(-50),
};
}
}
function waitExit(child, ms) {
return new Promise((resolve) => {
if (!child || child.exitCode != null) return resolve();
const t = setTimeout(() => {
try {
child.kill('SIGKILL');
} catch {
/* ignore */
}
resolve();
}, ms);
child.once('exit', () => {
clearTimeout(t);
resolve();
});
});
}
async function waitForHealth(rec, ms) {
const deadline = Date.now() + ms;
while (Date.now() < deadline) {
let r;
if (rec.kind === 'ssh' && rec._machine) {
try {
const body = await sshHttp(rec._machine, 'GET', '/health', { port: rec.healthPort });
r = { ok: body.ok !== false, statusCode: 200, paused: Boolean(body.paused), body };
} catch {
r = { ok: false };
}
} else {
r = await httpProbe({
host: rec.host || '127.0.0.1',
port: rec.healthPort,
path: '/health',
timeoutMs: 400,
});
}
if (r.ok || r.statusCode === 503) return r;
await new Promise((res) => setTimeout(res, 80));
}
return { ok: false, error: 'start-timeout' };
}

153
src/worker.js Normal file
View file

@ -0,0 +1,153 @@
#!/usr/bin/env node
/**
* Fleet-managed replica: health HTTP + pause + heartbeat.
* Roles tree-node / archive-worm hold an in-process WormArchive.
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { createHash, randomBytes } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { RttWindow } from './rtt.js';
const role = process.env.FLEET_ROLE || 'unknown';
const service = process.env.FLEET_SERVICE || role;
const instance = process.env.FLEET_INSTANCE || `${service}-0`;
const machine = process.env.FLEET_MACHINE || 'local';
const port = Number(process.env.FLEET_HEALTH_PORT || 0);
const bind = process.env.FLEET_HEALTH_BIND || '127.0.0.1';
const stateDir = process.env.FLEET_STATE_DIR || path.join(process.cwd(), 'data', instance);
const startedAt = new Date().toISOString();
const rtt = new RttWindow(400);
const hz = Number(process.env.FLEET_MESSAGE_HZ || 6);
fs.mkdirSync(stateDir, { recursive: true });
let paused = false;
let forceUnhealthy = false;
let archive = null;
let puts = 0;
if (role === 'tree-node' || role === 'archive-worm') {
try {
const wormPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../../verae-archive-worm/src/archive.js',
);
const { WormArchive } = await import(wormPath);
archive = new WormArchive(instance);
} catch {
archive = {
archiveId: instance,
put() {
puts += 1;
},
query() {
return null;
},
};
}
}
function processMessage() {
const t0 = process.hrtime.bigint();
if (paused) return null;
const payload = randomBytes(32);
if (archive) {
const sha = createHash('sha256').update(payload).digest('hex');
archive.put({ sha256: sha, kind: role === 'tree-node' ? 'tree' : 'publicMeta', record: { n: puts } });
archive.query(sha);
puts += 1;
} else {
createHash('sha256').update(payload).digest('hex');
puts += 1;
}
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
rtt.add(ms);
return ms;
}
function snapshot() {
return {
ok: !paused && !forceUnhealthy,
paused,
forceUnhealthy,
role,
service,
instance,
machine,
port,
pid: process.pid,
startedAt,
puts,
archiveId: archive?.archiveId || null,
rtt: rtt.stats(),
};
}
function beat() {
const body = { ...snapshot(), ts: Date.now() };
fs.writeFileSync(path.join(stateDir, 'heartbeat.json'), JSON.stringify(body));
}
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1');
const json = (code, obj) => {
res.writeHead(code, { 'content-type': 'application/json' });
res.end(JSON.stringify(obj));
};
if (url.pathname === '/health' && req.method === 'GET') {
const snap = snapshot();
return json(snap.ok ? 200 : 503, snap);
}
if (url.pathname === '/info' && req.method === 'GET') return json(200, snapshot());
if (url.pathname === '/pause' && req.method === 'POST') {
paused = true;
beat();
return json(200, snapshot());
}
if (url.pathname === '/resume' && req.method === 'POST') {
paused = false;
forceUnhealthy = false;
beat();
return json(200, snapshot());
}
if (url.pathname === '/unhealthy' && req.method === 'POST') {
forceUnhealthy = true;
beat();
return json(200, snapshot());
}
if (url.pathname === '/message' && req.method === 'POST') {
const ms = processMessage();
return json(paused ? 503 : 200, { rttMs: ms, ...snapshot() });
}
if (url.pathname === '/metrics' && req.method === 'GET') {
return json(200, { rtt: rtt.stats(), samples: rtt.samples.slice(-80) });
}
json(404, { error: 'not found' });
});
server.listen(port, bind, () => {
beat();
process.stdout.write(`fleet-worker ${instance}@${machine} health http://${bind}:${port}/health\n`);
});
const iv = setInterval(beat, 400);
const msgIv =
hz > 0
? setInterval(() => {
if (!paused && !forceUnhealthy) processMessage();
}, Math.max(50, Math.round(1000 / hz)))
: null;
function shutdown() {
clearInterval(iv);
if (msgIv) clearInterval(msgIv);
try {
server.close();
} catch {
/* ignore */
}
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);