From 7c1139d5b60f4d5af38c651533f7cec14386fa8e Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:41:07 -0400 Subject: [PATCH] Fix: stop a stalled socket proxy from delaying health checks --- api/src/routes/health.js | 19 +++++++++++++------ api/test/health-route.test.js | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/api/src/routes/health.js b/api/src/routes/health.js index 368f77ec..d1f4050f 100644 --- a/api/src/routes/health.js +++ b/api/src/routes/health.js @@ -15,10 +15,12 @@ const { } = require('../proxy'); const { PING_MS, BATCH_MS } = require('../timeouts'); const backoff = require('../poll-backoff'); +const { latest } = require('../latest'); const { IS_DEMO } = require('../demo'); const demoData = require('../demo-data'); const log = require('../log'); const SOCKET_PROXY_URL_DEFAULT = process.env.SOCKET_PROXY_URL || ''; +const CONTAINERS_KEEP_MS = 2 * 60 * 1000; function socketProxyUrl(cfg) { return cfg.settings?.server?.socketProxyUrl || SOCKET_PROXY_URL_DEFAULT; @@ -150,7 +152,12 @@ on('GET', '/api/health', async (req, res) => { const socketUrl = socketProxyUrl(cfg); /* Without a proxy address every container lookup misses, which would report every container-backed app as unhealthy. */ - const containers = socketUrl ? await fetchContainerHealth(socketUrl) : Object.create(null); + const containersReady = socketUrl + ? latest(`containers:${socketUrl}`, () => fetchContainerHealth(socketUrl), { + holdMs: BATCH_MS, + keepMs: CONTAINERS_KEEP_MS, + }).then(o => (o?.ok ? o.value : Object.create(null))) + : Promise.resolve(Object.create(null)); const result = Object.create(null); await Promise.allSettled( cfg.items @@ -163,7 +170,11 @@ on('GET', '/api/health', async (req, res) => { container and a ping, and the tile needs the detail from each. */ let unhealthy = false; const detail = {}; + const key = `ping:${item.id}`; + const skipped = ping ? backoff.skip(key) : false; + const pinged = ping && !skipped ? pingUnchecked(ping, BATCH_MS, item.skipTlsVerify === true) : null; if (cName) { + const containers = await containersReady; const norm = cName.toLowerCase().replace(/[\s_]+/g, '-'); const c = containers[cName] || containers[norm]; unhealthy = !c || c.unhealthy; @@ -171,13 +182,9 @@ on('GET', '/api/health', async (req, res) => { detail.status = c?.status || ''; } if (ping) { - const key = `ping:${item.id}`; /* status 0 is a host that never answered. A status of any kind means the address is reachable and stays on the normal cycle. */ - const skipped = backoff.skip(key); - const r = skipped - ? backoff.remembered(key) - : await pingUnchecked(ping, BATCH_MS, item.skipTlsVerify === true); + const r = skipped ? backoff.remembered(key) : await pinged; if (!r.ok) unhealthy = true; if (!skipped) { if (r.ok) backoff.success(key); diff --git a/api/test/health-route.test.js b/api/test/health-route.test.js index aba2b8e7..375ae70f 100644 --- a/api/test/health-route.test.js +++ b/api/test/health-route.test.js @@ -174,3 +174,19 @@ test('a ping check still runs while no socket proxy is configured', async () => assert.equal(r.pingStatus, 200); assert.equal(r.state, undefined); }); + +test('a stalled socket proxy delays the health reply by at most the batch timeout', async t => { + const { BATCH_MS } = require('../src/timeouts'); + const stalled = http.createServer(() => {}); + const stalledBase = await listen(stalled); + t.after(() => close(stalled)); + targetStatus = 200; + configure({ container: 'myapp', ping: `${targetBase}/` }, stalledBase); + + const t0 = Date.now(); + const r = (await health()).a1; + const took = Date.now() - t0; + assert.ok(took < BATCH_MS + 1000, `${took} ms`); + assert.equal(r.pingStatus, 200, 'the ping ran while the proxy stalled'); + assert.equal(r.state, 'unknown'); +});