a73x

caf6fe94

fix(test): five soak persistence-phase gaps found in review

a73x   2026-08-14 09:26

Commit message
fix(test): five soak persistence-phase gaps found in review

1. RSS=${RSS:-0} after the awk sample: an unreaped zombie PDPID leaves
   /proc/$PDPID/status present but VmRSS-less, so awk exits 0 with empty
   output and the `|| echo 0` fallback never fires — the died-mid-phase
   guard was silently skipped by `[ "" -eq 0 ]` erroring-as-false.
2. Vacuous-green guard: capture each cycle's client stdout to $PCAP and
   require "cycle-$c" to appear in it, failing and breaking the cycle
   loop otherwise. Without this, a client that fails instantly leaves
   fds/RSS trivially flat and the phase reports green having never
   driven the daemon.
3. fd-count check resamples once after a 1s settle before failing: the
   client's detach is fire-and-forget, so the daemon may not have closed
   the fd yet at the instant of sampling. A real leak won't self-heal in
   a second, so this doesn't weaken the equality check.
4. kill/wait moved out of the else branch so a daemon that never bound
   its socket is still reaped, not left running.
5. Any persistence-phase failure now copies $PLOG and $PCAP into
   $FAILDIR before cleanup, matching the run loop's evidence convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

test/soak.sh
Old New
@@ -64,9 +64,11 @@ done
64 # history) that are capacity, not leakage. 64 # history) that are capacity, not leakage.
65 PSOCK="$TMP/mux-soak-persist-$$.sock" 65 PSOCK="$TMP/mux-soak-persist-$$.sock"
66 PLOG="$TMP/mux-soak-persist-$$.log" 66 PLOG="$TMP/mux-soak-persist-$$.log"
67 PCAP="$TMP/mux-soak-persist-$$.cap"
67 CYCLES="${SOAK_CYCLES:-20}" 68 CYCLES="${SOAK_CYCLES:-20}"
68 WARMUP=3 69 WARMUP=3
69 RSS_BOUND_KB=4096 70 RSS_BOUND_KB=4096
71 PERSIST_FAILED_BEFORE=$FAILED
70 "$MUXD" run --sock "$PSOCK" --shell /bin/sh > "$PLOG" 2>&1 & 72 "$MUXD" run --sock "$PSOCK" --shell /bin/sh > "$PLOG" 2>&1 &
71 PDPID=$! 73 PDPID=$!
72 _i=0 74 _i=0
@@ -77,17 +79,36 @@ if [ ! -S "$PSOCK" ]; then
77 printf 'persistence: daemon never bound\n' >> "$SUMMARY" 79 printf 'persistence: daemon never bound\n' >> "$SUMMARY"
78 else 80 else
79 BASE_RSS=0; BASE_FD=0; RSS=0; FD=0 81 BASE_RSS=0; BASE_FD=0; RSS=0; FD=0
82 ATTACH_FAILED=0
80 c=1 83 c=1
81 while [ "$c" -le "$CYCLES" ]; do 84 while [ "$c" -le "$CYCLES" ]; do
82 { printf 'echo cycle-%s\n' "$c"; sleep 1; printf '\034'; } | \ 85 { printf 'echo cycle-%s\n' "$c"; sleep 1; printf '\034'; } | \
83 timeout 30 "$MUX" --sock "$PSOCK" > /dev/null 2>&1 86 timeout 30 "$MUX" --sock "$PSOCK" > "$PCAP" 2>/dev/null
87 # Vacuous-green guard: a regression that makes the client fail
88 # instantly would leave fds/RSS trivially flat and this phase green
89 # without ever having driven the daemon. Require the echo to land.
90 if ! grep -q "cycle-$c" "$PCAP"; then
91 echo "soak FAIL: persistence cycle $c client never attached/echoed"
92 FAILED=$((FAILED + 1))
93 printf 'persistence: cycle %s no attach\n' "$c" >> "$SUMMARY"
94 ATTACH_FAILED=1
95 break
96 fi
84 RSS=$(awk '/VmRSS/{print $2}' "/proc/$PDPID/status" 2>/dev/null || echo 0) 97 RSS=$(awk '/VmRSS/{print $2}' "/proc/$PDPID/status" 2>/dev/null || echo 0)
98 # The daemon pid can be an unreaped zombie by now: /proc/$PDPID/status
99 # still exists but has no VmRSS line, awk exits 0 with empty output,
100 # and the `|| echo 0` fallback never fires (awk didn't fail). Guard
101 # the empty string directly so the died-mid-phase check below isn't
102 # skipped by `[ "" -eq 0 ]` erroring-as-false.
103 RSS=${RSS:-0}
85 FD=$(ls "/proc/$PDPID/fd" 2>/dev/null | wc -l) 104 FD=$(ls "/proc/$PDPID/fd" 2>/dev/null | wc -l)
86 [ "$c" -eq "$WARMUP" ] && { BASE_RSS=$RSS; BASE_FD=$FD; } 105 [ "$c" -eq "$WARMUP" ] && { BASE_RSS=$RSS; BASE_FD=$FD; }
87 c=$((c + 1)) 106 c=$((c + 1))
88 done 107 done
89 echo "soak persistence: $CYCLES cycles, RSS ${BASE_RSS}->${RSS} kB, fds ${BASE_FD}->${FD}" 108 echo "soak persistence: $CYCLES cycles, RSS ${BASE_RSS}->${RSS} kB, fds ${BASE_FD}->${FD}"
90 if [ "$RSS" -eq 0 ] || [ "$BASE_RSS" -eq 0 ]; then 109 if [ "$ATTACH_FAILED" -eq 1 ]; then
110 : # cycle loop already recorded the no-attach failure above
111 elif [ "$RSS" -eq 0 ] || [ "$BASE_RSS" -eq 0 ]; then
91 echo "soak FAIL: persistence daemon died mid-phase" 112 echo "soak FAIL: persistence daemon died mid-phase"
92 FAILED=$((FAILED + 1)) 113 FAILED=$((FAILED + 1))
93 printf 'persistence: daemon died mid-phase\n' >> "$SUMMARY" 114 printf 'persistence: daemon died mid-phase\n' >> "$SUMMARY"
@@ -96,6 +117,14 @@ else
96 # detach must close. RSS gets a bound, not equality — allocators 117 # detach must close. RSS gets a bound, not equality — allocators
97 # retain pages — but growth past it over this few cycles is a leak. 118 # retain pages — but growth past it over this few cycles is a leak.
98 if [ "$FD" -ne "$BASE_FD" ]; then 119 if [ "$FD" -ne "$BASE_FD" ]; then
120 # Detach is fire-and-forget from the client's side; the daemon
121 # may not have closed the fd yet at the instant we sampled. One
122 # settle beat kills the flake without weakening the equality
123 # check itself — a real leak won't self-heal in a second.
124 sleep 1
125 FD=$(ls "/proc/$PDPID/fd" 2>/dev/null | wc -l)
126 fi
127 if [ "$FD" -ne "$BASE_FD" ]; then
99 echo "soak FAIL: persistence fd count $BASE_FD -> $FD across detached cycles" 128 echo "soak FAIL: persistence fd count $BASE_FD -> $FD across detached cycles"
100 FAILED=$((FAILED + 1)) 129 FAILED=$((FAILED + 1))
101 printf 'persistence: fd leak %s->%s\n' "$BASE_FD" "$FD" >> "$SUMMARY" 130 printf 'persistence: fd leak %s->%s\n' "$BASE_FD" "$FD" >> "$SUMMARY"
@@ -106,17 +135,26 @@ else
106 printf 'persistence: RSS grew %s kB\n' "$((RSS - BASE_RSS))" >> "$SUMMARY" 135 printf 'persistence: RSS grew %s kB\n' "$((RSS - BASE_RSS))" >> "$SUMMARY"
107 fi 136 fi
108 fi 137 fi
109 kill "$PDPID" 2>/dev/null
110 wait "$PDPID" 2>/dev/null
111 # The daemon's own Zig-side verdict rides along for free (6a).
112 if grep -q "LEAK:" "$PLOG"; then
113 echo "soak FAIL: persistence daemon reported leaked allocations:"
114 grep "LEAK:" "$PLOG"
115 FAILED=$((FAILED + 1))
116 printf 'persistence: LEAK marker\n' >> "$SUMMARY"
117 fi
118 fi 138 fi
119 rm -f "$PLOG" "$PSOCK" 139 # Kill unconditionally, not just on the bound-socket path — a daemon that
140 # never bound its socket is still a running process; never assume it died.
141 kill "$PDPID" 2>/dev/null
142 wait "$PDPID" 2>/dev/null
143 # The daemon's own Zig-side verdict rides along for free (6a).
144 if grep -q "LEAK:" "$PLOG" 2>/dev/null; then
145 echo "soak FAIL: persistence daemon reported leaked allocations:"
146 grep "LEAK:" "$PLOG"
147 FAILED=$((FAILED + 1))
148 printf 'persistence: LEAK marker\n' >> "$SUMMARY"
149 fi
150 if [ "$FAILED" -gt "$PERSIST_FAILED_BEFORE" ]; then
151 # Same convention as the run loop above: a failure destroys nothing
152 # that would explain it.
153 mkdir -p "$FAILDIR"
154 cp "$PLOG" "$FAILDIR/persistence.log" 2>/dev/null
155 [ -f "$PCAP" ] && cp "$PCAP" "$FAILDIR/persistence.cap" 2>/dev/null
156 fi
157 rm -f "$PLOG" "$PSOCK" "$PCAP"
120 rm -f "$LOG" 158 rm -f "$LOG"
121 echo "---" 159 echo "---"
122 if [ "$FAILED" -eq 0 ]; then 160 if [ "$FAILED" -eq 0 ]; then