gbnet-hs-0.1.0.0: src/GBNet/Simulator.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : GBNet.Simulator
-- Description : Network condition simulator for testing
--
-- Simulates packet loss, latency, jitter, duplicates, reordering,
-- and bandwidth limiting for testing purposes.
module GBNet.Simulator
( -- * Simulator
NetworkSimulator (..),
newNetworkSimulator,
simulatorProcessSend,
simulatorReceiveReady,
simulatorPendingCount,
simulatorConfig,
-- * Delayed packet
DelayedPacket (..),
)
where
import Control.Monad (when)
import Control.Monad.State.Strict (State, runState)
import qualified Data.ByteString as BS
import Data.Foldable (toList)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Word (Word64)
import GBNet.Class (MonoTime (..))
import GBNet.Config (SimulationConfig (..))
import GBNet.Reliability (elapsedMs)
import GBNet.Util (nextRandom, randomDouble)
import Optics ((&), (.~))
import Optics.State (use)
import Optics.State.Operators ((%=), (.=))
import Optics.TH (makeFieldLabelsNoPrefix)
-- | Maximum extra delay (ms) applied to out-of-order packets.
outOfOrderMaxDelayMs :: Double
outOfOrderMaxDelayMs = 50.0
-- | Packets with total delay below this threshold (ms) are delivered immediately.
immediateDeliveryThresholdMs :: Double
immediateDeliveryThresholdMs = 1.0
-- | Maximum extra delay (ms) applied to duplicate packets.
duplicateJitterMs :: Double
duplicateJitterMs = 20.0
-- | A packet delayed for later delivery.
data DelayedPacket = DelayedPacket
{ dpData :: !BS.ByteString,
dpAddr :: !Word64, -- Address key (simplified)
dpDeliverAt :: !MonoTime
}
deriving (Show)
makeFieldLabelsNoPrefix ''DelayedPacket
-- | Network condition simulator.
data NetworkSimulator = NetworkSimulator
{ nsConfig :: !SimulationConfig,
nsDelayedPackets :: !(Seq DelayedPacket),
nsTokenBucketTokens :: !Double,
nsLastTokenRefill :: !MonoTime,
nsRngState :: !Word64 -- Simple LCG RNG state
}
deriving (Show)
makeFieldLabelsNoPrefix ''NetworkSimulator
-- | Create a new network simulator.
newNetworkSimulator :: SimulationConfig -> MonoTime -> NetworkSimulator
newNetworkSimulator config now =
NetworkSimulator
{ nsConfig = config,
nsDelayedPackets = Seq.empty,
nsTokenBucketTokens = 0.0,
nsLastTokenRefill = now,
nsRngState = unMonoTime now -- Use time as initial seed
}
-- | Process an outgoing packet through the simulator.
-- Returns (immediate packets, updated simulator).
simulatorProcessSend ::
BS.ByteString ->
Word64 ->
MonoTime ->
NetworkSimulator ->
([(BS.ByteString, Word64)], NetworkSimulator)
simulatorProcessSend dat addr now =
runState $ do
r1 <- nextRandomS
r2 <- nextRandomS
r3 <- nextRandomS
r4 <- nextRandomS
config <- use #nsConfig
let lost = checkLoss config r1
if lost
then pure []
else do
overBandwidth <- checkBandwidthS (BS.length dat) now config
if overBandwidth
then pure []
else do
let totalDelayMs = calculateDelay config r2 r3 r4
deliverAt = now + MonoTime (round (totalDelayMs * 1e6))
immediate <- scheduleOrDeliverS dat addr deliverAt totalDelayMs
scheduleDuplicateS dat addr now totalDelayMs config
pure immediate
-- | Check if a packet should be dropped due to simulated loss.
checkLoss :: SimulationConfig -> Word64 -> Bool
checkLoss config rng =
let threshold = simPacketLoss config
in threshold > 0.0 && randomDouble rng < threshold
-- | Check if bandwidth limit is exceeded, consuming tokens if available.
checkBandwidthS :: Int -> MonoTime -> SimulationConfig -> State NetworkSimulator Bool
checkBandwidthS packetSize now config
| simBandwidthLimitBytesPerSec config <= 0 = pure False
| otherwise = do
refillTokensS now
tokens <- use #nsTokenBucketTokens
let needed = fromIntegral packetSize
if tokens < needed
then pure True
else do
#nsTokenBucketTokens .= (tokens - needed)
pure False
-- | Calculate total delay including latency, jitter, and out-of-order reordering.
calculateDelay :: SimulationConfig -> Word64 -> Word64 -> Word64 -> Double
calculateDelay config rJitter rOoOChance rOoODelay =
let baseLatency = fromIntegral (simLatencyMs config) :: Double
jitter
| simJitterMs config > 0 = randomDouble rJitter * fromIntegral (simJitterMs config)
| otherwise = 0.0
outOfOrderChance = simOutOfOrderChance config
extraDelay
| outOfOrderChance > 0.0 && randomDouble rOoOChance < outOfOrderChance =
randomDouble rOoODelay * outOfOrderMaxDelayMs
| otherwise = 0.0
in baseLatency + jitter + extraDelay
-- | Deliver a packet immediately or schedule it for delayed delivery.
scheduleOrDeliverS ::
BS.ByteString -> Word64 -> MonoTime -> Double -> State NetworkSimulator [(BS.ByteString, Word64)]
scheduleOrDeliverS dat addr deliverAt totalDelayMs
| totalDelayMs < immediateDeliveryThresholdMs = pure [(dat, addr)]
| otherwise = do
let delayed = DelayedPacket {dpData = dat, dpAddr = addr, dpDeliverAt = deliverAt}
#nsDelayedPackets %= (Seq.|> delayed)
pure []
-- | Maybe schedule a duplicate packet with extra jitter.
scheduleDuplicateS :: BS.ByteString -> Word64 -> MonoTime -> Double -> SimulationConfig -> State NetworkSimulator ()
scheduleDuplicateS dat addr now baseDelayMs config = do
r <- nextRandomS
let dupChance = simDuplicateChance config
when (dupChance > 0.0 && randomDouble r < dupChance) $ do
let dupDeliverAt = now + MonoTime (round ((baseDelayMs + randomDouble r * duplicateJitterMs) * 1e6))
dupPacket = DelayedPacket {dpData = dat, dpAddr = addr, dpDeliverAt = dupDeliverAt}
#nsDelayedPackets %= (Seq.|> dupPacket)
-- | Retrieve packets ready for delivery.
-- Scans all delayed packets since they may not be sorted by delivery time.
simulatorReceiveReady :: MonoTime -> NetworkSimulator -> ([(BS.ByteString, Word64)], NetworkSimulator)
simulatorReceiveReady now sim =
let (ready, notReady) = Seq.partition (\pkt -> dpDeliverAt pkt <= now) (nsDelayedPackets sim)
results = map (\pkt -> (dpData pkt, dpAddr pkt)) (toList ready)
in (results, sim & #nsDelayedPackets .~ notReady)
-- | Get count of pending delayed packets.
simulatorPendingCount :: NetworkSimulator -> Int
simulatorPendingCount = Seq.length . nsDelayedPackets
-- | Get the simulation configuration.
simulatorConfig :: NetworkSimulator -> SimulationConfig
simulatorConfig = nsConfig
{-# INLINE simulatorConfig #-}
-- | Refill token bucket based on elapsed time (State version).
refillTokensS :: MonoTime -> State NetworkSimulator ()
refillTokensS now = do
sim <- use #nsLastTokenRefill
config <- use #nsConfig
tokens <- use #nsTokenBucketTokens
let elapsedSecs = elapsedMs sim now / 1000.0
refillRate = fromIntegral (simBandwidthLimitBytesPerSec config)
newTokens = tokens + elapsedSecs * refillRate
maxTokens = refillRate -- Cap at 1 second worth
cappedTokens = min newTokens maxTokens
#nsTokenBucketTokens .= cappedTokens
#nsLastTokenRefill .= now
-- | Stateful wrapper around the shared 'nextRandom' from GBNet.Util.
nextRandomS :: State NetworkSimulator Word64
nextRandomS = do
s <- use #nsRngState
let (output, next) = nextRandom s
#nsRngState .= next
pure output