fleetd #493: never build into the path a running daemon holds #510
Reference in New Issue
Block a user
Delete Branch "worker/493-479f45-2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes fleetd #493.
The defect
redeploy-fleetd.shranmvn -f fleetd/pom.xml clean installdirectly intofleetd/target/fleetd.jar— the exact path the still-running daemon was startedfrom — before ever stopping it. A JVM loads classes lazily, so a class the
daemon had not needed yet could be read from a jar that had already been
replaced or removed while the OLD daemon was still alive. The failure landed on
the shutdown drain (
NoClassDefFoundErroron an unloaded failure-path class,process still exits 143, looks clean). See fleetd #413.
The fix
stage_built_jar()— right after a successful build, moves the jar Mavenproduced (still
target/fleetd.jar, sincepom.xmlis unchanged and out ofthis ticket's scope) to a staging path,
target/fleetd-new.jar. Dies (daemonuntouched — this runs before stop) if the build reported success but left no
jar, or if the move itself fails.
wait_for_daemon_exit(timeout)— the existing wait-for-exit loop, extractedto its own function so the ordering guarantee below is checkable.
swap_staged_jar(staged, live)— the actual swap: a plainmvof the stagedjar onto the live path. Called only after
wait_for_daemon_exitconfirms theOLD daemon (if any) has exited, right before the "start" section. If it
fails, the script dies and never starts a new daemon.
require_no_build_jar()—--no-buildrestarts whatever jar is already atthe live path; same check, same truthful message as before
(
no jar at $JAR — run without --no-build).fleetd-new.jarfrom an interrupted prior run is wiped before thenext build starts, so it can never poison the next run.
build still never takes the fleet down.
jar_id()now takes an optional path argument (defaults to$JAR, the livepath) so the post-build "jar now:" line can report the staged jar's hash
without ever changing what a bare
jar_idmeans —--checkand the final"pid …, jar …" line both still call it with no args, so neither can be fooled
by a leftover staged file.
("aborted — nothing changed") now names the staged jar when a build already
ran, since staging moves the freshly built jar off the live path before that
prompt runs — "nothing changed" was no longer quite true otherwise.
Testing
Baseline (measured on the unmodified tree,
HEAD):bash scripts/test-redeploy-fleetd.shexits
0, invokes 23 test functions, and prints 3 literalFAIL:lines fromits own internal mutation-test cells (not real suite failures) plus a final
PASS: redeploy log classifier.After this change: same command exits
0, invokes 33 test functions (10 new:stage_built_jarx2,swap_staged_jarx3,require_no_build_jarx2,wait_for_daemon_exitx2, plus one source-order test), and prints the same 3literal
FAIL:lines plus the same finalPASS:line.bash -npasses on bothscripts/redeploy-fleetd.shandscripts/test-redeploy-fleetd.sh.Mutation proof (5 guards, each: mutant applied with two different-string
greps, suite run, restored,
shasum -a 256byte-identical, green control run)stage_built_jardisabled (return 0inserted before its body) →FAIL: stage_built_jar left the jar behind at the live path …/fleetd.jar,exit 1.
swap_staged_jardisabled →FAIL: swap_staged_jar left the staged file behind at …/fleetd-new.jar, exit 1.require_no_build_jardisabled →FAIL: require_no_build_jar accepted a missing jar, exit 1.wait_for_daemon_exitdisabled →FAIL: wait_for_daemon_exit returned before actually re-checking running_pid, exit 1.starttoright before the drain gate (ahead of
wait_for_daemon_exit) →FAIL: swap_staged_jar (line 604) is not after wait_for_daemon_exit (line 665), exit 1.Each mutant was restored from a saved copy and confirmed byte-identical via
shasum -a 256before the corresponding green control run (exit 0, same 3literal
FAIL:lines, same finalPASS:line).What I could NOT verify
forbidden from running the script against the live fleetd (it's the channel
this session talks through), so I never exercised the actual
build → stage → stop → wait → swap → start sequence against a real process.
The unit tests exercise the pure functions and the source-order guarantee in
isolation only.
packagestep (which still targetstarget/fleetd.jardirectly perpom.xml'sfinalName=fleetd, unchanged —out of this ticket's scope) does an atomic rename-over-existing-file or an
in-place truncate-and-rewrite of the live inode. I did not run a real
mvn clean installto inspect this, and the brief's own text explicitlyaccepts either outcome ("if Maven still writes target/fleetd.jar as part of
package, copy or move it to the staging name immediately").
fleetdmodule (out of scope: onlythe two
scripts/*.shfiles changed; "Nothing under fleetd/src. No Java.").Per the brief: the primary should run the real redeploy as the acceptance
test.
Out of scope, spotted but not investigated
None spotted beyond what's already noted above.
redeploy-fleetd.sh's build step wrote straight into fleetd/target/fleetd.jar via `mvn clean install` while the OLD daemon was still running from that exact path. A JVM loads classes lazily, so a class the daemon had not touched yet could be read from a jar already replaced or removed -- the failure landed on the shutdown drain (NoClassDefFoundError, exit 143, looks clean). Stage the freshly built jar at target/fleetd-new.jar (stage_built_jar), confirm the OLD pid has actually exited (wait_for_daemon_exit, extracted from the existing wait loop), and only then swap it into the live path (swap_staged_jar) -- strictly after the wait, strictly before start. A failed swap dies without starting. --no-build and --check keep their existing, truthful behavior (require_no_build_jar; jar_id still reads the live path by default). A leftover staged jar from an interrupted run is wiped before the next build. The build still runs before anything is stopped, so a failed build still never takes the fleet down. Also fixed: the drain-gate abort message ("aborted -- nothing changed") now names the staged jar when one exists, since staging already moves the freshly built jar off the live path before that prompt runs. Adds unit tests for stage_built_jar, swap_staged_jar, require_no_build_jar, wait_for_daemon_exit, and a source-order test proving swap sits after the wait and before start (sourcing stops before the main flow ever runs, so the ordering itself can only be checked by reading the script's own call sites).