Files
OpenFUT/scripts/systemd
funman300 8ae432223a ops: systemd supervision for Core and the FIFA17 host (staging-proven)
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).
2026-08-22 20:46:15 +00:00
..

OpenFUT systemd units

Supervision for OpenFUT Core and the FIFA17 UTAS host. Replaces the previous setsid nohup … nsenter … launch, which had no restart policy, no boot persistence and no supervisor-visible logs.

file scope installed?
openfut-staging-core.service staging Core, port 18081 yes — proving ground
openfut-staging-host.service staging host, port 8299 yes
openfut-netns.service publishes the container netns to /run/netns/openfut template only
openfut-core.service production Core, port 18080 template only
openfut-host.service production host, port 8099 template only
openfut-netns-bind.sh resolves the container netns by NAME, idempotently helper
openfut-wait-tcp.sh bounded readiness gate helper

Production templates are not installed. Deploy only via the plan in OpenFUT-Vault/06 Operations/OpenFUT Service Supervision (staging-proven).md.

The three decisions worth knowing

1. Wants=, not Requires=, from host → Core. Measured on staging: Requires propagates a Core stop into a host stop, and a later Core start does not bring the host 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 a Python fallback), and resumes the moment Core returns, with no supervisor intervention.

2. Readiness is an ExecStartPre TCP gate, not ordering. After=/Wants= order units; Type=exec only proves the binary exec'd. Neither means Core can serve. Core binds its listener after opening the DB, running migrations and loading the content pack, so "port open" is a genuine readiness signal. The gate is bounded and fails rather than blocking: a host that waits forever looks healthy to the supervisor while serving nobody.

3. The netns is resolved by container NAME at every start. Production must run inside openfut-fut-backend's network namespace. The container is restart=unless-stopped, and its netns inode changes on restart — observed net:[4026539938] → net:[4026540033]. A hardcoded pid is therefore wrong by construction, and any process left in the old namespace keeps running with no interfaces, silently serving nobody. openfut-netns-bind.sh re-resolves by name and refreshes a stale bind mount; NetworkNamespacePath= then enters it declaratively.

Still open: a container restart strands already-running services in the dead namespace. Re-running openfut-netns.service plus restarting Core/host repairs it, but nothing triggers that automatically yet. See the promotion plan's "Residual gap".

Operating

systemctl status openfut-staging-core openfut-staging-host
journalctl -u openfut-staging-core -f
sudo systemctl restart openfut-staging-core          # host survives and recovers
sudo systemctl stop openfut-staging-host openfut-staging-core

Config lives in EnvironmentFiles (…/systemd/core.env, host.env), generated from the live process environment so supervision changed how the processes start and nothing about what they do. Binaries are immutable copies, so a later cargo build cannot change what is running.

Interaction with the staging lifecycle script

scripts/sold-staging-up.py still starts its own unsupervised processes and refuses to run while a staging stack is up. Stop the units first:

sudo systemctl stop openfut-staging-host openfut-staging-core
python3 scripts/sold-staging-up.py --club real --variant highest …

Reconciling the two (having the script drive the units) is deliberately out of scope for the supervision milestone.