2.1: restored bridge session is expired at startup, before bridges are started
Version
Reproduced on eclipse-mosquitto:2.1.2 (also 2.1.0/2.1.1). Not present on the 2.0.x series.
Summary
A broker configured as an outgoing bridge with cleansession false and persistence true will, on restart, expire its own restored bridge session at startup if the persisted session's expiry time has already passed — which happens once the uplink has been down longer than persistent_client_expiration. At startup the log shows:
Restored N client messages
Expiring client local.<bridge> due to timeout.Expiring the bridge session is wrong — outgoing bridge sessions are meant never to expire (see the context->bridge == NULL /* Outgoing bridge connection never expire */ guard in context__disconnect). The consequences are (1) the bridge's persisted offline queue is silently discarded, and (2) in production, with real accumulated broker state, the bridge connects but stops forwarding outbound messages until the next restart.
Root cause
2.1 added a session_expiry__check() call to broker startup that runs before bridges are started:
src/mosquitto.c (2.1.2):
544: session_expiry__check(); // new in 2.1; sweeps the expiry list at startup
558: rc = listeners__start();
567: bridge__start_all(); // bridge session is only reclaimed hereOn restore, session_expiry__add_from_persistence() puts the restored bridge session on the expiry list (it is only skipped when persistent_client_expiration == 0 and the session interval is "never"). session_expiry__check() has no bridge guard:
src/session_expiry.c (2.1.2):
if(item->context->session_expiry_time < db.now_real_s){ // no "&& !context->bridge"
... "Expiring client %s due to timeout." ...
context__add_to_disused(context);
}So the restored bridge session is expired at mosquitto.c:544, before bridge__start_all() at :567 can reclaim it.
On 2.0.x this cannot happen: session_expiry__check() is not called at startup at all (only from the run loop in src/loop.c), and bridge__start_all() runs before that loop — so bridge__new() always reclaims the restored session and marks it never-expire first.
Minimal reproduction (two stock 2.1.2 brokers, no patch)
#!/usr/bin/env bash
set -eu
IMG=eclipse-mosquitto:2.1.2
net=exp-net; work=$(mktemp -d); mkdir -p "$work/data"; chmod 777 "$work/data"
cat > "$work/remote.conf" <<EOF
listener 1883
allow_anonymous true
persistence false
EOF
cat > "$work/edge.conf" <<EOF
listener 1883
allow_anonymous true
log_type all
log_dest stdout
persistence true
persistence_location /mosquitto/data/
persistent_client_expiration 15s
connection remote
address remote:1883
bridge_protocol_version mqttv50
remote_clientid edgebridge
try_private true
cleansession false
topic test/# out 1
EOF
docker network create $net >/dev/null 2>&1 || true
sc(){ docker run -d --name exp-remote --network $net --network-alias remote -v "$work/remote.conf:/mosquitto/config/mosquitto.conf:ro" $IMG >/dev/null; }
se(){ docker rm -f exp-edge >/dev/null 2>&1 || true; docker run -d --name exp-edge --network $net -p 18831:1883 -v "$work/edge.conf:/mosquitto/config/mosquitto.conf:ro" -v "$work/data:/mosquitto/data" $IMG >/dev/null; }
sc; sleep 1; se; sleep 4 # both up, bridge connected
docker rm -f exp-remote >/dev/null 2>&1 # uplink outage
for i in $(seq 1 200); do mosquitto_pub -h 127.0.0.1 -p 18831 -t test/backlog -m "b$i" -q 1; done
docker stop -t 5 exp-edge >/dev/null; se; sleep 4 # restart #1: persist the queued backlog
sleep 20 # age past persistent_client_expiration
docker stop -t 5 exp-edge >/dev/null; se; sleep 4 # restart #2: trigger
docker logs exp-edge 2>&1 | grep -aE "Restored [0-9]+ client|Expiring client local|Connecting bridge"
docker rm -f exp-edge exp-remote >/dev/null 2>&1; docker network rm $net >/dev/null 2>&1 || true; rm -rf "$work"Actual output (restart #2)
Restored 200 client messages
Expiring client local.edgebridge due to timeout.
Connecting bridge remote (remote:1883)The 200 persisted offline messages are discarded with the expired session.
Expected
The restored bridge session should not be expired at startup; bridge__start_all() should reclaim it (as on 2.0.x), preserving the offline queue and outbound forwarding.
Impact
- The bridge's persisted offline queue (
persistence+cleansession false— the standard store-and-forward setup) is silently lost across a restart if the uplink was down longer thanpersistent_client_expiration. - In production, with real accumulated broker state, the bridge connects (keepalives fine) but sends zero PUBLISHes until the next manual restart — a silent outbound-forwarding stall. This does not reproduce with a trivial synthetic DB, only with a real one; I can share a
mosquitto.dbthat triggers the full stall on request.
Suggested fix
Restore the 2.0 ordering/intent: start bridges before the startup session_expiry__check(), or skip bridge contexts in session_expiry__check(), or don't add bridge sessions to the expiry list in session_expiry__add_from_persistence().
Source: eclipse-mosquitto/mosquitto