caf6fe94
fix(test): five soak persistence-phase gaps found in review
a73x 2026-08-14 09:26
Commit message
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 |