mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
wait_for_peers reads the connected-peer count through a pipeline ending in `|| echo 0`, so a container that never answers contributes 0. That is safe against a floor of 1 or more, where 0 reads as "not converged yet" and the wait eventually times out, and it is unsafe against a floor of 0, where the first read from a dead container satisfies the wait and the caller proceeds as though convergence had been observed. No caller passes 0 today; every one passes 1 or more, and the single variable minimum is advisory. Rather than harden the reader, reject the input: a floor below 1 is now an error. That makes the shape unreachable instead of repairing instances of it, and it costs a future caller nothing except a clear message saying that asserting "exactly zero peers" needs a reader that distinguishes no answer from zero, which this floor is not. wait_for_links goes with it. It had no caller anywhere in the tree on any branch, and it carried the identical fallback, so the only thing it could do was hand the hazard to whoever called it first. An uncalled helper cannot be wrong today, which is exactly why it was the risk worth removing rather than the one worth keeping for symmetry. The hermetic gate suite gains a case covering both directions, with docker stubbed so it stays container-free: a floor of 0 is refused with a message naming why, and the same unreachable container with a floor of 1 still polls its full budget and times out. Verified by removing the guard and confirming exactly the three zero-floor assertions red while both floor-of-one controls stay green.
128 lines
5.7 KiB
Bash
128 lines
5.7 KiB
Bash
#!/bin/bash
|
|
# Shared convergence wait helpers for FIPS integration tests.
|
|
#
|
|
# Source this file to get wait_for_peers() and wait_until_connected().
|
|
#
|
|
# Usage:
|
|
# source "$(dirname "$0")/../../lib/wait-converge.sh"
|
|
# wait_for_peers <container> <min_peers> [timeout_secs]
|
|
# wait_until_connected <ping_fn> <max_secs> <stall_secs> [poll_secs] \
|
|
# [near_converged_slack]
|
|
#
|
|
# There was a wait_for_links() here. It was removed rather than kept for
|
|
# symmetry: it had no caller anywhere in the tree on any branch, and its
|
|
# reader carried the same failure-to-zero fallback wait_for_peers does. An
|
|
# uncalled helper cannot be wrong today, so the risk was that the first
|
|
# caller to appear would inherit the hazard below without the reasoning
|
|
# that goes with it. `git log` has the implementation if one is needed.
|
|
|
|
# Wait until a container has at least min_peers connected peers.
|
|
# Returns 0 on success, 1 on timeout.
|
|
#
|
|
# The read below falls back to 0 when the container does not answer, which
|
|
# is safe ONLY because this is a floor: a fallback of 0 reads as "not
|
|
# converged yet", the loop keeps polling, and a container that never answers
|
|
# times out and returns 1. That safety is a property of the comparison, not
|
|
# of the reader.
|
|
#
|
|
# A minimum of 0 inverts it. `[ 0 -ge 0 ]` is true, so the first read from a
|
|
# dead container would satisfy the wait immediately and the caller would
|
|
# proceed as though convergence had been observed. No caller passes 0, and
|
|
# rejecting it here means none can start to — which is cheaper than auditing
|
|
# every future caller, and is why this is a hard error rather than a warning.
|
|
# A caller that genuinely wants to assert "exactly zero peers" needs a reader
|
|
# that distinguishes no-answer from zero, not this floor.
|
|
wait_for_peers() {
|
|
local container="$1"
|
|
local min_peers="$2"
|
|
local timeout="${3:-30}"
|
|
|
|
if [ "$min_peers" -lt 1 ]; then
|
|
echo " wait_for_peers: refusing a minimum of $min_peers for $container — a floor of 0 is satisfied by a container that never answered" >&2
|
|
return 2
|
|
fi
|
|
|
|
for i in $(seq 1 "$timeout"); do
|
|
local count
|
|
count=$(docker exec "$container" fipsctl show peers 2>/dev/null \
|
|
| python3 -c "import sys,json; print(sum(1 for p in json.load(sys.stdin).get('peers',[]) if p.get('connectivity')=='connected'))" 2>/dev/null || echo 0)
|
|
if [ "$count" -ge "$min_peers" ]; then
|
|
echo " $container: $count peer(s) after ${i}s"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo " $container: TIMEOUT waiting for $min_peers peer(s) after ${timeout}s"
|
|
return 1
|
|
}
|
|
|
|
# Wait until a connectivity check reports every pair reachable, using a
|
|
# progress-aware deadline instead of a fixed one.
|
|
#
|
|
# wait_until_connected <ping_fn> <max_secs> <stall_secs> [poll_secs] \
|
|
# [near_converged_slack]
|
|
#
|
|
# <ping_fn> is the name of a function that runs the suite's own
|
|
# connectivity check and sets two globals each call:
|
|
# PASSED number of reachable pairs this round
|
|
# FAILED number of unreachable pairs this round
|
|
#
|
|
# The convergence signal is the suite's real pings (the same signal it
|
|
# asserts on), not a structural proxy. Behaviour:
|
|
# - converged: FAILED == 0 -> return 0.
|
|
# - progressing: PASSED climbed past the best seen -> reset the stall
|
|
# clock and keep waiting (slow-but-improving is not a failure, so it
|
|
# does not false-time-out under CI load).
|
|
# - stuck: PASSED has not improved for stall_secs -> return 1 (fail
|
|
# fast rather than burn the whole budget on a genuinely wedged pair),
|
|
# BUT only when FAILED > near_converged_slack (default 2). A mesh
|
|
# that is genuinely far from convergence still bails fast on stall.
|
|
# - near-converged hold: when FAILED <= near_converged_slack and the
|
|
# stall window has elapsed, do NOT bail. A handful of straggling
|
|
# pairs (e.g. a deep node whose last pair clears only after stacked
|
|
# discovery backoff + late bloom propagation) is a rare timing event,
|
|
# not a routing defect, so the gate keeps polling toward max_secs
|
|
# rather than emitting a false RED with budget still unspent. A
|
|
# genuinely never-converging single pair still hits the hard cap.
|
|
# - hard cap: max_secs elapsed -> return 1 (never runs unbounded).
|
|
#
|
|
# Returns 0 once fully connected, 1 on stall or timeout.
|
|
wait_until_connected() {
|
|
local ping_fn="$1"
|
|
local max_secs="$2"
|
|
local stall_secs="$3"
|
|
local poll_secs="${4:-1}"
|
|
local near_converged_slack="${5:-2}"
|
|
|
|
local start_secs=$SECONDS
|
|
local best=-1
|
|
local last_progress=$SECONDS
|
|
local held_for_budget=0
|
|
|
|
while (( SECONDS - start_secs < max_secs )); do
|
|
"$ping_fn"
|
|
if (( FAILED == 0 )); then
|
|
echo " converge: all $PASSED pair(s) reachable after $((SECONDS - start_secs))s"
|
|
return 0
|
|
fi
|
|
if (( PASSED > best )); then
|
|
best=$PASSED
|
|
last_progress=$SECONDS
|
|
echo " converge: $PASSED reachable, $FAILED pending (progressing) after $((SECONDS - start_secs))s"
|
|
elif (( SECONDS - last_progress >= stall_secs )); then
|
|
if (( FAILED > near_converged_slack )); then
|
|
echo " converge: STUCK at $PASSED reachable / $FAILED pending — no progress for ${stall_secs}s (after $((SECONDS - start_secs))s)"
|
|
return 1
|
|
fi
|
|
if (( held_for_budget == 0 )); then
|
|
held_for_budget=1
|
|
echo " converge: near-converged ($PASSED reachable / $FAILED pending <= slack=$near_converged_slack) — holding for full budget, not bailing (after $((SECONDS - start_secs))s)"
|
|
fi
|
|
fi
|
|
sleep "$poll_secs"
|
|
done
|
|
|
|
echo " converge: TIMEOUT at $PASSED reachable / $FAILED pending after ${max_secs}s"
|
|
return 1
|
|
}
|