Skip to content

fix: Supervise both children instead of gating startup on the model - #15

Merged
harshaneel merged 2 commits into
mainfrom
hg/entrypoint-no-startup-gate
Aug 5, 2026
Merged

fix: Supervise both children instead of gating startup on the model#15
harshaneel merged 2 commits into
mainfrom
hg/entrypoint-no-startup-gate

Conversation

@harshaneel

Copy link
Copy Markdown
Owner

Fixes #13.

Summary

  • entrypoint.sh no longer gates startup on the model. It polled llama-server's health for 120 seconds before starting the proxy, and on timeout called a cleanup that ran jobs -p | xargs kill followed by a bare wait. jobs lists nothing in a non-interactive shell, so nothing was killed and the wait blocked forever on a live child. The script never reached its exit, leaving a container that stayed up with a healthy engine on 127.0.0.1:8080 and nothing listening on 8090. The same function was on the INT/TERM trap, so docker stop hung too.
  • Both children are now supervised for their whole lifetime. They start in the background with recorded PIDs, and the script exits when either dies, propagating its status. The old exec localaik handed off and stopped watching, so llama-server crashing after startup silently broke every request instead of stopping the container.
  • Readiness is reported, not gated. /health already probes upstream and answers 503 when it is unreachable, so the proxy answers immediately and tells you the truth while the model loads.
  • HEALTHCHECK --start-period goes 60s to 180s, matching the readiness bounds the Makefile, CI and the integration helpers already use. With the shell-level bound gone, the healthcheck is now the signal for a model that never finishes loading, so it should not be shorter than the waits everything else uses.

Behaviour changes worth knowing about

These images have 1,367 pulls, so the changes are called out rather than buried.

  • "Port 8090 is open" no longer implies the model is loaded. A TCP liveness check, nc -z, or Compose depends_on without condition: service_healthy will now let requests through early, and those get a 502 from the proxy. Wait for GET /health to return 200 instead. This is documented in Quick start, not only in Limitations.
  • There is no longer any upper bound on model load. An engine that hangs without dying leaves the container running indefinitely serving 503. Docker reports unhealthy after roughly 195s, but nothing self-heals. That is the deliberate trade for deleting a bound that did not work: a container that reports 503 honestly beats one that wedges while looking alive. Use a restart policy or an orchestrator liveness probe if you want automatic recovery.
  • An engine that dies still stops the container, now with a non-zero status and a clear log line, which is what the old code was trying and failing to do.

Test Plan

  • 5 new tests in internal/entrypoint/ drive the real script with stub children: the proxy starts without waiting for the model, a crashing engine propagates its status, a child exiting 0 still escalates to non-zero, a crashing proxy stops the engine, and SIGTERM stops both and exits 0.
  • All 5 fail against the old script, verified by restoring it, so the coverage is real rather than decorative.
  • Passed 3 consecutive runs. They prefer /bin/dash when present, since that is what the container runs, rather than silently testing only bash locally.
  • Placed under internal/ so make test-unit runs them. In integration/ they would only run after make docker-up succeeded, which is the step a startup regression breaks first.
  • make lint clean, all unit tests pass.
  • Full image built and run: /health returns 503 {"status":"unhealthy"} immediately instead of refusing connections, then 200 once loaded, Docker reports healthy, and a real chat completion succeeds.
  • docker inspect confirms Docker flips to healthy on the first passing check, so the longer start-period does not delay readiness.
  • docker stop returns in 0s with exit code 0.
  • Engine death at startup (LLAMA_SERVER_BIN=/bin/false) exits the container with status 1 and logs localaik: llama-server exited with status 1, stopping the container.

Review notes

Three review passes ran. Two findings changed the design:

  • Arming the trap before the spawns, which the fix requires, makes it reference unset PIDs. Under set -u, kill "${A:-}" "${B:-}" errors on the empty argument and || true swallows it, so the second child would never be signaled. It iterates per PID instead.
  • The start-period was initially raised to 300s, which was arbitrary and inconsistent with four existing 180s bounds. Since the healthcheck is now the only remaining signal for a wedged load, inflating it weakened the thing this design depends on.

Also addressed: the SIGTERM test raced the trap-installation window, the tests discarded the script's output on failure, and the surviving child is now waited for so its final log lines reach docker logs.

Not done, worth a follow-up: adding shellcheck to make lint, which would have caught the original jobs -p bug. It is not installed in this environment, so it could not be verified here.

🤖 Generated with Claude Code

harshaneel and others added 2 commits August 4, 2026 17:55
entrypoint.sh polled llama-server's health for 120 seconds before starting
the proxy, and on timeout called a cleanup that ran `jobs -p | xargs kill`
followed by a bare `wait`. `jobs` lists nothing in a non-interactive shell,
so nothing was killed and the wait blocked forever on a live child. The
script never reached its exit, leaving a container that stayed up with a
healthy engine on 127.0.0.1:8080 and nothing listening on 8090. The same
function was on the INT/TERM trap, so docker stop hung too.

The gate is gone. Both children start in the background with recorded PIDs
and the script exits when either dies, propagating its status. The proxy
already probes upstream on /health and answers 503 when it is unreachable,
so readiness is reported rather than gated, and llama-server dying after
startup is now noticed instead of silently breaking every request.

HEALTHCHECK start-period rises to 180s, matching the readiness bounds the
Makefile, CI and integration helpers already use, since the healthcheck is
now the signal for a model that never finishes loading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The proxy section said existing wait loops work unchanged, which holds only
for loops polling /health for a 200. The port opens before the upstream is
reachable there too, so a TCP liveness check has the same gap the bundled
tags now document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@harshaneel
harshaneel force-pushed the hg/entrypoint-no-startup-gate branch from 6a7547c to 15dbe44 Compare August 5, 2026 00:57
@harshaneel
harshaneel merged commit c22ecb2 into main Aug 5, 2026
3 checks passed
@harshaneel harshaneel mentioned this pull request Aug 5, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

entrypoint.sh: container wedges instead of failing when model load exceeds 120s

1 participant