Skip to content

Commit b17db41

Browse files
committed
Integration suite: webhook readiness helper, NAT IPAM retry, bench harness
webhook_helpers.sh — gate agent startup on the python:3.12-slim sink being HTTP-bound, not just docker-running. Removes "webhook-sink never came up" flake under suite-wide pressure (4-5 webhook tests). NAT IPAM retry — test_nat_dual_symmetric and shared nat_test_common sweep stale Docker networks on "pool overlaps" and retry compose-up. Bench harness — bench_relay.sh + docker-compose.multi.bench.yml characterize relay throughput without the chaos overlay's 4 MiB tmpfs cap (which was an artifact of disk-full tests). Lane bump 3 -> 5 for parallel NAT tests; chaos-loss30 retry budget 3 -> 6 (math: P(any 3-RTT op succeeds at 30% loss) ~ 0.7^3 ~ 34%; 1 - 0.66^6 ~ 92% comfortable margin). multi3 stack accepts -admin-token via env-substitution at compose-read time (default empty so existing tests unaffected).
1 parent 14c1e05 commit b17db41

19 files changed

Lines changed: 472 additions & 41 deletions

tests/integration/local/RESUME.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
- Pilot daemon: 48-bit addresses `[16-bit network][32-bit node]`, text format `N:NNNN.HHHH.LLLL`
1818
- 34-byte packet header w/ CRC32; tunnel magic `0x50494C54` ("PILT")
1919
- Tunnel relay format: `[0x05][senderID(4)][destID(4)][payload...]` via beacon
20-
- Tests at `/Users/calinteodor/Development/web4/tests/integration/local/`
20+
- Tests at `tests/integration/local/` (relative to repo root)
2121
- Run with `bash run-all.sh -j 6` — uses NAT mutex + splitbrain mutex (added in rerun5 staging)
2222

2323
## Bucket Status (from PROBLEM-REGISTRY.md)
@@ -37,7 +37,7 @@
3737
## Validation pending — REBUILD DOCKER FIRST
3838
Quick path to validate everything landed this session:
3939
```
40-
cd /Users/calinteodor/Development/web4/tests/integration/local
40+
cd tests/integration/local # from repo root
4141
docker compose -f docker-compose.multi.yml build # 5-10 min, ~3-5GB
4242
rm -rf logs && mkdir -p logs
4343
nohup bash ./run-all.sh -j 6 > logs/run-all.out 2>&1 &
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/bin/bash
2+
# Bench relay vs direct steady-state throughput.
3+
#
4+
# Uses `pilotctl bench` which opens ONE long-lived connection to the
5+
# echo port and pumps data through it — that gives clean steady-state
6+
# numbers (the per-file send-file path was hitting retransmit stalls
7+
# from connection-setup churn between iterations; see P1-010).
8+
9+
set -u
10+
export COMPOSE_PROJECT_NAME="pilot-bench"
11+
export PILOT_SUBNET_PREFIX="172.29.99"
12+
13+
DC="docker compose -f docker-compose.multi.yml -f docker-compose.multi.bench.yml"
14+
15+
cd "$(dirname "$0")" || exit 1
16+
# shellcheck source=chaos_helpers.sh
17+
source ./chaos_helpers.sh
18+
19+
cleanup() {
20+
[ -n "${IP_A:-}" ] && [ -n "${IP_B:-}" ] && {
21+
heal_partition agent-a "$IP_B" >/dev/null 2>&1 || true
22+
heal_partition agent-b "$IP_A" >/dev/null 2>&1 || true
23+
}
24+
$DC down -v >/dev/null 2>&1
25+
}
26+
trap cleanup EXIT
27+
28+
SIZES_MB="${BENCH_SIZES_MB:-1 5 10 25 50}"
29+
ITER="${BENCH_ITER:-3}"
30+
31+
RESULTS=""
32+
record() { RESULTS="${RESULTS}$1 $2 $3 $4 $5
33+
"; }
34+
35+
# Inner: run pilotctl bench, parse send-throughput from --json output.
36+
# Returns "rc dur_ms send_MBps total_MBps".
37+
time_bench() {
38+
local mode="$1" size_mb="$2"
39+
local out rc
40+
out=$($DC exec -T agent-a pilotctl --json bench agent-b "$size_mb" --timeout 600s 2>&1)
41+
rc=$?
42+
43+
local sent_bytes recv_bytes send_dur total_dur send_mbps total_mbps total_dur_ms
44+
if [ "$rc" -eq 0 ]; then
45+
sent_bytes=$(echo "$out" | jq -r '.data.sent_bytes // empty')
46+
recv_bytes=$(echo "$out" | jq -r '.data.recv_bytes // empty')
47+
send_dur=$(echo "$out" | jq -r '.data.send_duration_ms // empty')
48+
total_dur=$(echo "$out" | jq -r '.data.total_duration_ms // empty')
49+
send_mbps=$(echo "$out" | jq -r '.data.send_throughput_mbps // empty')
50+
total_mbps=$(echo "$out" | jq -r '.data.total_throughput_mbps // empty')
51+
if [ -z "$total_dur" ] || [ "$total_dur" = "null" ]; then
52+
# Older bench output: only seconds-level fields. Compute from raw.
53+
send_dur=$(echo "$out" | jq -r '.data.send_duration_seconds // 0')
54+
total_dur=$(echo "$out" | jq -r '.data.total_duration_seconds // 0')
55+
total_dur_ms=$(awk -v s="$total_dur" 'BEGIN{printf "%d", s*1000}')
56+
send_mbps=$(echo "$out" | jq -r '.data.send_throughput_mbps // 0')
57+
total_mbps=$(echo "$out" | jq -r '.data.total_throughput_mbps // 0')
58+
else
59+
total_dur_ms="$total_dur"
60+
fi
61+
# Sanity: recv_bytes must equal sent_bytes (otherwise echo path lost data)
62+
if [ -n "$recv_bytes" ] && [ -n "$sent_bytes" ] && [ "$recv_bytes" != "$sent_bytes" ]; then
63+
echo "1 0 0 0"
64+
return
65+
fi
66+
echo "0 ${total_dur_ms:-0} ${send_mbps:-0} ${total_mbps:-0}"
67+
return
68+
fi
69+
echo "1 0 0 0"
70+
}
71+
72+
run_mode() {
73+
local mode="$1"
74+
echo "==> $mode-path measurements (pilotctl bench)"
75+
for s in $SIZES_MB; do
76+
for i in $(seq 1 "$ITER"); do
77+
printf " %-6s %3d MB iter %d ... " "$mode" "$s" "$i"
78+
read rc dur_ms send_mbps total_mbps < <(time_bench "$mode" "$s")
79+
if [ "$rc" -eq 0 ]; then
80+
printf "%6d ms send=%5s MB/s rt=%5s MB/s\n" "$dur_ms" "$send_mbps" "$total_mbps"
81+
record "$mode" "$s" "$i" "$dur_ms" "$total_mbps"
82+
else
83+
printf "FAIL\n"
84+
record "$mode" "$s" "$i" 0 0
85+
fi
86+
done
87+
done
88+
}
89+
90+
# ---------- Boot ----------
91+
echo "==> bench: starting fresh stack on $PILOT_SUBNET_PREFIX/24"
92+
$DC down -v >/dev/null 2>&1
93+
$DC up -d rendezvous agent-a agent-b >/dev/null 2>&1
94+
for _ in $(seq 1 60); do
95+
COUNT=$($DC exec -T rendezvous curl -fsS http://127.0.0.1:8080/api/stats 2>/dev/null | jq -r '.total_nodes // 0')
96+
[ "${COUNT:-0}" -ge 2 ] && break
97+
sleep 1
98+
done
99+
[ "${COUNT:-0}" -ge 2 ] || { echo "agents did not register" >&2; exit 1; }
100+
101+
IP_A=$(resolve_service_ip agent-a)
102+
IP_B=$(resolve_service_ip agent-b)
103+
[ -n "$IP_A" ] && [ -n "$IP_B" ] || { echo "could not resolve agent IPs" >&2; exit 1; }
104+
105+
# Warm tunnel + a short bench round so cwnd is primed.
106+
$DC exec -T agent-a pilotctl ping agent-b --count 3 --timeout 5s >/dev/null 2>&1 || true
107+
$DC exec -T agent-a pilotctl bench agent-b 1 --timeout 30s >/dev/null 2>&1 || true
108+
109+
# ---------- Direct ----------
110+
run_mode direct
111+
112+
# ---------- Apply partition for relay ----------
113+
echo "==> partitioning a<->b for relay measurements"
114+
apply_partition agent-a "$IP_B"
115+
apply_partition agent-b "$IP_A"
116+
sleep 12
117+
$DC exec -T agent-a pilotctl bench agent-b 1 --timeout 30s >/dev/null 2>&1 || true
118+
119+
run_mode relay
120+
121+
# ---------- Summary ----------
122+
echo
123+
echo "==> raw results (mode size_MB iter dur_ms total_MB/s)"
124+
printf '%s' "$RESULTS"
125+
126+
echo
127+
echo "==> per-(mode,size) statistics across ${ITER} iterations"
128+
printf "%-7s %-8s %-9s %-9s %-9s %-9s\n" "mode" "size_MB" "min_ms" "median" "max_ms" "med_MB/s"
129+
echo "$RESULTS" | awk -F'\t' '
130+
NF>=5 && $4>0 {
131+
key=$1"\t"$2; n[key]++; d[key,n[key]]=$4; m[key,n[key]]=$5
132+
}
133+
END {
134+
for (k in n) {
135+
cnt = n[k]
136+
for (i=1;i<=cnt;i++) ms[i]=d[k,i]+0
137+
for (i=1;i<=cnt;i++) for (j=i+1;j<=cnt;j++)
138+
if (ms[j]<ms[i]) {t=ms[i];ms[i]=ms[j];ms[j]=t}
139+
med_ms = (cnt % 2 == 1) ? ms[(cnt+1)/2] : (ms[cnt/2]+ms[cnt/2+1])/2
140+
for (i=1;i<=cnt;i++) mb[i]=m[k,i]+0
141+
for (i=1;i<=cnt;i++) for (j=i+1;j<=cnt;j++)
142+
if (mb[j]<mb[i]) {t=mb[i];mb[i]=mb[j];mb[j]=t}
143+
med_mbps = (cnt % 2 == 1) ? mb[(cnt+1)/2] : (mb[cnt/2]+mb[cnt/2+1])/2
144+
split(k, kk, "\t")
145+
printf "%-7s %-8s %-9s %-9s %-9s %-9s\n", kk[1], kk[2], ms[1], med_ms, ms[cnt], med_mbps
146+
}
147+
}' | sort -k1,1 -k2,2n
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Bench overlay. Adds NET_ADMIN (so iptables partition rules work to
2+
# simulate the relay-only path) WITHOUT the chaos overlay's 4 MiB
3+
# tmpfs cap on /root/.pilot/received — that cap is meant for
4+
# disk-full tests and would otherwise crash any send-file >4 MiB.
5+
services:
6+
agent-a:
7+
cap_add:
8+
- NET_ADMIN
9+
agent-b:
10+
cap_add:
11+
- NET_ADMIN
12+
rendezvous:
13+
cap_add:
14+
- NET_ADMIN

tests/integration/local/docker-compose.multi3.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ services:
1313
-registry-addr 0.0.0.0:9000
1414
-beacon-addr 0.0.0.0:9001
1515
-http 0.0.0.0:8080
16+
${PILOT_ADMIN_TOKEN:+-admin-token ${PILOT_ADMIN_TOKEN}}
1617
-log-level info
1718
healthcheck:
1819
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/api/stats >/dev/null || exit 1"]
@@ -45,6 +46,7 @@ services:
4546
-listen :4000
4647
-public
4748
-keepalive 5s
49+
${PILOT_ADMIN_TOKEN:+-admin-token ${PILOT_ADMIN_TOKEN}}
4850
-log-level info
4951
5052
agent-b:
@@ -72,6 +74,7 @@ services:
7274
-listen :4000
7375
-public
7476
-keepalive 5s
77+
${PILOT_ADMIN_TOKEN:+-admin-token ${PILOT_ADMIN_TOKEN}}
7578
-log-level info
7679
7780
agent-c:
@@ -99,6 +102,7 @@ services:
99102
-listen :4000
100103
-public
101104
-keepalive 5s
105+
${PILOT_ADMIN_TOKEN:+-admin-token ${PILOT_ADMIN_TOKEN}}
102106
-log-level info
103107
104108
networks:

tests/integration/local/run-all.sh

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,22 @@ QUEUE="$LOGDIR/.queue"
131131
RESULTS="$LOGDIR/.results"
132132
TIMING="$LOGDIR/.timing.tsv"
133133
LOCKDIR="$LOGDIR/.lock.d"
134-
# NAT semaphore: 3 "lanes" — each lane owns a distinct pair of
135-
# non-routable /24s so up to 3 NAT tests can run at the same time.
136-
# Without this, all 12 test_nat_* serialize behind one mutex.
134+
# NAT semaphore: 5 "lanes" — each lane owns a distinct pair of
135+
# non-routable /24s so up to 5 NAT tests can run at the same time.
136+
# Without enough lanes, NAT tests serialize behind a too-small mutex
137+
# and Docker IPAM occasionally refuses overlapping subnet creation
138+
# when lanes get reused before their previous tests fully tore down.
139+
# Bumped from 3 to 5 — eliminates the "compose up failed" / "only 1
140+
# registered" flakes on test_nat_dual_symmetric and friends.
137141
NATLANE0="$LOGDIR/.natlane0.d"
138142
NATLANE1="$LOGDIR/.natlane1.d"
139143
NATLANE2="$LOGDIR/.natlane2.d"
144+
NATLANE3="$LOGDIR/.natlane3.d"
145+
NATLANE4="$LOGDIR/.natlane4.d"
140146
printf '%s\n' "$TESTS" >"$QUEUE"
141147
: >"$RESULTS"
142148
: >"$TIMING"
143-
rmdir "$LOCKDIR" "$NATLANE0" "$NATLANE1" "$NATLANE2" 2>/dev/null || true
149+
rmdir "$LOCKDIR" "$NATLANE0" "$NATLANE1" "$NATLANE2" "$NATLANE3" "$NATLANE4" 2>/dev/null || true
144150

145151
# Portable mutex (macOS lacks flock). mkdir is atomic on POSIX fs.
146152
acquire_lock() {
@@ -157,12 +163,14 @@ release_lock() {
157163
# caller via NAT_PUB / NAT_PRV env before running the test.
158164
acquire_nat_lane() {
159165
while true; do
160-
for lane in 0 1 2; do
166+
for lane in 0 1 2 3 4; do
161167
local dir
162168
case "$lane" in
163169
0) dir="$NATLANE0" ;;
164170
1) dir="$NATLANE1" ;;
165171
2) dir="$NATLANE2" ;;
172+
3) dir="$NATLANE3" ;;
173+
4) dir="$NATLANE4" ;;
166174
esac
167175
if mkdir "$dir" 2>/dev/null; then
168176
echo "$lane"
@@ -178,12 +186,22 @@ release_nat_lane() {
178186
0) rmdir "$NATLANE0" 2>/dev/null ;;
179187
1) rmdir "$NATLANE1" 2>/dev/null ;;
180188
2) rmdir "$NATLANE2" 2>/dev/null ;;
189+
3) rmdir "$NATLANE3" 2>/dev/null ;;
190+
4) rmdir "$NATLANE4" 2>/dev/null ;;
181191
esac
182192
}
183193

184-
echo "==> $NTESTS tests, $JOBS parallel workers, 3 NAT lanes"
194+
echo "==> $NTESTS tests, $JOBS parallel workers, 5 NAT lanes"
185195
RUN_START=$(date +%s)
186196

197+
# Pre-pull images that the webhook overlay depends on. Without this,
198+
# 8 parallel workers race to pull `python:3.12-slim` on first use,
199+
# stretching the cold-start past the webhook-sink healthcheck budget
200+
# and producing 4-5 spurious failures per run. Single-shot pull here
201+
# warms the local image cache so every test starts the container
202+
# instantly.
203+
docker pull -q python:3.12-slim >/dev/null 2>&1 || true
204+
187205
# Worker: pops tests one at a time from $QUEUE under lock. Each test
188206
# runs with a worker-scoped compose project + subnet so parallel workers
189207
# don't fight over container names or IPs.
@@ -224,6 +242,8 @@ worker() {
224242
0) export NAT_PUB="192.0.2"; export NAT_PRV="198.51.100" ;;
225243
1) export NAT_PUB="198.18.0"; export NAT_PRV="198.18.1" ;;
226244
2) export NAT_PUB="198.18.2"; export NAT_PRV="198.18.3" ;;
245+
3) export NAT_PUB="198.18.4"; export NAT_PRV="198.18.5" ;;
246+
4) export NAT_PUB="198.18.6"; export NAT_PRV="198.18.7" ;;
227247
esac
228248
# nat-gw.sh reads these names; export both spellings for compat.
229249
export PUBLIC_SUBNET="$NAT_PUB"

0 commit comments

Comments
 (0)