Skip to content

Commit 42d7b13

Browse files
Allow application to burst a sequence of sdu's
1 parent 6ac49ca commit 42d7b13

8 files changed

Lines changed: 147 additions & 46 deletions

File tree

network-mux/bench/socket_read_write/Main.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ mkMiniProtocolState num = do
144144
mpv <- newTVarIO StatusRunning
145145

146146
let mpi = MiniProtocolInfo (MiniProtocolNum num) InitiatorDirectionOnly
147-
(MiniProtocolLimits maxBound) Nothing
147+
(MiniProtocolLimits maxBound Nothing) Nothing
148148
return $ MiniProtocolState mpi mpq mpv
149149

150150
-- | Run a server that accept connections on `ad`.
@@ -253,7 +253,7 @@ startServerEgresss pollInterval sndSizeV ad = forever $ do
253253
let wasEmpty = BL.null buf
254254
writeTVar w (BL.append buf msg)
255255
when wasEmpty $
256-
writeTBQueue eq (TLSRDemand mc md $ Wanton w)
256+
writeTBQueue eq (TLSRDemand mc md (Wanton w) $ ProtocolBurst 1)
257257
else retry
258258

259259
setupServer :: Socket -> IO Socket.SockAddr

network-mux/demo/mux-demo.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ debugTracer = showTracing (Tracer putStrLn_)
8080
defaultProtocolLimits :: MiniProtocolLimits
8181
defaultProtocolLimits =
8282
MiniProtocolLimits {
83-
maximumIngressQueue = 64_000
83+
maximumIngressQueue = 64_000,
84+
burst = Nothing
8485
}
8586

8687
--

network-mux/demo/mux-leios-demo.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ reqrespTracer tag = Tracer $ \case
115115
defaultProtocolLimits :: MiniProtocolLimits
116116
defaultProtocolLimits =
117117
MiniProtocolLimits {
118-
maximumIngressQueue = 10_000_000
118+
maximumIngressQueue = 10_000_000,
119+
burst = Nothing
119120
}
120121

121122

network-mux/network-mux.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ library
6767
statistics-linreg >=0.3 && <0.4,
6868
strict,
6969
time >=1.9.1 && <1.16,
70+
transformers,
7071
vector >=0.12 && <0.14,
7172

7273
if os(windows)

network-mux/src/Network/Mux.hs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import Data.ByteString.Lazy qualified as BL
6464
import Data.Int (Int64)
6565
import Data.Map (Map)
6666
import Data.Map.Strict qualified as Map
67-
import Data.Maybe (isNothing)
67+
import Data.Maybe (fromMaybe, isNothing)
6868
import Data.Monoid.Synchronisation (FirstToFinish (..))
6969
import Data.Strict.Tuple (pattern (:!:))
7070

@@ -76,6 +76,7 @@ import Control.Monad
7676
import Control.Monad.Class.MonadAsync
7777
import Control.Monad.Class.MonadFork
7878
import Control.Monad.Class.MonadThrow
79+
import Control.Monad.Class.MonadTime.SI (Time (..))
7980
import Control.Monad.Class.MonadTimer.SI hiding (timeout)
8081
import Control.Tracer
8182

@@ -307,7 +308,8 @@ miniProtocolJob TracersI {
307308
miniProtocolInfo =
308309
MiniProtocolInfo {
309310
miniProtocolNum,
310-
miniProtocolDir
311+
miniProtocolDir,
312+
miniProtocolLimits
311313
},
312314
miniProtocolIngressQueue,
313315
miniProtocolStatusVar
@@ -323,9 +325,11 @@ miniProtocolJob TracersI {
323325
where
324326
jobAction = do
325327
w <- newTVarIO BL.empty
326-
let chan = muxChannel channelTracer_ egressQueue (Wanton w)
328+
lastSent <- newTVarIO (Time 0)
329+
bucket <- newTVarIO 0
330+
let chan = muxChannel channelTracer_ egressQueue (Wanton w lastSent bucket)
327331
miniProtocolNum miniProtocolDirEnum
328-
miniProtocolIngressQueue
332+
miniProtocolIngressQueue (burst miniProtocolLimits)
329333
(result, remainder) <- miniProtocolAction chan
330334
traceWith tracer_ (TraceTerminating miniProtocolNum miniProtocolDirEnum)
331335
atomically $ do
@@ -668,8 +672,9 @@ muxChannel
668672
-> MiniProtocolNum
669673
-> MiniProtocolDir
670674
-> IngressQueue m
675+
-> Maybe ProtocolBurst
671676
-> ByteChannel m
672-
muxChannel tracer egressQueue want@(Wanton w) mc md q =
677+
muxChannel tracer egressQueue want@(Wanton w _ _) mc md q mBurst =
673678
Channel { send, recv }
674679
where
675680
-- A soft limit on the egress buffer (Wanton) size.
@@ -682,6 +687,8 @@ muxChannel tracer egressQueue want@(Wanton w) mc md q =
682687
egressSoftBufferLimit :: Int64
683688
egressSoftBufferLimit = 0x3ffff
684689

690+
burst = fromMaybe (ProtocolBurst 0 0) mBurst
691+
685692
send :: BL.ByteString -> m ()
686693
send encoding = do
687694
-- We send CBOR encoded messages by encoding them into by ByteString
@@ -696,7 +703,7 @@ muxChannel tracer egressQueue want@(Wanton w) mc md q =
696703
let wasEmpty = BL.null buf
697704
writeTVar w (BL.append buf encoding)
698705
when wasEmpty $
699-
writeTBQueue egressQueue (TLSRDemand mc md want)
706+
writeTBQueue egressQueue (TLSRDemand mc md want burst)
700707
else retry
701708

702709
traceWith tracer $ TraceChannelSendEnd mc

network-mux/src/Network/Mux/Egress.hs

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
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

812
module Network.Mux.Egress
@@ -14,8 +18,13 @@ module Network.Mux.Egress
1418
, Wanton (..)
1519
) where
1620

21+
import Control.Exception
1722
import Control.Monad
23+
import Control.Monad.Trans.Class
24+
import Control.Monad.Trans.Except
25+
import Data.Bool
1826
import Data.ByteString.Lazy qualified as BL
27+
import Data.Word (Word32)
1928

2029
import Control.Concurrent.Class.MonadSTM.Strict
2130
import 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.
123132
data 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

network-mux/src/Network/Mux/Types.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Network.Mux.Types
2525
, IngressQueue
2626
, MiniProtocolIx
2727
, MiniProtocolDir (..)
28+
, ProtocolBurst (..)
2829
, protocolDirEnum
2930
, MiniProtocolState (..)
3031
, MiniProtocolStatus (..)
@@ -93,15 +94,23 @@ newtype MiniProtocolNum = MiniProtocolNum Word16
9394
deriving (Eq, Ord, Enum, Ix, Show)
9495

9596
-- | Per Miniprotocol limits
96-
newtype MiniProtocolLimits =
97+
data MiniProtocolLimits =
9798
MiniProtocolLimits {
9899
-- | Limit on the maximum number of bytes that can be queued in the
99100
-- miniprotocol's ingress queue.
100101
--
101-
maximumIngressQueue :: Int
102+
maximumIngressQueue :: !Int,
103+
burst :: !(Maybe ProtocolBurst)
102104
}
103105
deriving Show
104106

107+
108+
data ProtocolBurst = ProtocolBurst {
109+
pbMaxBytes :: !Word32,
110+
pbRefillRate :: !Word32
111+
}
112+
deriving (Eq, Show)
113+
105114
-- $interface
106115
--
107116
-- To run a node you will also need a bearer and a way to run a server, see

0 commit comments

Comments
 (0)