8ae432223a
Replaces the detached `setsid nohup … nsenter …` launch, which had no restart policy, no boot persistence and no supervisor-visible logs. Staging units are installed and proven; production units are TEMPLATES and are not installed. Three decisions, each measured rather than assumed: * `Wants=`, not `Requires=`, from host to Core. With `Requires`, stopping Core stopped the host AND a later Core start did not bring it back -- a routine Core restart would leave the client with no server. With `Wants` the host survives a Core outage, answers 503 core_unavailable, never falls back to Python, and resumes the moment Core returns with no intervention. Both halves tested. * Readiness is a bounded ExecStartPre TCP gate, because ordering proves nothing about readiness and Type=exec only proves the binary exec'd. Core binds its listener after migrations and content load, so "port open" is a real signal. The gate FAILS rather than blocking: a host that waits forever looks healthy while serving nobody. * The netns is resolved by container NAME every start. The container is restart=unless-stopped and its netns inode CHANGES on restart (measured: 4026539938 -> 4026540033), so a hardcoded pid is wrong by construction and anything left in the old namespace serves nobody. Proven equivalent to today's nsenter against a scratch container, never production's namespace. `systemd-analyze verify` caught two real defects before deployment: StartLimitIntervalSec/StartLimitBurst sat in [Service], where systemd 252 silently ignores them, so the crash-loop ceiling was not taking effect; and a Documentation URL containing %20 parsed as a specifier. Both fixed and the effective properties re-confirmed from the running units. Staging evidence: Core-first ordering, host refused when Core is absent or merely not listening, outage survival, automatic recovery, restart, graceful stop with no strays, boot simulated via multi-user.target, 3x SIGKILL contained at ~5s spacing, journald logs, and economy state byte-identical throughout (integrity ok, fk 0).
43 lines
1.5 KiB
Bash
Executable File
43 lines
1.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# Bounded wait for a TCP listener. Exits 0 as soon as it connects, 1 on timeout.
|
|
#
|
|
# Used as the FIFA17 host's ExecStartPre so the host cannot start "successfully"
|
|
# while Core is absent. A TCP connect is a MEANINGFUL readiness signal here, not
|
|
# a guess: openfut-core binds its listener only after it has opened the
|
|
# database, run migrations and loaded the content pack — the startup log order
|
|
# is `Running database migrations` -> `Loaded N cards` -> `Listening`, and the
|
|
# 2026-08-22 promotion demonstrated it directly when a bind collision aborted a
|
|
# Core that had already applied migration 0028. So "port open" implies
|
|
# "migrations done, content loaded".
|
|
#
|
|
# Deliberately no retry-forever: a host that blocks indefinitely looks healthy
|
|
# to a supervisor while serving nothing. It fails, and the restart policy
|
|
# decides what happens next.
|
|
#
|
|
# usage: openfut-wait-tcp.sh <host> <port> <timeout-seconds>
|
|
set -eu
|
|
|
|
HOST="${1:?host}"
|
|
PORT="${2:?port}"
|
|
TIMEOUT="${3:-30}"
|
|
|
|
i=0
|
|
while [ "$i" -lt "$TIMEOUT" ]; do
|
|
# /dev/tcp is a bash-ism; use a portable connect instead.
|
|
if command -v nc >/dev/null 2>&1; then
|
|
nc -z "$HOST" "$PORT" 2>/dev/null && exit 0
|
|
else
|
|
python3 - "$HOST" "$PORT" <<'PY' && exit 0
|
|
import socket, sys
|
|
s = socket.socket()
|
|
s.settimeout(1)
|
|
sys.exit(0 if s.connect_ex((sys.argv[1], int(sys.argv[2]))) == 0 else 1)
|
|
PY
|
|
fi
|
|
i=$((i + 1))
|
|
sleep 1
|
|
done
|
|
|
|
echo "openfut-wait-tcp: $HOST:$PORT did not accept connections within ${TIMEOUT}s" >&2
|
|
exit 1
|