Reconnects and lost events¶
ConnState carries three fields, and the third is the reason this page exists:
The failure being described¶
Chat gateways drop connections. That is normal, and every client reconnects.
What is not normal, and not visible, is how the reconnect went. Two outcomes look identical from the outside:
Resumed. The client reconnects, presents its session, and the platform replays everything buffered during the gap. Nothing is lost.
Re-identified. The resume is refused — wrong endpoint, session expired, sequence too old — so the client starts a fresh session. Everything buffered during the gap is gone.
The second produces no error. Nothing crashes, nothing retries, no log line says anything is wrong. The bot reconnects, reports healthy, and has silently missed every message sent during the outage.
For a support bot those are questions nobody will ever answer. It is the worst shape of failure available: invisible, unattributable, and indistinguishable from nobody having asked.
So the contract surfaces it.
Why Healthy() ignores it¶
A session that resumed with loss is usable. The connection is live, messages are arriving, and restarting would help nothing. Folding the loss into a health check would take a working bot out of rotation for something a restart cannot fix.
The loss is a separate signal, worth alerting on in its own right:
if st := p.Reader.State(); st.LastReconnectLostEvents {
metrics.ReconnectLoss.Inc()
log.Warn("reconnect re-identified; messages during the gap were dropped")
}
Knowing you missed something is not the same as being broken. It is the difference between a gap you can go back and check and a gap you never knew about.
Why a bool and not just a status¶
An earlier design had Status alone. Three values can say connected,
reconnecting or disconnected, and none of them distinguishes a self-healing
resume from a reconnect that dropped an hour of questions. The state you end in
is the same either way.
Providers must record it, not derive it¶
This is the part that bites.
The flag has to be recorded from whatever the platform tells you, at the moment it tells you. It cannot be reconstructed from connection state afterwards.
The Discord provider originally inferred it from the live session ID, on the reasoning that a resumed session keeps its ID. It does — but Discord also issues an ID after a re-identify, so the inference was false in exactly the case it existed to catch. The flag could never fire. Every test passed, the connection worked, and the signal was dead.
Discord says which happened directly, and unambiguously:
| Frame | Meaning |
|---|---|
RESUMED |
the gap was replayed |
a second READY |
the session restarted; the gap is gone |
Recording those two events is three lines and it cannot be wrong. See Author a provider.
Testing it¶
You cannot test this by closing the connection. Every client library treats a local close as deliberate and either does not reconnect or reconnects differently.
What is needed is a connection that dies the way a network dies: peer-side,
unannounced, with no close frame. The chat-platform-discord module does it
with a TCP proxy in front of the gateway, cutting connections with SetLinger(0)
so the drop is an RST rather than a FIN, and asserts that the session resumed
with its ID intact.
It is behind an integration build tag because it needs live credentials. It is
also the only test that would notice if a dependency bump quietly changed this
behaviour — which is the entire reason it exists.