gbnet-hs 0.2.0.0 → 0.2.1.0
raw patch · 4 files changed
+88/−61 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ GBNet.Config: smallReliableThreshold :: Int
Files
- CHANGELOG.md +14/−0
- gbnet-hs.cabal +1/−1
- src/GBNet/Config.hs +9/−0
- src/GBNet/Connection.hs +64/−60
CHANGELOG.md view
@@ -1,5 +1,19 @@ # Changelog +## 0.2.1.0++### Bug Fixes++- **Congestion bypass for small reliable packets**: Small reliable messages (<= 64 bytes) now bypass the congestion window, preventing upgrade/command stalls when heavy unreliable traffic fills the cwnd. Binary rate limit still applies to all traffic.++### New++- `smallReliableThreshold` constant exported from `GBNet.Config`++### Internal++- Refactored `processChannelMessages` from nested if/else to guards + Maybe-monadic pipeline with named where-helpers+ ## 0.2.0.0 ### Breaking Changes
gbnet-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: gbnet-hs-version: 0.2.0.0+version: 0.2.1.0 synopsis: Transport-level networking library with zero-copy Storable serialization description: A transport-level networking library providing reliable UDP with
src/GBNet/Config.hs view
@@ -47,6 +47,7 @@ maxChannelCount, defaultDeltaBaselineTimeoutMs, defaultMaxBaselineSnapshots,+ smallReliableThreshold, -- * Configuration NetworkConfig (..),@@ -194,6 +195,14 @@ -- | Default rate limit for connection requests per second. defaultRateLimitPerSecond :: Int defaultRateLimitPerSecond = 10++-- | Maximum wire size (bytes) for a reliable message to bypass the+-- congestion window. Small reliable packets (e.g. upgrade commands)+-- are latency-sensitive and must not be starved by heavy unreliable+-- state traffic that fills the cwnd. The binary rate limit still+-- applies to all traffic.+smallReliableThreshold :: Int+smallReliableThreshold = 64 -- | Default delta baseline timeout in milliseconds. defaultDeltaBaselineTimeoutMs :: Double
src/GBNet/Connection.hs view
@@ -85,7 +85,7 @@ import Data.Word (Word32, Word64, Word8) import GBNet.Channel (Channel, ChannelError (..), ChannelMessage (..)) import qualified GBNet.Channel as Channel-import GBNet.Config (NetworkConfig (..))+import GBNet.Config (NetworkConfig (..), smallReliableThreshold) import GBNet.Congestion ( BandwidthTracker, CongestionController,@@ -598,65 +598,69 @@ else conn processChannelMessages :: MonoTime -> Connection -> Int -> Connection-processChannelMessages now conn chIdx =- let mtu = ncMtu (connConfig conn)- canSendCong = ccCanSend 0 mtu (connCongestion conn)- canSendCwnd = case connCwnd conn of- Just cw -> cwCanSend mtu cw && cwCanSendPaced now cw- Nothing -> True- in if not canSendCong || not canSendCwnd- then conn- else case IntMap.lookup chIdx (connChannels conn) of- Nothing -> conn- Just channel ->- case Channel.getOutgoingMessage channel of- Nothing -> conn- Just (msg, channel') ->- let msgData = cmData msg- msgSeqRaw = unSequenceNum (cmSequence msg)- -- Prepend payload header: channel (3 bits) + is_fragment (1 bit)- -- Format: [headerByte:1][channelSeqHi:1][channelSeqLo:1][msgData:N]- headerByte = fromIntegral chIdx .&. 0x07 -- Not a fragment- seqHi = fromIntegral (msgSeqRaw `shiftR` 8) :: Word8- seqLo = fromIntegral (msgSeqRaw .&. 0xFF) :: Word8- wireData = BS.cons headerByte $ BS.cons seqHi $ BS.cons seqLo msgData- header = createHeaderInternal conn- pkt =- OutgoingPacket- { opHeader = header {packetType = Payload},- opType = Payload,- opPayload = wireData- }- cong' = ccDeductBudget (BS.length wireData) (connCongestion conn)- cwnd' = fmap (cwOnSend (BS.length wireData) now) (connCwnd conn)- rel' =- if Channel.channelIsReliable channel- then- Rel.onPacketSent- (connLocalSeq conn)- now- (ChannelId (fromIntegral chIdx))- (cmSequence msg)- (BS.length wireData)- (connReliability conn)- else connReliability conn- conn' =- conn- & #connChannels- %~ IntMap.insert chIdx channel'- & #connSendQueue- %~ (Seq.|> pkt)- & #connLocalSeq- %~ (+ 1)- & #connCongestion- .~ cong'- & #connCwnd- .~ cwnd'- & #connReliability- .~ rel'- & #connDataSentThisTick- .~ True- in processChannelMessages now conn' chIdx+processChannelMessages now conn chIdx+ | not canSendBinary = conn+ | otherwise = case tryDequeue of+ Nothing -> conn+ Just (msg, channel', wireData, wireSize, isReliable)+ | not (cwndAllows isReliable wireSize) -> conn+ | otherwise ->+ processChannelMessages now (emitPacket conn msg channel' wireData wireSize isReliable) chIdx+ where+ mtu = ncMtu (connConfig conn)+ canSendBinary = ccCanSend 0 mtu (connCongestion conn)++ -- Small reliable packets bypass cwnd to prevent upgrade stalls+ cwndAllows isReliable wireSize+ | isReliable && wireSize <= smallReliableThreshold = True+ | otherwise = maybe True (\cw -> cwCanSend mtu cw && cwCanSendPaced now cw) (connCwnd conn)++ tryDequeue = do+ channel <- IntMap.lookup chIdx (connChannels conn)+ (msg, channel') <- Channel.getOutgoingMessage channel+ let msgSeqRaw = unSequenceNum (cmSequence msg)+ -- Payload header: channel (3 bits) | is_fragment (1 bit), then 16-bit channel seq+ headerByte = fromIntegral chIdx .&. 0x07+ seqHi = fromIntegral (msgSeqRaw `shiftR` 8) :: Word8+ seqLo = fromIntegral (msgSeqRaw .&. 0xFF) :: Word8+ wireData = BS.cons headerByte $ BS.cons seqHi $ BS.cons seqLo (cmData msg)+ pure (msg, channel', wireData, BS.length wireData, Channel.channelIsReliable channel)++ emitPacket c msg channel' wireData wireSize isReliable =+ let header = createHeaderInternal c+ pkt =+ OutgoingPacket+ { opHeader = header {packetType = Payload},+ opType = Payload,+ opPayload = wireData+ }+ cong' = ccDeductBudget wireSize (connCongestion c)+ cwnd' = fmap (cwOnSend wireSize now) (connCwnd c)+ rel'+ | isReliable =+ Rel.onPacketSent+ (connLocalSeq c)+ now+ (ChannelId (fromIntegral chIdx))+ (cmSequence msg)+ wireSize+ (connReliability c)+ | otherwise = connReliability c+ in c+ & #connChannels+ %~ IntMap.insert chIdx channel'+ & #connSendQueue+ %~ (Seq.|> pkt)+ & #connLocalSeq+ %~ (+ 1)+ & #connCongestion+ .~ cong'+ & #connCwnd+ .~ cwnd'+ & #connReliability+ .~ rel'+ & #connDataSentThisTick+ .~ True -- | Enqueue an empty keepalive/ack-only packet. -- Both keepalive and ack-only use the same wire format (Keepalive with empty payload).