11{-# LANGUAGE BangPatterns #-}
2+ {-# LANGUAGE BlockArguments #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE MultiParamTypeClasses #-}
5+ {-# LANGUAGE MultiWayIf #-}
46{-# LANGUAGE NamedFieldPuns #-}
57{-# LANGUAGE RankNTypes #-}
8+ {-# LANGUAGE ScopedTypeVariables #-}
9+ {-# LANGUAGE TupleSections #-}
610{-# LANGUAGE TypeFamilies #-}
711
812module Network.Mux.Egress
@@ -14,8 +18,13 @@ module Network.Mux.Egress
1418 , Wanton (.. )
1519 ) where
1620
21+ import Control.Exception
1722import Control.Monad
23+ import Control.Monad.Trans.Class
24+ import Control.Monad.Trans.Except
25+ import Data.Bool
1826import Data.ByteString.Lazy qualified as BL
27+ import Data.Word (Word32 )
1928
2029import Control.Concurrent.Class.MonadSTM.Strict
2130import Control.Monad.Class.MonadAsync
@@ -121,12 +130,16 @@ type EgressQueue m = StrictTBQueue m (TranslocationServiceRequest m)
121130-- responsible for the segmentation of concrete representation into
122131-- appropriate SDU's for onward transmission.
123132data TranslocationServiceRequest m =
124- TLSRDemand ! MiniProtocolNum ! MiniProtocolDir ! (Wanton m )
133+ TLSRDemand ! MiniProtocolNum ! MiniProtocolDir ! (Wanton m ) ! ProtocolBurst
125134
126135-- | A Wanton represent the concrete data to be translocated, note that the
127136-- TVar becoming empty indicates -- that the last fragment of the data has
128137-- been enqueued on the -- underlying bearer.
129- newtype Wanton m = Wanton { want :: StrictTVar m BL. ByteString }
138+ data Wanton m = Wanton {
139+ want :: ! (StrictTVar m BL. ByteString ),
140+ wLastSent :: ! (StrictTVar m Time ),
141+ wBucket :: ! (StrictTVar m Word32 )
142+ }
130143
131144
132145-- | Process the messages from the mini protocols - there is a single
@@ -150,9 +163,18 @@ muxer egressQueue tracer Bearer { writeMany, sduSize, batchSize, egressInterval
150163 withTimeoutSerial $ \ timeout ->
151164 forever $ do
152165 start <- getMonotonicTime
153- TLSRDemand mpc md d <- atomically $ readTBQueue egressQueue
154- sdu <- processSingleWanton egressQueue sduSize mpc md d
155- sdus <- buildBatch [sdu] (sduLength sdu)
166+ (sdu, mBurst) <- atomically do
167+ demand@ (TLSRDemand mpc md d (ProtocolBurst pbMaxBytes _pbRefillRate)) <- readTBQueue egressQueue
168+ eSdu <- processSingleWanton sduSize mpc md d
169+ case eSdu of
170+ Right sdu | pbMaxBytes > 0 -> do
171+ -- we do not check if the protocol has any tokens to burst,
172+ -- that is deferred to buildBatch below.
173+ (sdu, True ) <$ unGetTBQueue egressQueue demand
174+ | otherwise -> (sdu, False ) <$ writeTBQueue egressQueue demand
175+ Left sdu -> pure (sdu, False )
176+
177+ sdus <- buildBatch [sdu] (sduLength sdu) mBurst start
156178 void $ writeMany tracer timeout sdus
157179 end <- getMonotonicTime
158180 empty <- atomically $ isEmptyTBQueue egressQueue
@@ -168,51 +190,109 @@ muxer egressQueue tracer Bearer { writeMany, sduSize, batchSize, egressInterval
168190 sduLength sdu = fromIntegral msHeaderLength + fromIntegral (msLength sdu)
169191
170192 -- Build a batch of SDUs to submit in one go to the bearer.
171- -- The egress queue is still processed one SDU at the time
172- -- to ensure that we don't cause starvation.
193+ -- Streams which are permitted to burst will have that many
194+ -- sdu's serviced back-to-back before the scheduler moves to process the
195+ -- next request on the queue. Any remaining sdu's which did not
196+ -- fit in the burst allowance are placed on the back of the queue
197+ -- to ensure that we don't cause starvation. In particular, a burst
198+ -- of 1 will have the muxer process one sdu at a time from the queue,
199+ -- and any remaining work is put on the back of the queue.
173200 -- The batch size is either limited by the bearer
174201 -- (e.g the SO_SNDBUF for Socket) or number of SDUs.
175202 --
176- buildBatch s sl = reverse <$> go s sl
203+ buildBatch s sl mBurst0 start = reverse <$> go 1 s sl mBurst0
177204 where
178- go sdus _ | length sdus >= maxSDUsPerBatch = return sdus
179- go sdus sdusLength | sdusLength >= batchSize = return sdus
180- go sdus ! sdusLength = do
181- demand_m <- atomically $ tryReadTBQueue egressQueue
182- case demand_m of
183- Just (TLSRDemand mpc md d) -> do
184- sdu <- processSingleWanton egressQueue sduSize mpc md d
185- go (sdu: sdus) (sdusLength + sduLength sdu)
186- Nothing -> return sdus
205+ toDouble :: DiffTime -> Double
206+ toDouble = realToFrac
207+
208+ go ! count sdus _ _ | count >= maxSDUsPerBatch = return sdus
209+ go _ sdus sdusLength _ | sdusLength >= batchSize = return sdus
210+ go count sdus ! sdusLength mBurst = do
211+ mResult <- atomically $ tryReadTBQueue egressQueue
212+ case mResult of
213+ Nothing -> return sdus
214+ Just demand@ (TLSRDemand mpc md d@ Wanton { wLastSent, wBucket } (ProtocolBurst pbMaxBytes pbRefillRate)) -> do
215+ (count', sdusLength', sdus') <- atomically do
216+ delta <- (start `diffTime` ) <$> stateTVar wLastSent (, start)
217+ isEmpty <- isEmptyTBQueue egressQueue
218+ sduSize0 <- stateTVar wBucket \ tokens ->
219+ let tokens' = truncate $ min (fromIntegral pbMaxBytes)
220+ (fromIntegral tokens + fromIntegral pbRefillRate * toDouble delta)
221+ -- we leverage burst and deduct credits only where there is contention
222+ -- between protocols
223+ sduSize0 = bool (Left sduSize) (Right $ min sduSize (fromIntegral tokens')) (mBurst && not isEmpty)
224+ in (sduSize0, tokens')
225+ let step (! count', ! sdusLength', ! sdus', ! eSize) mx = do
226+ -- the first one is always free
227+ -- For Left's, we don't count the wanton bytes against the burst allowance
228+ -- to permit a full sdu in the first iteration
229+ let (size, consumedTokens) = either (, const 0 ) (, id ) eSize
230+ x <- lift $ mx size
231+ case x of
232+ Left sdu -> do
233+ lift $ modifyTVar wBucket \ tokens ->
234+ let tokens' = tokens - consumedTokens (fromIntegral (msLength sdu))
235+ in assert (tokens >= consumedTokens (fromIntegral $ msLength sdu))
236+ tokens'
237+ let sdusLength'' = sdusLength' + sduLength sdu
238+ throwE (succ count', sdusLength'', sdu: sdus')
239+ Right sdu -> do
240+ nextSdu <- lift $ stateTVar wBucket \ tokens ->
241+ let tokens' = tokens - consumedTokens (fromIntegral (msLength sdu))
242+ nextSdu = min sduSize (fromIntegral tokens')
243+ in assert (tokens >= consumedTokens (fromIntegral $ msLength sdu))
244+ (nextSdu, tokens')
245+ let sdusLength'' = sdusLength' + sduLength sdu
246+ count'' = succ count'
247+ if | nextSdu <= 400 -> do -- 8 bytes header / 2% burst efficiency
248+ -- there is more payload, but burst allowance has been exhausted
249+ lift $ writeTBQueue egressQueue demand
250+ throwE (count'', sdusLength'', sdu: sdus')
251+ | sdusLength'' >= batchSize || count'' >= maxSDUsPerBatch -> do
252+ lift $ unGetTBQueue egressQueue demand
253+ throwE (count'', sdusLength'', sdu: sdus')
254+ | otherwise -> pure (count'', sdusLength'', sdu: sdus', Right nextSdu)
255+ either pure (\ (a, b, c, _d) -> (a, b, c) <$ writeTBQueue egressQueue demand)
256+ =<< runExceptT do
257+ when (either id id sduSize0 <= 400 ) do
258+ -- edge case where the protocol is bursty, but there aren't enough tokens
259+ -- available. The muxer forever loop does not check this
260+ -- when it calls to build a batch, so we handle it here.
261+ lift $ writeTBQueue egressQueue demand
262+ throwE (count, sdusLength, sdus)
263+ foldM step (count, sdusLength, sdus, sduSize0)
264+ (if isEmpty
265+ then [const $ processSingleWanton sduSize mpc md d]
266+ -- ^ grab full sdu, save the tokens for cases with contention
267+ else repeat (\ sduSize' -> processSingleWanton sduSize' mpc md d))
268+ go count' sdus' sdusLength' False
269+
187270
188271-- | Pull a `maxSDU`s worth of data out out the `Wanton` - if there is
189272-- data remaining requeue the `TranslocationServiceRequest` (this
190273-- ensures that any other items on the queue will get some service
191274-- first.
192- processSingleWanton :: MonadSTM m
193- => EgressQueue m
194- -> SDUSize
275+ processSingleWanton :: (MonadSTM m )
276+ => SDUSize
195277 -> MiniProtocolNum
196278 -> MiniProtocolDir
197279 -> Wanton m
198- -> m SDU
199- processSingleWanton egressQueue (SDUSize sduSize)
280+ -- Right: more sdu's remain; Left: finished
281+ -> STM m (Either SDU SDU )
282+ processSingleWanton sduSize
200283 mpc md wanton = do
201- blob <- atomically $ do
284+ ( blob, wrap) <- do
202285 -- extract next SDU
203286 d <- readTVar (want wanton)
204287 let (frag, rest) = BL. splitAt (fromIntegral sduSize) d
205288 -- if more to process then enqueue remaining work
206289 if BL. null rest
207- then writeTVar (want wanton) BL. empty
290+ then (frag, Left ) <$ writeTVar (want wanton) BL. empty
208291 else do
209292 -- Note that to preserve bytestream ordering within a given
210293 -- miniprotocol the readTVar and writeTVar operations
211294 -- must be inside the same STM transaction.
212- writeTVar (want wanton) rest
213- writeTBQueue egressQueue (TLSRDemand mpc md wanton)
214- -- return data to send
215- pure frag
295+ (frag, Right ) <$ writeTVar (want wanton) rest
216296 let sdu = SDU {
217297 msHeader = SDUHeader {
218298 mhTimestamp = RemoteClockModel 0 ,
@@ -222,5 +302,5 @@ processSingleWanton egressQueue (SDUSize sduSize)
222302 },
223303 msBlob = blob
224304 }
225- return sdu
305+ pure $ wrap sdu
226306 -- paceTransmission tNow
0 commit comments