@@ -77,6 +77,10 @@ STREAM_MB=8
7777# the 180 s summary wait and the 300 s remote probe budget.
7878STREAM_DURATION=120
7979KERNEL_NIC_IP=" "
80+ # Runtime fault injection (FF_FAULT). Empty = production form; a non-empty name
81+ # requires a fault-injection build manifest (checked by reload_checks.verify-build).
82+ FAULT=" "
83+ FAULT_DELAY_MS=15000
8084ZC_BUILD=auto
8185RTE_FRESH_MIN=10
8286# KNI owner must be a *secondary* proc_id, never the resident primary (0):
@@ -139,7 +143,11 @@ Usage: test_graceful_reload.sh -t <TARGET_IP> [options]
139143 test (write <DPDK_NIC_IP> in any report).
140144 -c, --cases <list> Comma-separated case list, or 'all'.
141145 available: precheck,baseline,rt01,rt02,rv9,gr0,
142- rt12,rt13
146+ rt12,rt13,rt20,rt20b,rt21,rt22,rt23
147+ --fault <name> runtime fault injection (FF_FAULT); requires a
148+ fault-injection build manifest. rt23 is the only
149+ fault case that runs on the production form
150+ --fault-delay-ms <n> FF_FAULT_DELAY_MS for ready_delay (1..59000)
143151 default : precheck,rt01,rv9
144152 -r, --rounds <n> reload-loop rounds for rv9 (default 100)
145153 -i, --interval <s> reload-loop cadence in seconds (default 15)
@@ -213,6 +221,8 @@ while [ $# -gt 0 ]; do
213221 --baseline-duration) BASELINE_DURATION=" ${2:- } " ; shift 2 ;;
214222 --stream-mb) STREAM_MB=" ${2:- } " ; shift 2 ;;
215223 --kernel-nic-ip) KERNEL_NIC_IP=" ${2:- } " ; shift 2 ;;
224+ --fault) FAULT=" ${2:- } " ; shift 2 ;;
225+ --fault-delay-ms) FAULT_DELAY_MS=" ${2:- } " ; shift 2 ;;
216226 --zc-build) ZC_BUILD=" ${2:- } " ; shift 2 ;;
217227 --rte-fresh-min) RTE_FRESH_MIN=" ${2:- } " ; shift 2 ;;
218228 -h|--help) usage; exit 0 ;;
226236local n
227237local -a values=()
228238for n in TARGET_IP CLIENT CASES ROUNDS INTERVAL POLL WORKERS DRAIN_TIMEOUT STARTUP_WAIT \
229- STREAM_MB RTE_FRESH_MIN SHUTDOWN_TIMEOUT BASELINE_DURATION GRACEFUL ZC_BUILD \
239+ STREAM_MB RTE_FRESH_MIN SHUTDOWN_TIMEOUT BASELINE_DURATION GRACEFUL ZC_BUILD FAULT \
240+ FAULT_DELAY_MS \
230241 NGINX_BIN FSTACK_TPL PROBE_DIR OUT BUILD_MANIFEST KERNEL_NIC_IP LCORE_MASK LCORE_LIST; do
231242 values+=(" $n =${! n} " )
232243done
@@ -809,6 +820,187 @@ do_hup_case() { # tag graceful shutdown_timeout probe-kind(none|stream|lc|cps)
809820 return 0
810821}
811822
823+ # ---- fault-injection cases (F1) ------------------------------------------
824+ # These need a fault-injection build (FF_RELOAD_FAULT_INJECTION=1) and a
825+ # manifest that declares it; reload_checks.verify-build enforces the pairing in
826+ # both directions. They never contribute to functional acceptance: rt23 is the
827+ # only one that runs on the production form.
828+ fault_case () { # tag fault expect(ok|abort) criterion [abort-signature]
829+ local tag=" $1 " fault=" $2 " expect=" $3 " crit=" $4 " sig=" ${5:- } "
830+ local rc=0 conf out before
831+ # The --fault option drives the build-form gate; it must name the same
832+ # fault the case injects, otherwise a verdict could be labelled wrongly.
833+ if [ " $FAULT " != " $fault " ]; then
834+ say " $tag : --fault=$FAULT does not match the case fault $fault "
835+ record " $tag " " FAIL" " $crit " " fault option/case mismatch ($FAULT vs $fault )"
836+ return 1
837+ fi
838+ export FF_FAULT=" $fault "
839+ if [ " $fault " = " ready_delay" ]; then
840+ export FF_FAULT_DELAY_MS=" $FAULT_DELAY_MS "
841+ else
842+ unset FF_FAULT_DELAY_MS
843+ fi
844+ conf=$( gen_nginx_conf " $tag " 0)
845+ push_probes || { unset FF_FAULT; unset FF_FAULT_DELAY_MS; return 1; }
846+ if ! start_stack " $tag " 1 0; then
847+ stop_stack " $tag " " $conf " || rc=1
848+ unset FF_FAULT
849+ record " $tag " " FAIL" " $crit " " start failed (fault=$fault )"
850+ return 1
851+ fi
852+ before=$( worker_count)
853+ if have_probe m4_lc.py; then
854+ launch_probe " $tag " 120 m4_lc.py --server " $TARGET_IP " --conns 12 \
855+ --interval 0.1 --duration 45 --fresh 0.5 --timeout 2 \
856+ || { unset FF_FAULT; unset FF_FAULT_DELAY_MS; stop_stack " $tag " " $conf " ; record " $tag " " FAIL" " $crit " " probe launch failed" ; return 1; }
857+ sleep 3
858+ fi
859+ probe_running || rc=1
860+ local hrc=0
861+ if [ " $expect " = abort ]; then
862+ # An aborted reload never prints the completion line, so waiting for it
863+ # would only burn the drain timeout. Wait for the abort signature
864+ # instead, bounded by the READY/park budget plus a margin.
865+ nginx_signal " $conf " reload || hrc=1
866+ local deadline=$(( SECONDS + 90 )) found=0
867+ while [ " $SECONDS " -lt " $deadline " ]; do
868+ if [ -n " $sig " ]; then
869+ if grep -q " graceful reload aborted: $sig " " $ERRLOG " ; then found=1; break ; fi
870+ elif grep -q " graceful reload aborted" " $ERRLOG " ; then
871+ found=1; break
872+ fi
873+ sleep 1
874+ done
875+ if [ " $found " != " 1" ]; then
876+ say " $tag : no bounded abort within 90 s (errlog: $ERRLOG )"
877+ hrc=1
878+ fi
879+ else
880+ hup_once " $conf " || hrc=1
881+ fi
882+ # hrc == 0 means the expected event happened: the bounded abort signature
883+ # for expect=abort, the completion line for expect=ok.
884+ [ " $hrc " = " 0" ] || { say " $tag : expect=$expect not satisfied (rc=$hrc )" ; rc=1; }
885+ # NB: no probe_running check here -- by the time the bounded abort/completion
886+ # is observed the probe (45 s) has normally finished; the probe's own
887+ # summary below is the evidence that G_old kept serving, not a liveness bit.
888+ # G_old must have kept serving: the probe's own verdict must be clean.
889+ local summary=" no probe" fetch=0
890+ if ! have_probe m4_lc.py; then
891+ # No probe means no evidence that G_old kept serving: never a silent
892+ # pass (same rule as the other cases in this harness).
893+ unset FF_FAULT
894+ unset FF_FAULT_DELAY_MS
895+ stop_stack " $tag " " $conf " || rc=1
896+ record " $tag " " SKIP" " $crit " " NO_DATA (m4_lc.py absent from $PROBE_DIR )"
897+ return 0
898+ fi
899+ if have_probe m4_lc.py; then
900+ summary=$( wait_client_summary /tmp/gr_${tag} _lc_out.log ' LC_SUMMARY' 120) || fetch=$?
901+ if [ " $fetch " = " 1" ]; then
902+ summary=" NO_DATA (m4_lc.py did not report within 120 s)" ; rc=1
903+ elif [ " $fetch " = " 2" ]; then
904+ say " $tag : probe reported a summary but failed its own criterion" ; rc=1
905+ fi
906+ check_summary lc " $summary " || { say " $tag : lc verdict below target: $summary " ; rc=1; }
907+ fi
908+ # no double master and no lost generation: the count must be back to
909+ # exactly the pre-reload set.
910+ if [ " $( worker_count) " -ne " $before " ]; then
911+ say " $tag : worker count changed ($before -> $( worker_count) )"
912+ rc=1
913+ fi
914+ unset FF_FAULT
915+ unset FF_FAULT_DELAY_MS
916+ stop_stack " $tag " " $conf " || rc=1
917+ if [ " $rc " = " 0" ]; then
918+ record " $tag " " PASS" " $crit " \
919+ " fault=$fault expect=$expect hrc=$hrc workers_before=$before traffic=$summary (fault-injection build)"
920+ else
921+ record " $tag " " FAIL" " $crit " \
922+ " fault=$fault expect=$expect hrc=$hrc workers_before=$before traffic=$summary (fault-injection build)"
923+ fi
924+ return $rc
925+ }
926+
927+ case_rt20 () {
928+ say " === case rt20 (READY never arrives -> bounded abort) ==="
929+ fault_case " rt20" ready_never abort \
930+ " READY wait times out within NGX_FF_RELOAD_READY_WAIT_SEC (60s); reload aborts to T0_IDLE; G_old keeps serving; no second master" \
931+ " READY wait timed out"
932+ }
933+ case_rt20b () {
934+ say " === case rt20b (READY late but reachable -> completes) ==="
935+ fault_case " rt20b" ready_delay ok \
936+ " READY delayed by FF_FAULT_DELAY_MS (<60s) still completes: 6/6 FSM, workers back to N, service restored"
937+ }
938+ case_rt21 () {
939+ say " === case rt21 (handover flip fails -> T2/T_ERROR/T0) ==="
940+ fault_case " rt21" flip_fail abort \
941+ " T2 -> T_ERROR -> T0 with 'rx ownership flip failed'; G_old keeps serving; no half-handover" \
942+ " rx ownership flip failed"
943+ }
944+ case_rt22 () {
945+ say " === case rt22 (park never confirmed -> bounded abort) ==="
946+ fault_case " rt22" park_never abort \
947+ " park budget (FF_RELOAD_HANDOVER_TIMEOUT_MS_DEFAULT 100U) expires: T_ERROR -> T0 with 'G_old park confirmation timed out'" \
948+ " G_old park confirmation timed out"
949+ }
950+ case_rt23 () {
951+ say " === case rt23 (reload re-entry during drain is refused) ==="
952+ local rc=0 conf out
953+ # rt23 is the only fault-matrix case that runs on the PRODUCTION form:
954+ # --fault/BUILD_MANIFEST are per-run globals, so refuse the mixed form and
955+ # drop any inherited fault variables.
956+ if [ -n " $FAULT " ]; then
957+ say " rt23: requires the production form (--fault=$FAULT )"
958+ record " rt23" " FAIL" " second HUP during T3 is refused; first reload still completes" \
959+ " fault form not allowed for rt23"
960+ return 1
961+ fi
962+ unset FF_FAULT
963+ unset FF_FAULT_DELAY_MS
964+ conf=$( gen_nginx_conf " rt23" 0)
965+ prep_stream_payload || return 1
966+ push_probes || return 1
967+ if ! start_stack " rt23" 1 0; then
968+ stop_stack " rt23" " $conf " || rc=1
969+ record " rt23" " FAIL" " second HUP during T3 is refused; first reload still completes" " start failed"
970+ return 1
971+ fi
972+ launch_probe rt23 300 m4_stream.py --server " $TARGET_IP " \
973+ --path /dl/big.bin --streams 12 --chunk 16384 --gap 0.1 \
974+ --timeout 5 --stall 3.0 --duration " $STREAM_DURATION " \
975+ --expect-md5 " $STREAM_MD5 " || return 1
976+ sleep 3
977+ probe_running || rc=1
978+ # The second HUP must land WHILE the first one is draining (T3), not after
979+ # it finished, otherwise it simply starts a second reload.
980+ nginx_signal " $conf " reload || rc=1
981+ sleep 5
982+ nginx_signal " $conf " reload || rc=1
983+ local deadline=$(( SECONDS + 150 )) done=0
984+ while [ " $SECONDS " -lt " $deadline " ]; do
985+ if grep -q " graceful reload complete" " $ERRLOG " ; then done=1; break ; fi
986+ sleep 1
987+ done
988+ [ " $done " = " 1" ] || { say " rt23: first reload did not complete within 150 s" ; rc=1; }
989+ grep -q " graceful reload rejected: previous reload still in progress" " $ERRLOG " \
990+ || { say " rt23: re-entry refusal not found in $ERRLOG " ; rc=1; }
991+ HUP_SUMMARY=$( wait_client_summary /tmp/gr_rt23_stream.log ' STREAM_SUMMARY' 120) \
992+ || HUP_SUMMARY=" NO_DATA (m4_stream.py did not report within 120 s)"
993+ stop_stack " rt23" " $conf " || rc=1
994+ if [ " $rc " = " 0" ]; then
995+ record " rt23" " PASS" " second HUP during T3 is refused; first reload still completes" \
996+ " fsm=$HUP_FSM /6 drain=${HUP_DRAIN} ms traffic=$HUP_SUMMARY "
997+ else
998+ record " rt23" " FAIL" " second HUP during T3 is refused; first reload still completes" \
999+ " fsm=$HUP_FSM /6 drain=${HUP_DRAIN} ms traffic=$HUP_SUMMARY "
1000+ fi
1001+ return $rc
1002+ }
1003+
8121004case_rt01 () {
8131005 say " === case rt01 (unloaded HUP) ==="
8141006 local rc=0
@@ -1250,7 +1442,7 @@ main() {
12501442 [ " $( " $KILLTOOL " --capabilities) " = pidfd-identity-v1 ] || return 4
12511443 zc_probe_selftest || return 4
12521444 run_case precheck || { print_summary; return 3; }
1253- for t in baseline rt01 rt02 rv9 gr0 rt12 rt13; do
1445+ for t in baseline rt01 rt02 rv9 gr0 rt12 rt13 rt20 rt20b rt21 rt22 rt23 ; do
12541446 need_case " $t " || continue
12551447 run_case " $t "
12561448 [ " $CLEANUP_FAILED " = 0 ] || break
0 commit comments