fleetd #603: share HEALTH_WAIT between the pid poll and the health check #607
Reference in New Issue
Block a user
Delete Branch "worker/redeploy-slowstart-ead0e5-5"
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 #603.
The bug: the start step's pid-wait loop gave the new process its own short, fixed 10s budget before a hard
die, while the health check right after it waits a fullHEALTH_WAIT(60s) for the same daemon to answer. Under launchd,launchctl loadreturns before the java process exists, and on a slow host that took longer than 10s -- so the script reported "no process appeared" on a deploy that had fully succeeded (confirmed by hand: pid, healthz 200, and a fresh log line all present).The fix:
wait_for_new_pid(the dual of the existingwait_for_daemon_exit) now sharesHEALTH_WAITinstead of holding its own shorter budget.await_daemon_startedfolds the pid-appeared check and the healthz check into one decision (same shape asswap_if_built/refuse_drain_gate/report_shutdown_drain): a pid miss is no longer fatal by itself -- it falls through to the health check, which is direct proof the daemon is up, rather than a proxy for it. A genuine failure still dies, andreport_health's own die() still prints the log tail. The main flow callsawait_daemon_startedunconditionally (no bareifleft to invert), sotest_no_untested_main_flow_conditionalsneeded no allowlist changes.Tests added (scripts/test-redeploy-fleetd.sh): unit tests for
wait_for_new_pid(mirroring the existingwait_for_daemon_exittests), and two behavioural tests onawait_daemon_startedcovering both of the ticket's acceptance criteria in the same suite run: a slow-but-real start (pid appears well after the old 10s budget, health answers -> succeeds) and a genuine failure (pid never appears, health never answers -> dies, log tail included). Also updatedtest_report_health_call_site_present's grep target since that call site moved inside the new function, and addedtest_await_daemon_started_call_site_present.Same-shape sweep (not fixed here): I looked for other fixed, short timeouts that decide a hard
diewhile a longer, more truthful check is never reached. The only other timing-budgetdiein the script iswait_for_daemon_exit/STOP_WAIT(30s) at the stop step. I don't think it shares this defect: there is no healthz-like truer signal available there -- pid-still-alive really is the ground truth for "did the old daemon actually stop", so there's nothing more truthful being skipped. Flagging this reasoning for review rather than asserting it's clean.Build:
scripts/test-redeploy-fleetd.shexits 0, ends withPASS: redeploy log classifier(129 test invocations, including the 5 new ones).mvn -o clean installin fleetd/: BUILD SUCCESS, Tests run: 1819, Failures: 0, Errors: 0, Skipped: 0.Lead review. Strong work. The design is better than what I asked for, and the core fix is genuinely pinned — I proved that with my own mutation rather than taking your word. One untested path left. Not merging yet; it is one test.
Verified by me
My mutation 1 — KILLED. The fix is real.
I put the hardcoded budget back:
That is the evidence that matters.
test_await_daemon_started_slow_pid_then_healthy_succeedsis a real behavioural test, not a source-text check: it drivesrunning_pidto stay empty for 11 calls and then return, stubssleepto a no-op, and assertsDIED_CALLED = 0. Revert the fix and it goes red. Good.My mutation 2 — SURVIVED. Here is the gap.
I made a pid miss fatal again — the
elsebranch straight back to the old behaviour:A survivor has more than one explanation, so I checked before reporting it. This is not an equivalent mutant and not a weak test. It is a different behaviour that nothing exercises:
test_await_daemon_started_slow_pid_then_healthy_succeeds— the pid does appear, sowait_for_new_pidreturns 0 and theelsebranch never runs.test_await_daemon_started_never_appears_dies_with_log_tail— pid never appears and health fails, so it dies either way. The mutation changes whichdiefires, not whether one does.Neither test covers the case the fall-through exists for: pid never appears, but
/healthzanswers — must NOT die.That is not a hypothetical.
running_pid()'s own comment says it under-counts, because its allowlist only recognises a plainjava -jar. The day the launch method changes, the pid never matches and the daemon is perfectly healthy. Your fall-through handles that correctly, and right now nothing would notice if someone removed it — which is how #603 came back through a different door the first time.What to add
One test:
running_pidnever returns anything,poll_health_bodysucceeds. AssertDIED_CALLED = 0, and assert the warn line is emitted so the operator is told the pid could not be identified.Acceptance as a property, both halves observed: it passes as written, and it fails when that
warnis replaced bydie "no process appeared". Run that mutation yourself and paste the green and the red. I have just shown the current suite stays green under it, so a new test that does not go red has not closed anything.While you are there, decide what
NEW_PIDshould be on that path. Your code does[ -n "$NEW_PID" ] || NEW_PID="$(running_pid)"afterwards, which on this path is still empty, so the result line printspid unknown. I think that is right — it is the honest answer, and it matches the three-state thinking in the gauge work: never print a number you could not establish. Assert it rather than leave it incidental.On your two flagged items — both judged correct
The doubled worst-case time-to-fail (~120s). Accepted, and thank you for surfacing it rather than burying it. It only costs time on a genuine failure, and trading a slower true failure for the removal of a false one is the right direction. The false FAIL was pushing people toward a hand-rolled stop/start, which is banned.
wait_for_daemon_exit/STOP_WAIT(30s). Your reasoning holds and I checked it: there is no truer signal to fall through to, because pid-still-alive is ground truth for "did the old daemon stop". Adiethere is correct. Good sweep, and right call to flag it as a conclusion for review rather than assert it clean.test_report_health_call_site_present. Not weakened — the call genuinely moved into the function and the new grep target is equally specific. Correctly declared.The call-site presence checks. I told you to prefer behavioural tests, and you did; these sit alongside real ones and match this file's existing convention, so they are fine. Your comment explaining why a grep cannot distinguish "falls through" from "still dies" is exactly right, and it is the reason mutation 2 matters.
Re-run the full suite after the change and report the real output.
Added the missing test and pushed (
856dfc6):test_await_daemon_started_pid_never_found_but_healthy_warns_and_survives--running_pidnever returns anything,poll_health_bodysucceeds. AssertsDIED_CALLED = 0, the warn line is emitted, andNEW_PIDstays empty (pinned, not left incidental).Ran your mutation myself before reporting, both halves:
git diff --stat scripts/redeploy-fleetd.shwas empty after reverting, confirming the mutation only ever touched a scratch copy of the running state, not what's committed.Full suite after the new test, unpiped: exit 0, final line
PASS: redeploy log classifier(130 test invocations now).mvn -o clean installin fleetd/: BUILD SUCCESS, Tests run: 1819, Failures: 0, Errors: 0, Skipped: 0.