gbnet-hs 0.2.2.0 → 0.2.3.0
raw patch · 4 files changed
+86/−2 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- gbnet-hs.cabal +1/−1
- src/GBNet/Peer.hs +7/−1
- test/Main.hs +72/−0
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Changelog +## 0.2.3.0++### Bug Fixes++- **Fix keepalive regression**: `drainAllConnectionQueues` unconditionally called `recordBytesSent` even when zero bytes were sent, resetting `connLastSendTime` every tick. This prevented the keepalive timer from ever triggering, causing connections to silently die on any single packet loss (no ACK feedback, no retransmission). Now only records when actual bytes are sent.+ ## 0.2.2.0 ### Bug Fixes
gbnet-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: gbnet-hs-version: 0.2.2.0+version: 0.2.3.0 synopsis: Transport-level networking library with zero-copy Storable serialization description: A transport-level networking library providing reliable UDP with
src/GBNet/Peer.hs view
@@ -237,9 +237,15 @@ let (connPackets, drained) = Conn.drainSendQueue conn (rawPackets, encrypted) = encryptOutgoing peerId protocolId drained connPackets !bytesSent = sum (map (BS.length . rpData) rawPackets)+ -- Only record send stats when bytes were actually sent.+ -- Recording zero bytes resets connLastSendTime every tick,+ -- which prevents the keepalive timer from ever triggering.+ !updatedConn+ | bytesSent > 0 = Conn.recordBytesSent bytesSent now encrypted+ | otherwise = encrypted in foldl' (flip queueRawPacket)- (acc & #npConnections %~ Map.insert peerId (Conn.recordBytesSent bytesSent now encrypted))+ (acc & #npConnections %~ Map.insert peerId updatedConn) rawPackets -- | Encrypt outgoing packets and update connection nonce state.
test/Main.hs view
@@ -263,6 +263,9 @@ -- Bandwidth tracking testBandwidthTracking + -- Keepalive regression (0.2.2.0 bug: recordBytesSent 0 prevented keepalives)+ testKeepaliveRegression+ -- Migration cooldown sweep testMigrationCooldownSweep @@ -2033,6 +2036,75 @@ oneSecondNs = 1000000000 :: MonoTime tickStepMs = 10 :: MonoTime testPayload = "bandwidth-tracking-test"++--------------------------------------------------------------------------------+-- Keepalive regression test (0.2.2.0 bug)+--------------------------------------------------------------------------------++-- | Verify keepalives fire after the keepalive interval with no data sent.+-- Regression: drainAllConnectionQueues called recordBytesSent unconditionally+-- (even with 0 bytes), resetting connLastSendTime every tick and preventing+-- the keepalive timer from ever triggering.+testKeepaliveRegression :: IO ()+testKeepaliveRegression = do+ putStrLn "Keepalive fires after idle period (regression):"+ sock <- newTestUdpSocket+ let serverAddr = testAddr serverPort+ clientAddr = testAddr clientPort+ config = defaultNetworkConfig+ startTime = oneSecondNs++ let serverPeer = newPeerState sock serverAddr config serverSeed+ clientPeer0 = newPeerState sock clientAddr config clientSeed+ clientPeer1 = peerConnect (peerIdFromAddr serverAddr) startTime clientPeer0++ let world0 = initWorld startTime serverAddr clientAddr++ -- Full handshake (5 ticks)+ let ((_, cp2), w1) = tickPeerInWorld clientAddr [] clientPeer1 world0+ let w2 = stepWorld tickStepMs w1+ let ((_, sp1), w3) = tickPeerInWorld serverAddr [] serverPeer w2+ let w4 = stepWorld tickStepMs w3+ let ((_, cp3), w5) = tickPeerInWorld clientAddr [] cp2 w4+ let w6 = stepWorld tickStepMs w5+ let ((_, sp2), w7) = tickPeerInWorld serverAddr [] sp1 w6+ let w8 = stepWorld tickStepMs w7+ let ((_, cp4), w9) = tickPeerInWorld clientAddr [] cp3 w8+ let _w10 = stepWorld tickStepMs w9++ -- Both connected. Verify connection exists.+ let serverPid = peerIdFromAddr serverAddr+ assertEqual "client connected" True (peerIsConnected serverPid cp4)++ -- Check bytes sent immediately after handshake (should be 0 — no data sent)+ case peerStats serverPid cp4 of+ Nothing -> error " FAIL: no stats after handshake"+ Just stats -> assertEqual "no data bytes yet" 0 (nsBytesSent stats)++ -- Run 110 idle ticks (1.1s at 10ms/step) to trigger keepalive (1000ms interval).+ -- No messages sent, just ticking.+ let tickIdle (peer, world) _ =+ let stepped = stepWorld tickStepMs world+ ((_, ticked), nextWorld) = tickPeerInWorld clientAddr [] peer stepped+ in (ticked, nextWorld)+ (cpAfterIdle, _) = foldl' tickIdle (cp4, _w10) [(1 :: Int) .. idleTicks]++ -- After 1.1 seconds idle, keepalive should have fired → nsBytesSent > 0+ case peerStats serverPid cpAfterIdle of+ Nothing -> error " FAIL: connection lost during idle"+ Just stats -> do+ assertEqual "keepalive sent bytes > 0" True (nsBytesSent stats > 0)+ assertEqual "keepalive sent packets > 0" True (nsPacketsSent stats > 0)++ putStrLn " PASS: Keepalive fires after idle period"+ where+ serverPort = 7025+ clientPort = 8025+ serverSeed = 100000000+ clientSeed = 200000000+ oneSecondNs = 1000000000 :: MonoTime+ tickStepMs = 10 :: MonoTime+ idleTicks = 110 :: Int -- 110 * 10ms = 1.1 seconds > 1s keepalive interval -------------------------------------------------------------------------------- -- Migration cooldown sweep