mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
build: probe container health with Node's fetch instead of wget
The probe now needs nothing but the node binary the image already runs, which frees the base image from carrying wget. The image HEALTHCHECK and the compose healthcheck use the same one-liner: a non-2xx answer, a refused connection or the 4 s abort exit 1, inside Docker's 5 s timeout. The boot-storm guard reads the abort from the fetch and refuses a probe that names wget or curl.
This commit is contained in:
1 parent
540f4ff84f
commit
65e35761ec
4 files changed
+29
-13
No files matched your search
+5
-2
@@ -369,9 +369,12 @@ ENV HOSTNAME="0.0.0.0"
|
||||
# migration waiting out a held lock retries for up to about eight minutes.
|
||||
# Five retries: a pool saturated for a minute during a boot storm makes the
|
||||
# probe's 2 s database check miss once or twice without marking the container
|
||||
# unhealthy (docker-compose.yml explains the numbers).
|
||||
# unhealthy (docker-compose.yml explains the numbers). The probe is Node's own
|
||||
# fetch: the Debian base carries neither wget nor curl, and installing one for
|
||||
# this line would add a TLS stack to the image's scan surface. A non-2xx
|
||||
# answer, a refused connection and the 4 s abort all exit 1.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10m --retries=5 \
|
||||
CMD wget --no-verbose --tries=1 --timeout=4 --spider http://127.0.0.1:3000/api/health || exit 1
|
||||
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/api/health',{signal:AbortSignal.timeout(4000)}).then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "server.js"]
|
||||
+5
-3
@@ -557,8 +557,10 @@ services:
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget --no-verbose --tries=1 --timeout=4 --spider http://127.0.0.1:3000/api/health || exit 1",
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://127.0.0.1:3000/api/health',{signal:AbortSignal.timeout(4000)}).then(r=>process.exit(r.ok?0:1),()=>process.exit(1))",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
@@ -567,7 +569,7 @@ services:
|
||||
# past that, so a boot storm that saturates the pool for a minute after
|
||||
# the start period (backfills, a restore, another tenant's import) costs
|
||||
# a probe or two, not the container's health and the proxy's routing.
|
||||
# wget's own 4 s timeout ends the request inside Docker's 5 s, so a
|
||||
# The probe's own 4 s abort ends the request inside Docker's 5 s, so a
|
||||
# stalled connection reads as one clean miss rather than a killed probe.
|
||||
# A database that stays away for 2.5 minutes is a real outage and shows.
|
||||
retries: 5
|
||||
|
||||
@@ -131,7 +131,7 @@ and restart the worker together with the web containers.
|
||||
|
||||
## Healthchecks
|
||||
|
||||
- `app` (web) healthcheck: `wget /api/health` every 30s.
|
||||
- `app` (web) healthcheck: a `node -e` fetch of `/api/health` every 30s.
|
||||
- `app-worker` does not expose a port; its liveness is tracked
|
||||
in-memory inside the process (`src/lib/jobs/worker-status.ts`) and
|
||||
read back by `/api/admin/status`. In a web/worker split this means
|
||||
|
||||
@@ -79,10 +79,21 @@ function imageCheck(): Check {
|
||||
};
|
||||
}
|
||||
|
||||
function wgetTimeout(command: string): number {
|
||||
const m = /wget[^|]*--timeout=(\d+)/.exec(command);
|
||||
if (!m) throw new Error(`no wget timeout in: ${command}`);
|
||||
return Number(m[1]);
|
||||
/**
|
||||
* The probe is Node's fetch, aborted by `AbortSignal.timeout(ms)`. The
|
||||
* image carries no wget or curl, so a probe that names one would fail every
|
||||
* time and mark a healthy container unhealthy.
|
||||
*/
|
||||
function probeTimeout(command: string): number {
|
||||
expect(command, "the probe needs a binary the image has").not.toMatch(
|
||||
/\b(wget|curl)\b/,
|
||||
);
|
||||
const m =
|
||||
/fetch\('http:\/\/127\.0\.0\.1:3000\/api\/health',\{signal:AbortSignal\.timeout\((\d+)\)\}\)/.exec(
|
||||
command,
|
||||
);
|
||||
if (!m) throw new Error(`no aborting /api/health fetch in: ${command}`);
|
||||
return Number(m[1]) / 1000;
|
||||
}
|
||||
|
||||
describe.each([
|
||||
@@ -96,9 +107,9 @@ describe.each([
|
||||
|
||||
it("ends the request inside Docker's timeout, after the database probe's", () => {
|
||||
const check = read();
|
||||
const wget = wgetTimeout(check.command);
|
||||
expect(wget).toBeGreaterThan(probeTimeoutSeconds);
|
||||
expect(wget).toBeLessThan(check.timeout);
|
||||
const abort = probeTimeout(check.command);
|
||||
expect(abort).toBeGreaterThan(probeTimeoutSeconds);
|
||||
expect(abort).toBeLessThan(check.timeout);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user