Compare commits

...
Author SHA1 Message Date
Narasimha-sc e987bdbbea agent: bound how long reconnection is delayed after the network recovers
The reconnection interval grows to 3 minutes, and nothing shortens it when the
network becomes usable again - the attempts made while it was down are what
pushed it there. After a network change that is the larger part of the delay:
measured with a SOCKS proxy that stalls the open streams and refuses new ones,
the client was dropped after 119 seconds, but reconnected 50 seconds after the
proxy started accepting again, with the interval still growing.

Detecting the dead client sooner makes this worse rather than better, as more of
the attempts are spent while the network cannot work yet.
2026-10-08 20:30:38 +00:00
Narasimha-sc 3882287e05 client: drop an unresponsive client without waiting for the ping schedule
The client is dropped by the liveness monitor, and the monitor only reached its
decision after smpPingInterval (10 minutes) without a received transmission: the
timeout counter was read inside the branch that sends a ping, which is only taken
when the ping is due. Until then the requests kept timing out against a client
that could not recover, which is how delivery stays broken for ~20 minutes after
the path to the server dies with the socket still open - what happens on a
network change behind a SOCKS proxy, where the socket is connected to the proxy
and stays up.

The counter is now read on every pass, and the loop does not sleep past the next
request timeout, which is the only time the counter can change. The recovery
window becomes the time those timeouts take (smpPingCount * tcpTimeout), with a
90 second floor, instead of a fixed 15 minutes: reaching the counter already
requires that many requests to have timed out in sequence with nothing received,
so the window only has to cover them, and the floor keeps a low request timeout
from dropping a connection that is only slow.

Slowness alone still does not drop a client - any received transmission resets
both the counter and the time, so only silence for the whole window does.
2026-10-08 20:29:19 +00:00
2 changed files with 16 additions and 11 deletions
+3 -1
View File
@@ -198,7 +198,9 @@ defaultReconnectInterval =
RetryInterval
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 180_000000
-- the network can become usable at any point in this interval, and nothing shortens it,
-- so it bounds how long reconnection is delayed after the network recovers
maxInterval = 30_000000
}
defaultMessageRetryInterval :: RetryInterval2
+13 -10
View File
@@ -675,19 +675,22 @@ getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qS
where
loop :: Int64 -> IO ()
loop delay = do
threadDelay' delay
-- the client is dropped here, so the loop cannot sleep past the next request timeout
threadDelay' $ if maxCnt > 0 then min delay requestTimeout else delay
diff <- diffUTCTime <$> getCurrentTime <*> readTVarIO lastReceived
let idle = diffToMicroseconds diff
remaining = smpPingInterval - idle
if remaining > 1_000_000 -- delay pings only for significant time
then loop remaining
else do
whenM (readTVarIO sendPings) $ void . runExceptT $ sendProtocolCommand c NRMBackground Nothing NoEntity (protocolPing @v @err @msg)
-- sendProtocolCommand/getResponse updates counter for each command
cnt <- readTVarIO timeoutErrorCount
-- drop client when maxCnt of commands have timed out in sequence, but only after some time has passed after last received response
when (maxCnt == 0 || cnt < maxCnt || diff < recoverWindow) $ loop smpPingInterval
recoverWindow = 15 * 60 -- seconds
due = remaining <= 1_000_000 -- delay pings only for significant time
when due $ whenM (readTVarIO sendPings) $ void . runExceptT $ sendProtocolCommand c NRMBackground Nothing NoEntity (protocolPing @v @err @msg)
-- sendProtocolCommand/getResponse updates counter for each command
cnt <- readTVarIO timeoutErrorCount
-- drop client when maxCnt of commands have timed out in sequence, but only after some time has passed after last received response
when (maxCnt == 0 || cnt < maxCnt || idle < recoverWindow) $ loop $ if due then smpPingInterval else remaining
-- the counter only changes when a request times out, so nothing is lost by checking it that often
requestTimeout = fromIntegral $ netTimeoutInt tcpTimeout NRMBackground
-- the counter requires maxCnt requests to time out, so the window only has to cover their timeouts,
-- with a floor, so that a low request timeout cannot drop a connection that is only slow
recoverWindow = max 90_000000 $ fromIntegral maxCnt * requestTimeout
maxCnt = smpPingCount networkConfig
process :: ProtocolClient v err msg -> IO ()