mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 11:36:15 +00:00
Make a zero-peer floor unreachable in the shared convergence wait
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.
This commit is contained in:
@@ -180,6 +180,47 @@ check "case4: returns 0 with 4 args" "$c4_rc_ok" "rc=$rc"
|
||||
c4_hold_ok=1; echo "$out" | grep -q "$HOLD_MSG" && c4_hold_ok=0
|
||||
check "case4: default slack triggered near-converged hold" "$c4_hold_ok"
|
||||
|
||||
# --- Case 5: wait_for_peers refuses a floor of zero -------------------
|
||||
#
|
||||
# The reader inside wait_for_peers falls back to 0 when a container does
|
||||
# not answer. That is safe against a floor of 1 or more, where 0 reads as
|
||||
# "not converged yet", and unsafe against a floor of 0, where the first
|
||||
# read from a dead container satisfies the wait immediately. This case is
|
||||
# the break-what-it-guards check for the rejection: drive the guard with a
|
||||
# floor of 0 and confirm it refuses, then drive the same dead container
|
||||
# with a floor of 1 and confirm the refusal is specific to the dangerous
|
||||
# input rather than a blanket failure.
|
||||
#
|
||||
# `docker` is stubbed to fail, which is what an unreachable container looks
|
||||
# like to this reader, so no container or network is involved and the suite
|
||||
# stays hermetic.
|
||||
echo
|
||||
echo "== Case 5: wait_for_peers refuses a zero floor =="
|
||||
docker() { return 1; }
|
||||
|
||||
out=$(wait_for_peers stub-container 0 2 2>&1); rc=$?
|
||||
echo "$out"
|
||||
c5_reject_ok=1; [ "$rc" -eq 2 ] && c5_reject_ok=0
|
||||
check "case5: floor of 0 is refused" "$c5_reject_ok" "rc=$rc"
|
||||
c5_msg_ok=1; echo "$out" | grep -q "refusing a minimum" && c5_msg_ok=0
|
||||
check "case5: refusal names the reason" "$c5_msg_ok"
|
||||
# The pre-guard behaviour, asserted so a regression is visible rather than
|
||||
# quiet: without the rejection this returned 0 on its first iteration
|
||||
# against a container that never answered.
|
||||
c5_notpass_ok=1; [ "$rc" -ne 0 ] && c5_notpass_ok=0
|
||||
check "case5: floor of 0 does not report success" "$c5_notpass_ok" "rc=$rc"
|
||||
|
||||
start=$SECONDS
|
||||
out=$(wait_for_peers stub-container 1 2 2>&1); rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
echo "$out"
|
||||
c5_floor1_ok=1; [ "$rc" -eq 1 ] && c5_floor1_ok=0
|
||||
check "case5: floor of 1 times out rather than being refused" "$c5_floor1_ok" "rc=$rc"
|
||||
c5_polled_ok=1; [ "$elapsed" -ge 2 ] && c5_polled_ok=0
|
||||
check "case5: floor of 1 polled its full budget" "$c5_polled_ok" "elapsed=${elapsed}s >= 2s"
|
||||
|
||||
unset -f docker
|
||||
|
||||
# --- Summary ----------------------------------------------------------
|
||||
echo
|
||||
echo "=============================================="
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Shared convergence wait helpers for FIPS integration tests.
|
||||
#
|
||||
# Source this file to get wait_for_links(), wait_for_peers(), and
|
||||
# wait_until_connected().
|
||||
# Source this file to get wait_for_peers() and wait_until_connected().
|
||||
#
|
||||
# Usage:
|
||||
# source "$(dirname "$0")/../../lib/wait-converge.sh"
|
||||
# wait_for_links <container> <min_links> [timeout_secs]
|
||||
# wait_for_peers <container> <min_peers> [timeout_secs]
|
||||
# wait_until_connected <ping_fn> <max_secs> <stall_secs> [poll_secs] \
|
||||
# [near_converged_slack]
|
||||
|
||||
# Wait until a container has at least min_links active links.
|
||||
# Returns 0 on success, 1 on timeout.
|
||||
wait_for_links() {
|
||||
local container="$1"
|
||||
local min_links="$2"
|
||||
local timeout="${3:-30}"
|
||||
|
||||
for i in $(seq 1 "$timeout"); do
|
||||
local count
|
||||
count=$(docker exec "$container" fipsctl show links 2>/dev/null \
|
||||
| python3 -c "import sys,json; print(len(json.load(sys.stdin).get('links',[])))" 2>/dev/null || echo 0)
|
||||
if [ "$count" -ge "$min_links" ]; then
|
||||
echo " $container: $count link(s) after ${i}s"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo " $container: TIMEOUT waiting for $min_links link(s) after ${timeout}s"
|
||||
return 1
|
||||
}
|
||||
#
|
||||
# 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 \
|
||||
|
||||
Reference in New Issue
Block a user