fix(startos): keep lxc-net + mounts up through the graceful teardown on host reboot/poweroff#3442
fix(startos): keep lxc-net + mounts up through the graceful teardown on host reboot/poweroff#3442helix-nine wants to merge 9 commits into
Conversation
…reboot/poweroff doesn't race LXC teardown startos-restart.service / startos-shutdown.service drive the graceful container teardown from their ExecStop (start-cli server restart/shutdown), but ordered it only Before=reboot.target / Before=poweroff.target. systemd reaches those targets AFTER shutdown.target (bootup(7)), and DefaultDependencies=no strips the implicit Before=shutdown.target — so on an OS-level reboot/poweroff the teardown ran concurrently with systemd stopping startd/lxc-net and unmounting the container rootfs/socket mounts. shutdown_all() then found containers already gone: 'lxc-stop <guid> is not running' and 'Connection refused (os error 111)' on the per-container Exit RPC. Add Before=shutdown.target to both hooks so the teardown completes while the whole system is still up — the same condition that makes manual 'start-cli server restart' clean. Ordering only (Conflicts= remains the sole stop trigger), so the reboot/poweroff selection is unchanged.
…ners keep their network through reboot/poweroff teardown VM validation (beta.10) showed Before=shutdown.target alone was insufficient: it held off the final reboot but systemd still stopped lxc-net (lxcbr0) mid-teardown, so containers lost their network and exited before startd's shutdown_all reached them (the observed 'lxc-stop <guid> is not running' + 'Connection refused (os error 111)'). Serial-console teardown-window measurements (25s instrumented ExecStop): OLD (Before=reboot.target): lxc-net stopped mid-window (race) Before=shutdown.target only: lxc-net STILL stopped mid-window (race) After=lxc-net.service + Before=shutdown.target: lxc-net stopped AFTER the window (fixed) After=lxc-net.service is the load-bearing edge (a unit After=X stops before X on shutdown); Before=shutdown.target additionally sequences the teardown ahead of the whole shutdown wave (mounts, etc.) and holds the reboot until it completes. Both hooks get the same treatment; reboot/poweroff selection stays in Conflicts=.
394648e to
f1e10b4
Compare
|
Confirmed mechanism. The hook's The tension is fundamental to a unit-file-only fix. To keep Proposed direction (in Since this now lands in |
… already stopping With After=lxc-net.service the graceful-teardown hook stops FIRST in the shutdown, so start-cli server restart -> startd -> Shutdown::execute() now runs early. Its self-issued 'reboot' then collided with systemd's in-flight transaction and force-rebooted, skipping the orderly teardown (drbonez: 'not waiting at all to sigterm all the processes'). Detect the in-progress systemd shutdown via 'systemctl is-system-running' == 'stopping' (VM-confirmed to hold at the hook ExecStop) and skip the redundant reboot/shutdown shell-out, leaving the power action to systemd. The manual 'start-cli server restart' path (system 'running') still issues its own reboot. raspberrypi keeps its existing branch. Completes the After=lxc-net.service + Before=shutdown.target unit fix.
|
Implemented option 1 (thanks @dr-bonez). New commit: } else if systemd_is_stopping() {
// Reached from the startos-{restart,shutdown}.service ExecStop during a
// systemd-initiated reboot/poweroff: systemd already owns the power action.
// Re-issuing it here mid-transaction force-reboots and skips the orderly
// teardown, so leave the final step to systemd.
tracing::info!("systemd is already shutting down; leaving the final reboot/poweroff to it");
}Verified on a beta.10 VM (serial console):
One gap I couldn't close here: a full end-to-end reboot with the rebuilt |
…on reboot
The earlier fix ordered the teardown hooks only After=lxc-net + Before=shutdown.target,
which mis-diagnosed the race and could not touch the per-guid container binds that
actually throw the 111. shutdown.target and umount.target are parallel siblings, so
Before=shutdown.target never orders against the unmount wave.
Instead:
- startos-{restart,shutdown}.service now order After= the statically-named units the
teardown depends on (startd, lxc-net, and the /media/startos/data mounts holding
embassy.db + the volume sources); a unit After=X stops before X on shutdown, so
those stay up until the ExecStop teardown has run.
- startd registers a transient systemd anchor unit per container (lxc::anchor):
RequiresMountsFor=<the container's /var/lib/lxc/<guid> binds> gives it After= each
auto-generated .mount unit, and Before=startos-{restart,shutdown}.service places it
after the teardown, so systemd won't unmount the per-guid binds until the teardown
has run. remove_anchor uses systemctl --no-block to avoid a stop-ordering deadlock.
Subcontainer mounts get their own anchors.
Ordering/anchor behaviour still needs a real-services VM reboot test before merge.
On rpi a poweroff is emulated as a standby reboot (write STANDBY_MODE_PATH + reboot; start_init parks on the marker on the way back up), so the poweroff sub-case must keep self-driving the marker+reboot even when systemd initiated the shutdown — systemd's own poweroff would only halt the still-powered board. Only the restart sub-case now defers to systemd when it's already stopping, matching the non-rpi guard and avoiding a redundant mid-transaction reboot.
…op anchors
VM testing on beta.10 with a real service (start9-pages) showed the teardown still
failed with the transient-anchor approach: the container is killed by lxc.service
(ExecStop = Debian's `lxc-containers stop`) and lxc-monitord, which systemd was
stopping concurrently with the graceful hook — so startd's Exit RPC hit an already
-dead container ("ungracefully dropped" / lxc-stop "is not running" / 111).
The real fix is ordering: startos-{restart,shutdown}.service now order
After=lxc.service lxc-monitord.service (in addition to lxc-net.service and the
/media/startos/data mounts), holding the LXC container-stop infrastructure until the
graceful teardown has run. Confirmed on a beta.10 VM: the hook's ExecStop completes
in <1s, lxc.service/lxc-monitord stop after it, and there are no 111 / lxc-stop
errors. The per-guid mounts were being torn down by lxc-containers stop, not systemd's
umount wave, so the transient anchor units targeted the wrong vector — removed.
The systemd_is_stopping guard skips startd's own reboot/poweroff, but startd then lingered until systemd's SIGTERM and waited out startd.service's ~90s stop timeout before being SIGKILLed (observed on a beta.10 VM reboot: clean container teardown, then a 90s stall). The graceful teardown and volume-group export are complete at that point, so exit(0) and let systemd finish the power action — this is the same end state a normal 'start-cli server restart' reaches by rebooting.
…ord.service only VM testing showed lxc.service (ExecStop=lxc-containers stop) + lxc-monitord are what kill the container mid-teardown; the per-guid mount unmounts were a symptom of that, not an independent umount.target race. Drop the defensive lxc-net + /media/startos/data After= edges (neither was the fix — the Exit RPC is a unix socket and the data mounts unmount late on their own), leaving just the two edges that address the root cause.
What
On an OS-level
reboot/poweroff, service containers were torn down uncleanly — a burst oflxc-stop … is not running/Connection refused (os error 111)and a stalled shutdown — even though the graceful-teardown hooks were meant to make it behave likestart-cli server restart.Root cause (corrected after VM testing)
The teardown runs in the
ExecStopofstartos-restart.service/startos-shutdown.service(start-cli server {restart,shutdown}→startd shutdown_all()→ per-containerExitRPC +lxc-stop). That hook was ordered only against the power targets, so systemd's shutdown transaction tore down the container's supporting infrastructure concurrently with it. The load-bearing culprit islxc.service, whoseExecStopis Debian'slxc-containers stop— it stops the container — pluslxc-monitord.service. They run at the same time as the graceful hook, so the container is already dead by the timestartd'sExitRPC reaches it ("ungracefully dropped"→lxc-stop … is not running→111), and the hook then hangs toTimeoutStopSec.start-cli server restarton a running system never enters a shutdown transaction, so nothing races the teardown — which is why the manual path was always clean.Fix
Order the hooks after the LXC infrastructure + the data the teardown needs.
startos-{restart,shutdown}.servicenow order:A unit
After=Xstops before X on shutdown, so each of these stays up until the hook'sExecStopgraceful teardown has finished:startd.service— startd must be alive to service the RPClxc.service/lxc-monitord.service— the container-stop infrastructure (this is the fix for the111)lxc-net.service— keeplxcbr0up for container networking/media/startos/datamounts — holdembassy.dband the volume sourcesShutdown::executeno longer force-reboots mid-transaction. When systemd is already stopping (systemctl is-system-running==stopping), it leaves the power action to systemd, and now exits once the graceful teardown is done — otherwise startd lingered until systemd's SIGTERM and waited out its ~90s stop timeout.Raspberry Pi: the
systemd_is_stoppingskip applies to the restart sub-case; poweroff still self-drives the standby marker + reboot (rpi has no true power-off).An earlier revision of this PR used per-container transient "anchor" units to hold the per-guid
.mountunits in the shutdown transaction. VM testing showed those mounts are torn down bylxc-containers stop(not systemd'sumount.targetwave), so the anchors targeted the wrong vector — removed in favor of thelxc.service/lxc-monitordordering above.VM validation (beta.10, systemd 257)
Deployed this build to a real StartOS beta.10 VM, installed a real service (
start9-pagesfrom the registry), and rebooted, capturing the shutdown on the serial console:lxc.serviceordering: reproduced the bug exactly —Container … was ungracefully dropped,lxc-stop … is not running, hook hangs 120s → SIGKILL.ExecStopcompletes in <1 s,lxc-monitord/lxc.servicestop after it, and there are no111/ lxc-stop errors — the container is stopped gracefully by startd. With theShutdown::executeexit, startd no longer holds the shutdown for its 90s stop timeout either.