Fix flapping online/offline status for powered-off hosts
All checks were successful
Deploy GSM / deploy (push) Successful in 25s
All checks were successful
Deploy GSM / deploy (push) Successful in 25s
Cached SSH connections had no keepalive, so when a VM/host is powered off the socket stays 'connected' (~2h OS TCP timeout) and execCommand hangs with no timeout, stalling status aggregation and leaving the dashboard on a stale 'online'. Add keepaliveInterval/keepaliveCountMax to ssh.connect (dead peers detected in ~15s). Wrap getServerStatus/getProcessUptime in an 8s timeout that drops the dead cached connection and trips the failed-host breaker, so the fast 'unreachable' path kicks in. Affects all servers (V Rising, Palworld, etc.). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,18 @@ const sshConnections = new Map();
|
||||
const failedHosts = new Map(); // Cache failed connections
|
||||
const FAILED_HOST_TTL = 60000; // 60 seconds before retry
|
||||
const SSH_TIMEOUT = 5000;
|
||||
const STATUS_TIMEOUT = 8000; // max time for a status/uptime check before treating the host as unreachable
|
||||
|
||||
// Race a promise against a timeout. Rejects with `label` if it doesn't settle in time.
|
||||
// Used so a hung SSH command (e.g. host powered off mid-session, socket not yet dead)
|
||||
// can't stall the whole status aggregation.
|
||||
function withTimeout(promise, ms, label = "operation-timed-out") {
|
||||
let timer;
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(label)), ms);
|
||||
});
|
||||
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
return JSON.parse(readFileSync(join(__dirname, "..", "config.json"), "utf-8"));
|
||||
@@ -52,7 +64,12 @@ async function getConnection(host, username = "root") {
|
||||
host,
|
||||
username,
|
||||
privateKeyPath: "/root/.ssh/id_ed25519",
|
||||
readyTimeout: SSH_TIMEOUT
|
||||
readyTimeout: SSH_TIMEOUT,
|
||||
// Detect dead peers fast: if a host is powered off, the TCP socket would
|
||||
// otherwise stay "connected" for ~2h. Keepalive closes it in ~15s so the
|
||||
// cached connection is invalidated and the next check fails fast -> offline.
|
||||
keepaliveInterval: 5000,
|
||||
keepaliveCountMax: 3
|
||||
});
|
||||
clearHostFailed(host, username);
|
||||
sshConnections.set(key, ssh);
|
||||
@@ -65,7 +82,8 @@ async function getConnection(host, username = "root") {
|
||||
|
||||
// Returns: "online", "starting", "stopping", "offline"
|
||||
export async function getServerStatus(server) {
|
||||
try {
|
||||
const connKey = (server.sshUser || "root") + "@" + server.host;
|
||||
const check = (async () => {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === 'docker') {
|
||||
@@ -113,8 +131,17 @@ export async function getServerStatus(server) {
|
||||
}
|
||||
return 'offline';
|
||||
}
|
||||
})();
|
||||
check.catch(() => {}); // prevent unhandled rejection if the timeout wins the race
|
||||
try {
|
||||
return await withTimeout(check, STATUS_TIMEOUT, "status-timeout");
|
||||
} catch (err) {
|
||||
console.error(`Failed to get status for ${server.id}:`, err.message);
|
||||
if (err.message === "status-timeout") {
|
||||
// Connection hung (host likely powered off mid-session) -> drop it and trip the breaker
|
||||
sshConnections.delete(connKey);
|
||||
markHostFailed(server.host, server.sshUser);
|
||||
}
|
||||
return 'offline';
|
||||
}
|
||||
}
|
||||
@@ -257,7 +284,8 @@ export async function getConsoleLog(server, lines = 50) {
|
||||
}
|
||||
|
||||
export async function getProcessUptime(server) {
|
||||
try {
|
||||
const connKey = (server.sshUser || "root") + "@" + server.host;
|
||||
const check = (async () => {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === "docker") {
|
||||
@@ -296,8 +324,16 @@ export async function getProcessUptime(server) {
|
||||
if (!isNaN(uptime)) return uptime;
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
check.catch(() => {}); // prevent unhandled rejection if the timeout wins the race
|
||||
try {
|
||||
return await withTimeout(check, STATUS_TIMEOUT, "uptime-timeout");
|
||||
} catch (err) {
|
||||
console.error(`Failed to get uptime for ${server.id}:`, err.message);
|
||||
if (err.message === "uptime-timeout") {
|
||||
sshConnections.delete(connKey);
|
||||
markHostFailed(server.host, server.sshUser);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user