paho reconnects on its own but does not restore subscriptions. A client without an on_connect re-subscribe holds a live socket with no subscription and goes silently deaf -- no error, no log. That is how voice_bridge died on 2026-10-13. All in-scope .13 clients already re-subscribe on every connect and log it (voice_bridge, Jervis voice-agent, IUC control-service, IUC kids Android app). The Hub has no MQTT client, n8n nodes are publish-only, Home Assistant re-subscribes by design, and the ESP32 firmware is owned by Directive 3. Add: - docs/mqtt-reconnect-compliance.md: inventory with file:line evidence and the live verification result. - verify/mqtt_reconnect_check.sh: static source check plus an opt-in --live check that restarts the .13 broker and asserts re-subscribe within 60s. Live check 2026-10-14T00:01:18Z: .13 mosquitto restarted; voice_bridge and control-service re-subscribed and ESP32 reconnected within 3s. Co-Authored-By: Paperclip <noreply@paperclip.ing>
115 lines
4.6 KiB
Bash
Executable File
115 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Directive 2 — MQTT reconnect rule verifier (Hub Fixes).
|
|
#
|
|
# paho reconnects on its own but does NOT restore subscriptions. A client that
|
|
# has no on_connect re-subscribe handler holds a live socket with no
|
|
# subscription and goes silently deaf — no error, no log. See
|
|
# docs/mqtt-reconnect-compliance.md.
|
|
#
|
|
# Usage:
|
|
# verify/mqtt_reconnect_check.sh # static source check (safe)
|
|
# verify/mqtt_reconnect_check.sh --live # restart the .13 broker and
|
|
# # assert re-subscribe within 60s
|
|
#
|
|
# The --live run restarts the shared .13 mosquitto container. It briefly
|
|
# interrupts voice intake and the kids' device control plane. Run it at a
|
|
# low-impact moment. Every docker call is bounded with `timeout`.
|
|
set -u
|
|
|
|
BROKER_CONTAINER="${BROKER_CONTAINER:-mosquitto}"
|
|
VOICE_BRIDGE_CONTAINER="${VOICE_BRIDGE_CONTAINER:-voice_bridge}"
|
|
CONTROL_CONTAINER="${CONTROL_CONTAINER:-webrestrict-control}"
|
|
WINDOW="${WINDOW:-60}" # seconds allowed for re-subscribe
|
|
DOCKER_TIMEOUT="${DOCKER_TIMEOUT:-20}"
|
|
|
|
LIVE=0
|
|
[ "${1:-}" = "--live" ] && LIVE=1
|
|
|
|
# Client sources: name|path|on_connect pattern|subscribe pattern|log pattern
|
|
CLIENTS=(
|
|
"voice_bridge |/home/sam/voice_bridge/mqtt_audio_bridge.py|_on_voice_connect|client\\.subscribe\\(\\[\\(VOICE_TOPIC_AUDIO|\\[bridge\\] subscribed"
|
|
"voice-agent |/home/sam/voice-agent/app.py|def on_connect|client\\.subscribe\\(\\[\\(topic|\\[mqtt\\] subscribed"
|
|
"control-svc |/home/sam/paseo/projects/internet_use_control/internal/mqtt/link.go|SetOnConnectHandler|c\\.Subscribe\\(sub\\.topic|mqtt subscribed"
|
|
"kids-android |/home/sam/paseo/projects/internet_use_control/android/app/src/main/java/au/audasmedia/webrestrict/kids/MqttController.kt|connectComplete|isAutomaticReconnect|c\\.subscribe\\(prefs\\.deviceTopic|subscribed to"
|
|
)
|
|
|
|
status() { printf '%-14s %s\n' "$1" "$2"; }
|
|
|
|
static_check() {
|
|
echo "== static check: on_connect re-subscribe + log =="
|
|
local row name path onc sub log rc=0
|
|
for row in "${CLIENTS[@]}"; do
|
|
IFS='|' read -r name path onc sub log <<<"$row"
|
|
name="$(echo "$name" | xargs)"
|
|
if [ ! -f "$path" ]; then
|
|
status "$name" "SKIP (source not on this host: $path)"
|
|
continue
|
|
fi
|
|
local ok="ok"
|
|
grep -qE "$onc" "$path" || ok="MISSING on_connect"
|
|
grep -qE "$sub" "$path" || ok="MISSING subscribe"
|
|
grep -qE "$log" "$path" || ok="MISSING log"
|
|
if [ "$ok" = "ok" ]; then
|
|
status "$name" "OK $path"
|
|
else
|
|
status "$name" "FAIL $ok ($path)"
|
|
rc=1
|
|
fi
|
|
done
|
|
return $rc
|
|
}
|
|
|
|
broker_ready() {
|
|
timeout "$DOCKER_TIMEOUT" docker exec "$BROKER_CONTAINER" \
|
|
mosquitto_sub -h localhost -p 1883 -t '$SYS/broker/uptime' -C 1 -W 3 \
|
|
>/dev/null 2>&1
|
|
}
|
|
|
|
live_check() {
|
|
local t0 epoch i ready=0
|
|
t0="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
epoch="$(date +%s)"
|
|
echo "== live check: restart $BROKER_CONTAINER at $t0 =="
|
|
|
|
timeout $((DOCKER_TIMEOUT + 20)) docker restart "$BROKER_CONTAINER" || {
|
|
echo "FAIL: could not restart $BROKER_CONTAINER"; return 1; }
|
|
|
|
for i in $(seq 1 15); do
|
|
if broker_ready; then ready=1; echo "broker back after ~$((i * 2))s"; break; fi
|
|
sleep 2
|
|
done
|
|
[ "$ready" = 1 ] || { echo "FAIL: broker did not come back within 30s"; return 1; }
|
|
|
|
local deadline=$(( $(date +%s) + WINDOW ))
|
|
local bridge=0 control=0 esp=0
|
|
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
[ "$bridge" = 0 ] && timeout "$DOCKER_TIMEOUT" docker logs "$VOICE_BRIDGE_CONTAINER" --since "$t0" 2>&1 \
|
|
| grep -q '\[bridge\] subscribed' && bridge=1
|
|
[ "$control" = 0 ] && timeout "$DOCKER_TIMEOUT" docker logs "$CONTROL_CONTAINER" --since "$t0" 2>&1 \
|
|
| grep -q 'mqtt subscribed' && control=1
|
|
[ "$esp" = 0 ] && timeout "$DOCKER_TIMEOUT" docker exec "$BROKER_CONTAINER" sh -c \
|
|
"awk -v t=$epoch -F: '/New client connected/ {if (\$1+0>=t) print}' /mosquitto/log/mosquitto.log" 2>/dev/null \
|
|
| grep -q 'ESP32Mic' && esp=1
|
|
[ "$((bridge + control + esp))" = 3 ] && break
|
|
sleep 2
|
|
done
|
|
|
|
local elapsed=$(( $(date +%s) - epoch ))
|
|
status "voice_bridge" "$([ "$bridge" = 1 ] && echo "OK re-subscribed" || echo "FAIL no re-subscribe")"
|
|
status "control-svc" "$([ "$control" = 1 ] && echo "OK re-subscribed" || echo "FAIL no re-subscribe")"
|
|
status "ESP32Mic" "$([ "$esp" = 1 ] && echo "OK reconnected (re-subscribe is Directive 3)" || echo "FAIL no reconnect")"
|
|
echo "elapsed: ${elapsed}s (window ${WINDOW}s)"
|
|
[ "$((bridge + control + esp))" = 3 ]
|
|
}
|
|
|
|
rc=0
|
|
static_check || rc=1
|
|
if [ "$LIVE" = 1 ]; then
|
|
echo
|
|
live_check || rc=1
|
|
fi
|
|
|
|
if [ "$rc" = 0 ]; then echo "RESULT: PASS"; else echo "RESULT: FAIL"; fi
|
|
exit "$rc"
|