Spread fleet replicas across extra machines and show message RTT percentiles
Some checks are pending
offline / test (push) Waiting to run

machines.json plus Add-machine UI place copies on the least-loaded host.
Monitor samples processing RTT and displays min, avg, p50, and p90 for
capacity planning. Remote hosts run src/agent.js.
This commit is contained in:
George Lambert 2026-09-11 13:25:05 -04:00
parent 6fbc808bb1
commit b325f6f697
17 changed files with 673 additions and 53 deletions

View file

@ -0,0 +1,92 @@
/**
* 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);
}
return list.map(normalizeMachine);
}
export function normalizeMachine(m) {
return {
id: String(m.id || '').trim(),
title: m.title || m.id,
kind: m.kind === 'agent' ? 'agent' : 'local',
host: m.host || '127.0.0.1',
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) {
const eligible = machines.filter((m) => canHost(m, serviceId, role));
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));
}