mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-07-31 09:31:08 +00:00
826 lines
40 KiB
Haskell
826 lines
40 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedLists #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
|
|
-- | Memory-leak load driver for the SMP server.
|
|
--
|
|
-- Starts an in-process SMP server (beta.2 code) and hammers a chosen command
|
|
-- path in a churn loop, printing GHC live-heap residency (measured after a
|
|
-- forced major GC) every checkpoint. A path whose residency climbs with the
|
|
-- iteration count is leaking; a flat path is clean.
|
|
--
|
|
-- Usage: smp-mem-bench <phase> <iterations>
|
|
--
|
|
-- Single-server phases (server on testPort):
|
|
-- plain | svc | svcrace | ntf | conc | svcsubs | getp | stuck | certchurn | link | ntfexp
|
|
-- tlsstall | tlshalf | tlschurn | tlspartial -- TLS/TCP stack
|
|
--
|
|
-- Two-server phases (proxy on testPort, lagged destination relay on testPort2):
|
|
-- proxyfwd | proxytmo | proxychurn | proxysess
|
|
--
|
|
-- Env: BENCHSTORE selects the store (see srvStoreCfg); SMP_LEAKDIAG_SEC sets the LEAKDIAG
|
|
-- interval (defaulted to 10s here). In two-server phases each LEAKDIAG line is tagged with the
|
|
-- listening port - "srv=5001" is the proxy, "srv=5002" the relay - because process-wide RTS
|
|
-- residency cannot attribute growth to one server.
|
|
module Main (main) where
|
|
|
|
import Control.Concurrent (threadDelay)
|
|
import Control.Concurrent.Async (concurrently_, forConcurrently_, mapConcurrently_, wait, withAsync)
|
|
import Control.Logger.Simple (LogConfig (..), LogLevel (..), setLogLevel, withGlobalLogging)
|
|
import qualified Control.Exception as E
|
|
import Control.Concurrent.STM
|
|
import Control.Monad
|
|
import Control.Monad.Trans.Except (ExceptT, runExceptT)
|
|
import Crypto.Random (ChaChaDRG)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import Data.Int (Int64)
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
import Data.Maybe (fromMaybe)
|
|
import Data.Time.Clock (getCurrentTime)
|
|
import qualified Data.X509.Validation as XV
|
|
import GHC.Stats
|
|
import qualified Network.Socket as N
|
|
import NetLag (LagTLS, clearLag, setDropSnd, setLag)
|
|
import SMPClient
|
|
import Simplex.Messaging.Client
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Protocol
|
|
import Simplex.Messaging.Server.Env.STM (AStoreType (..), ServerConfig (maxJournalMsgCount, msgQueueQuota, notificationExpiration))
|
|
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
|
|
import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..))
|
|
import Simplex.Messaging.Transport
|
|
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), defaultTransportClientConfig, runTransportClient)
|
|
import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials)
|
|
import Simplex.Messaging.Version (mkVersionRange)
|
|
import System.Environment (getArgs, lookupEnv, setEnv)
|
|
import System.Mem (performMajorGC)
|
|
import System.Timeout (timeout)
|
|
import Text.Printf (printf)
|
|
import Text.Read (readMaybe)
|
|
|
|
type H = THandleSMP TLS 'TClient
|
|
|
|
-- store config: PostgreSQL (matches production) when built with -fserver_postgres, else journal
|
|
benchCfg :: AServerConfig
|
|
#if defined(dbServerPostgres)
|
|
benchCfg = cfgMS (ASType SQSPostgres SMSPostgres)
|
|
#else
|
|
benchCfg = cfg
|
|
#endif
|
|
|
|
-- command helpers (copied from tests/ServerTests.hs) ------------------------
|
|
|
|
pattern Resp :: CorrId -> QueueId -> BrokerMsg -> Transmission (Either ErrorType BrokerMsg)
|
|
pattern Resp corrId queueId command <- (corrId, queueId, Right command)
|
|
|
|
pattern New :: RcvPublicAuthKey -> RcvPublicDhKey -> Command 'Creator
|
|
pattern New rPub dhPub = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) Nothing)
|
|
|
|
pattern New0 :: RcvPublicAuthKey -> RcvPublicDhKey -> Command 'Creator
|
|
pattern New0 rPub dhPub = NEW (NewQueueReq rPub dhPub Nothing SMOnlyCreate (Just (QRMessaging Nothing)) Nothing)
|
|
|
|
pattern Ids :: RecipientId -> SenderId -> RcvPublicDhKey -> BrokerMsg
|
|
pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure _linkId Nothing Nothing)
|
|
|
|
pattern Ids_ :: RecipientId -> SenderId -> RcvPublicDhKey -> ServiceId -> BrokerMsg
|
|
pattern Ids_ rId sId srvDh serviceId <- IDS (QIK rId sId srvDh _sndSecure _linkId (Just serviceId) Nothing)
|
|
|
|
pattern Msg :: MsgId -> MsgBody -> BrokerMsg
|
|
pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body}
|
|
|
|
_SEND :: MsgBody -> Command 'Sender
|
|
_SEND = SEND noMsgFlags
|
|
|
|
_SEND' :: MsgBody -> Command 'Sender
|
|
_SEND' = SEND MsgFlags {notification = True}
|
|
|
|
sendRecv :: forall p. PartyI p => H -> (Maybe TAuthorizations, ByteString, EntityId, Command p) -> IO (Transmission (Either ErrorType BrokerMsg))
|
|
sendRecv h@THandle {params} (sgn, corrId, qId, cmd) = do
|
|
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
|
Right () <- tPut1 h (sgn, tToSend)
|
|
tGet1 h
|
|
|
|
signSendRecv :: forall p. PartyI p => H -> C.APrivateAuthKey -> (ByteString, EntityId, Command p) -> IO (Transmission (Either ErrorType BrokerMsg))
|
|
signSendRecv h pk t = do
|
|
[r] <- signSendRecv_ h pk Nothing t
|
|
pure r
|
|
|
|
serviceSignSendRecv :: forall p. PartyI p => H -> C.APrivateAuthKey -> C.PrivateKeyEd25519 -> (ByteString, EntityId, Command p) -> IO (Transmission (Either ErrorType BrokerMsg))
|
|
serviceSignSendRecv h pk serviceKey t = do
|
|
[r] <- signSendRecv_ h pk (Just serviceKey) t
|
|
pure r
|
|
|
|
signSendRecv_ :: forall p. PartyI p => H -> C.APrivateAuthKey -> Maybe C.PrivateKeyEd25519 -> (ByteString, EntityId, Command p) -> IO (NonEmpty (Transmission (Either ErrorType BrokerMsg)))
|
|
signSendRecv_ h@THandle {params} (C.APrivateAuthKey a pk) serviceKey_ (corrId, qId, cmd) = do
|
|
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
|
Right () <- tPut1 h (authorize tForAuth, tToSend)
|
|
tGetClient h
|
|
where
|
|
authorize t = (,(`C.sign'` t) <$> serviceKey_) <$> case a of
|
|
C.SEd25519 -> Just . TASignature . C.ASignature C.SEd25519 $ C.sign' pk t'
|
|
C.SEd448 -> Just . TASignature . C.ASignature C.SEd448 $ C.sign' pk t'
|
|
C.SX25519 -> (\THAuthClient {peerServerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t') <$> thAuth params
|
|
#if !MIN_VERSION_base(4,18,0)
|
|
_sx448 -> undefined
|
|
#endif
|
|
where
|
|
t' = case (serviceKey_, thAuth params >>= clientService) of
|
|
(Just _, Just THClientService {serviceCertHash = XV.Fingerprint fp}) -> fp <> t
|
|
_ -> t
|
|
|
|
tPut1 :: H -> SentRawTransmission -> IO (Either TransportError ())
|
|
tPut1 h t = do
|
|
[r] <- tPut h [Right t]
|
|
pure r
|
|
|
|
tGet1 :: H -> IO (Transmission (Either ErrorType BrokerMsg))
|
|
tGet1 h = do
|
|
[r] <- tGetClient h
|
|
pure r
|
|
|
|
-- read and discard any pending transmissions until quiet, to resync after a race
|
|
drainAll :: H -> IO ()
|
|
drainAll h = timeout 40000 (tGet1 h) >>= maybe (pure ()) (const $ drainAll h)
|
|
|
|
-- measurement ---------------------------------------------------------------
|
|
|
|
liveBytesMiB :: IO Double
|
|
liveBytesMiB = do
|
|
performMajorGC
|
|
s <- getRTSStats
|
|
pure $ fromIntegral (gcdetails_live_bytes (gc s)) / (1024 * 1024)
|
|
|
|
report :: String -> Int -> Double -> Double -> IO ()
|
|
report phase i base cur =
|
|
printf "%-8s iter=%7d live=%9.1f MiB delta=%+9.1f MiB (%+.4f KiB/iter)\n"
|
|
phase i cur (cur - base) (if i == 0 then 0 else (cur - base) * 1024 / fromIntegral i)
|
|
|
|
withCheckpoints :: String -> Int -> Int -> (Int -> IO ()) -> IO ()
|
|
withCheckpoints phase iters cp step = do
|
|
base <- liveBytesMiB
|
|
report phase 0 base base
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
step i
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report phase i base
|
|
|
|
-- phases --------------------------------------------------------------------
|
|
|
|
-- regular recipient: create+subscribe, send, receive, ack, delete (churn)
|
|
runPlain :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runPlain g iters cp =
|
|
testSMPClient @TLS $ \recip ->
|
|
testSMPClient @TLS $ \sndr ->
|
|
withCheckpoints "plain" iters cp $ \i -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids rId sId _srvDh) <- signSendRecv recip rKey (corr, NoEntity, New rPub dhPub)
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND "hello")
|
|
Resp _ _ (Msg mId _) <- tGet1 recip
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, ACK mId)
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, DEL)
|
|
pure ()
|
|
|
|
-- recipient-service: create queue as service, send, service receives, ack, delete (churn)
|
|
runSvc :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runSvc g iters cp = do
|
|
creds <- genCredentials g Nothing (0, 2400) "localhost"
|
|
let (_fp, tlsCred) = tlsCredentials (creds :| [])
|
|
serviceKeys@(_, servicePK) <- atomically $ C.generateKeyPair g
|
|
testSMPClient @TLS $ \sndr ->
|
|
testSMPServiceClient @TLS (tlsCred, serviceKeys) $ \sh ->
|
|
withCheckpoints "svc" iters cp $ \i -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids_ rId sId _srvDh _serviceId) <- serviceSignSendRecv sh rKey servicePK (corr, NoEntity, New rPub dhPub)
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND "hello")
|
|
Resp _ _ (Msg mId _) <- tGet1 sh
|
|
Resp _ _ OK <- signSendRecv sh rKey (corr, rId, ACK mId)
|
|
Resp _ _ OK <- signSendRecv sh rKey (corr, rId, DEL)
|
|
pure ()
|
|
|
|
-- recipient-service with concurrent SEND vs DEL to probe the TOCTOU orphan
|
|
runSvcRace :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runSvcRace g iters cp = do
|
|
creds <- genCredentials g Nothing (0, 2400) "localhost"
|
|
let (_fp, tlsCred) = tlsCredentials (creds :| [])
|
|
serviceKeys@(_, servicePK) <- atomically $ C.generateKeyPair g
|
|
testSMPClient @TLS $ \sndr ->
|
|
testSMPServiceClient @TLS (tlsCred, serviceKeys) $ \sh ->
|
|
withCheckpoints "svcrace" iters cp $ \i -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids_ rId sId _srvDh _serviceId) <- serviceSignSendRecv sh rKey servicePK (corr, NoEntity, New rPub dhPub)
|
|
-- race an in-flight SEND (creates delivery Sub) against queue deletion
|
|
concurrently_
|
|
(void $ sendRecv sndr (Nothing, corr, sId, _SEND "hello"))
|
|
(void $ signSendRecv sh rKey (corr, rId, DEL))
|
|
drainAll sh
|
|
|
|
-- notifications: enable ntf on live queues and send ntf-flagged messages without draining
|
|
runNtf :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runNtf g iters cp =
|
|
testSMPClient @TLS $ \recip ->
|
|
testSMPClient @TLS $ \sndr ->
|
|
withCheckpoints "ntf" iters cp $ \i -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
(nPub, _nKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(rcvNtfPubDh, _dhNtfPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids rId sId _srvDh) <- signSendRecv recip rKey (corr, NoEntity, New rPub dhPub)
|
|
Resp _ _ (NID _nId _) <- signSendRecv recip rKey (corr, rId, NKEY nPub rcvNtfPubDh)
|
|
-- ntf-flagged send stores a notification; no notifier subscribed -> stays in ntfStore
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND' "hello")
|
|
Resp _ _ (Msg mId _) <- tGet1 recip
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, ACK mId)
|
|
pure ()
|
|
|
|
-- reusable steps ------------------------------------------------------------
|
|
|
|
genKeys :: TVar ChaChaDRG -> IO (RcvPublicAuthKey, C.APrivateAuthKey, RcvPublicDhKey)
|
|
genKeys g = do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
pure (rPub, rKey, dhPub)
|
|
|
|
plainStep :: TVar ChaChaDRG -> H -> H -> Int -> IO ()
|
|
plainStep g recip sndr i = do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids rId sId _) <- signSendRecv recip rKey (corr, NoEntity, New rPub dhPub)
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND "hello")
|
|
Resp _ _ (Msg mId _) <- tGet1 recip
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, ACK mId)
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, DEL)
|
|
pure ()
|
|
|
|
ntfStep :: TVar ChaChaDRG -> H -> H -> Int -> IO ()
|
|
ntfStep g recip sndr i = do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
(nPub, _nKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(rcvNtfPubDh, _dh :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids rId sId _) <- signSendRecv recip rKey (corr, NoEntity, New rPub dhPub)
|
|
Resp _ _ (NID _ _) <- signSendRecv recip rKey (corr, rId, NKEY nPub rcvNtfPubDh)
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND' "hello")
|
|
Resp _ _ (Msg mId _) <- tGet1 recip
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, ACK mId)
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, DEL)
|
|
pure ()
|
|
|
|
-- concurrency/scale: many client connections running a mixed workload
|
|
runConc :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runConc g iters _cp = do
|
|
let nW = 8
|
|
per = max 1 (iters `div` nW)
|
|
base <- liveBytesMiB
|
|
report "conc" 0 base base
|
|
counter <- newTVarIO (0 :: Int)
|
|
let target = nW * per
|
|
monitor = do
|
|
threadDelay 2000000
|
|
c <- readTVarIO counter
|
|
cur <- liveBytesMiB
|
|
report "conc" c base cur
|
|
when (c < target) monitor
|
|
worker w =
|
|
testSMPClient @TLS $ \recip ->
|
|
testSMPClient @TLS $ \sndr ->
|
|
forM_ ([1 .. per] :: [Int]) $ \i -> do
|
|
(if i `mod` 3 == 0 then ntfStep else plainStep) g recip sndr (w * per + i)
|
|
atomically $ modifyTVar' counter (+ 1)
|
|
withAsync monitor $ \_ -> mapConcurrently_ worker ([0 .. nW - 1] :: [Int])
|
|
cur <- liveBytesMiB
|
|
report "conc" target base cur
|
|
|
|
-- service SUBS reconnect: a fixed set of service queues, reconnect+resubscribe each iteration
|
|
runSvcSubs :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runSvcSubs g iters cp = do
|
|
creds <- genCredentials g Nothing (0, 2400) "localhost"
|
|
let (_fp, tlsCred) = tlsCredentials (creds :| [])
|
|
serviceKeys@(_, servicePK) <- atomically $ C.generateKeyPair g
|
|
let aServicePK = C.APrivateAuthKey C.SEd25519 servicePK
|
|
nQueues = 20
|
|
-- create nQueues associated with the service (persisted in the store)
|
|
(serviceId, rIds) <- testSMPServiceClient @TLS (tlsCred, serviceKeys) $ \sh -> do
|
|
xs <- forM ([1 .. nQueues] :: [Int]) $ \j -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids_ rId _sId _ sid) <- serviceSignSendRecv sh rKey servicePK (B.pack ("c" <> show j), NoEntity, New rPub dhPub)
|
|
pure (rId, sid)
|
|
pure (snd (head xs), map fst xs)
|
|
let idsHash = queueIdsHash rIds
|
|
base <- liveBytesMiB
|
|
report "svcsubs" 0 base base
|
|
-- each iteration: fresh service connection, SUBS to resubscribe all queues, then disconnect
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
testSMPServiceClient @TLS (tlsCred, serviceKeys) $ \sh ->
|
|
void $ signSendRecv_ sh aServicePK Nothing (B.pack (show i), serviceId, SUBS (fromIntegral nQueues) idsHash)
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report "svcsubs" i base
|
|
|
|
-- GET path: create-only (unsubscribed) queue, send, GET (poll), ack, delete
|
|
runGet :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runGet g iters cp =
|
|
testSMPClient @TLS $ \recip ->
|
|
testSMPClient @TLS $ \sndr ->
|
|
withCheckpoints "getp" iters cp $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids rId sId _) <- signSendRecv recip rKey (corr, NoEntity, New0 rPub dhPub)
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND "hello")
|
|
Resp _ _ (Msg mId _) <- signSendRecv recip rKey (corr, rId, GET)
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, ACK mId)
|
|
Resp _ _ OK <- signSendRecv recip rKey (corr, rId, DEL)
|
|
pure ()
|
|
|
|
-- forkDeliver blocked in SubPending: a subscriber that never reads its sndQ.
|
|
-- Deliveries to a full sndQ fork a deliverThread that blocks forever -> threads/subs_thread grow.
|
|
runStuck :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runStuck g iters cp =
|
|
testSMPClient @TLS $ \sndr ->
|
|
testSMPClient @TLS $ \recip -> do
|
|
-- phase 1: create + subscribe `iters` queues (recip reads only the IDS responses, no messages yet)
|
|
qs <- forM ([1 .. iters] :: [Int]) $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids _rId sId _) <- signSendRecv recip rKey (B.pack ('c' : show i), NoEntity, New rPub dhPub)
|
|
pure sId
|
|
-- phase 2: send one message to each; recip never reads -> server delivery threads block on the full sndQ
|
|
base <- liveBytesMiB
|
|
report "stuck" 0 base base
|
|
forM_ (zip ([1 ..] :: [Int]) qs) $ \(i, sId) -> do
|
|
_ <- sendRecv sndr (Nothing, B.pack ('s' : show i), sId, _SEND "x")
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report "stuck" i base
|
|
threadDelay 20000000 -- hold the subscriber open so diagnostics can sample the blocked threads
|
|
|
|
-- serviceLocks + services never evicted: connect as a messaging service with a fresh certificate
|
|
-- each iteration. getCreateService (run in the handshake) adds a services row + serviceLocks entry
|
|
-- that is never removed -> store_rcvServices grows.
|
|
runCertChurn :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runCertChurn g iters cp = do
|
|
base <- liveBytesMiB
|
|
report "certchurn" 0 base base
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
creds <- genCredentials g Nothing (0, 2400) "localhost"
|
|
let (_fp, tlsCred) = tlsCredentials (creds :| [])
|
|
serviceKeys <- atomically $ C.generateKeyPair g
|
|
testSMPServiceClient @TLS (tlsCred, serviceKeys) $ \_sh -> pure ()
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report "certchurn" i base
|
|
|
|
-- LINK path coverage: create a queue with short-link data, update it (LSET), secure it via the
|
|
-- link (LKEY), delete the link data (LDEL), delete the queue. Exercises the links map on create
|
|
-- and delete. (Not a leak repro: the links-not-removed-on-delete bug only bites useCache=True.)
|
|
runLink :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runLink g iters cp =
|
|
testSMPClient @TLS $ \r ->
|
|
testSMPClient @TLS $ \s ->
|
|
withCheckpoints "link" iters cp $ \i -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
(sPub, sKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
C.CbNonce corrId <- atomically $ C.randomCbNonce g
|
|
let sId = EntityId $ B.take 24 $ C.sha3_384 corrId -- sender ID must be derived from corrId
|
|
ld = (EncDataBytes "fixed data", EncDataBytes "user data")
|
|
qrd = QRMessaging $ Just (sId, ld)
|
|
Resp _ NoEntity (IDS (QIK rId _sId _srvDh _qm (Just lnkId) _svc _ntf)) <-
|
|
signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing))
|
|
Resp _ _ OK <- signSendRecv r rKey (B.pack ('a' : show i), rId, LSET lnkId ld)
|
|
Resp _ _ (LNK _sId2 _ld') <- signSendRecv s sKey (B.pack ('b' : show i), lnkId, LKEY sPub)
|
|
Resp _ _ OK <- signSendRecv r rKey (B.pack ('c' : show i), rId, LDEL)
|
|
Resp _ _ OK <- signSendRecv r rKey (B.pack ('d' : show i), rId, DEL)
|
|
pure ()
|
|
|
|
-- Note: the two proxy leaks are NOT reproducible in this load bench and are intentionally not
|
|
-- included. The sentCommands/PFWD-timeout leak needs a relay that keeps the session up but drops
|
|
-- RFWD (a mock relay). The empty-SessionVar leak is a precise disconnect-during-connect race
|
|
-- (reproduced deterministically by the SMPProxyTests unit test, not by a load loop). Both are
|
|
-- observable on a live proxy via the LEAKDIAG proxy_sentCommands / proxy_smpClients counters.
|
|
|
|
-- NtfStore key-retention leak: store notifications in many notifier queues, then let them expire.
|
|
-- With the fix, deleteExpiredNtfs removes the emptied outer keys, so LEAKDIAG ntfStore_keys rises
|
|
-- during creation then falls to ~0 after expiry; without it, the empty keys are retained.
|
|
-- Run with the short-expiry config (see main) so expiry fires within the run.
|
|
runNtfExp :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runNtfExp g iters _cp =
|
|
testSMPClient @TLS $ \recip ->
|
|
testSMPClient @TLS $ \sndr -> do
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
(nPub, _nKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(rcvNtfPubDh, _dh :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
let corr = B.pack (show i)
|
|
Resp _ _ (Ids rId sId _) <- signSendRecv recip rKey (corr, NoEntity, New0 rPub dhPub)
|
|
Resp _ _ (NID _nId _) <- signSendRecv recip rKey (corr, rId, NKEY nPub rcvNtfPubDh)
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, corr, sId, _SEND' "hi")
|
|
pure ()
|
|
base <- liveBytesMiB
|
|
report "ntfexp" iters base base
|
|
-- hold while notifications expire; LEAKDIAG samples ntfStore_keys over this window
|
|
forM_ ([1 .. 12] :: [Int]) $ \k -> threadDelay 5000000 >> (liveBytesMiB >>= report "ntfexp" (iters * 10 + k) base)
|
|
|
|
-- two-server topology: proxy + lagged relay ---------------------------------
|
|
--
|
|
-- The destination relay listens on LagTLS (see bench/NetLag.hs), so proxy->relay latency and
|
|
-- response-dropping are controlled from the bench without touching production code. The proxy
|
|
-- and all clients use plain TLS - LagTLS is wire-identical, only the local read/write path
|
|
-- differs.
|
|
--
|
|
-- Both servers log LEAKDIAG lines tagged with their listening port ("srv=5001" is the proxy,
|
|
-- "srv=5002" the relay), which is how per-server counters are attributed: process-wide RTS
|
|
-- residency conflates both servers with the bench clients.
|
|
|
|
proxySrv :: SMPServer
|
|
proxySrv = SMPServer testHost testPort testKeyHash
|
|
|
|
relaySrv :: SMPServer
|
|
relaySrv = SMPServer testHost2 testPort2 testKeyHash
|
|
|
|
-- an address with nothing listening, for connect-failure churn
|
|
deadSrv :: Int -> SMPServer
|
|
deadSrv i = SMPServer testHost2 (show (20000 + i)) testKeyHash
|
|
|
|
withProxyTopology :: Maybe String -> IO a -> IO a
|
|
withProxyTopology storeEnv action =
|
|
withSmpServerConfigOn (transport @TLS) (proxySrvCfg storeEnv) testPort $ \_ ->
|
|
withSmpServerConfigOn (transport @LagTLS) (relaySrvCfg storeEnv) testPort2 $ \_ ->
|
|
threadDelay 250000 >> action
|
|
|
|
proxySrvCfg :: Maybe String -> AServerConfig
|
|
proxySrvCfg = \case
|
|
#if defined(dbServerPostgres)
|
|
Just "pgjournal" -> proxyCfgMS (ASType SQSPostgres SMSJournal)
|
|
Just "journal" -> proxyCfgMS (ASType SQSMemory SMSJournal)
|
|
_ -> proxyCfgMS (ASType SQSPostgres SMSPostgres)
|
|
#else
|
|
_ -> proxyCfg
|
|
#endif
|
|
|
|
-- second store paths/db, so the relay does not collide with the proxy in one process.
|
|
-- Quota is raised (as SMPProxyTests does) so that forwarding under latency is not cut short by
|
|
-- QUOTA before the phase has run long enough to show a trend.
|
|
relaySrvCfg :: Maybe String -> AServerConfig
|
|
relaySrvCfg storeEnv = updateCfg (baseCfg storeEnv) $ \c -> c {msgQueueQuota = 128, maxJournalMsgCount = 256}
|
|
where
|
|
baseCfg = \case
|
|
#if defined(dbServerPostgres)
|
|
Just "journal" -> cfgJ2QS SQSMemory
|
|
_ -> cfgJ2QS SQSPostgres
|
|
#else
|
|
_ -> cfgJ2
|
|
#endif
|
|
|
|
-- a client connected to the proxy, able to issue PRXY/PFWD
|
|
proxyClient :: TVar ChaChaDRG -> Int64 -> IO SMPClient
|
|
proxyClient g n = do
|
|
ts <- getCurrentTime
|
|
getProtocolClient g NRMInteractive (n, proxySrv, Nothing) benchClientCfg [] Nothing ts (\_ -> pure ())
|
|
>>= either (fail . show) pure
|
|
|
|
benchClientCfg :: ProtocolClientConfig SMPVersion
|
|
benchClientCfg = defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion currentClientSMPRelayVersion}
|
|
|
|
runExceptT' :: Show e => ExceptT e IO a -> IO a
|
|
runExceptT' a = runExceptT a >>= either (fail . show) pure
|
|
|
|
-- a subscribed queue on the destination relay, with everything needed to drain it
|
|
data RelayQueue = RelayQueue
|
|
{ rqSndId :: SenderId,
|
|
rqRcvId :: RecipientId,
|
|
rqRcvKey :: C.APrivateAuthKey,
|
|
rqClient :: SMPClient,
|
|
rqMsgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg)
|
|
}
|
|
|
|
newRelayQueue :: TVar ChaChaDRG -> IO RelayQueue
|
|
newRelayQueue g = do
|
|
ts <- getCurrentTime
|
|
rqMsgQ <- newTBQueueIO 4096
|
|
rqClient <-
|
|
getProtocolClient g NRMInteractive (99, relaySrv, Nothing) benchClientCfg [] (Just rqMsgQ) ts (\_ -> pure ())
|
|
>>= either (fail . show) pure
|
|
(rPub, rqRcvKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(rdhPub, _rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
QIK {sndId = rqSndId, rcvId = rqRcvId} <-
|
|
runExceptT' $ createSMPQueue rqClient NRMInteractive Nothing (rPub, rqRcvKey) rdhPub Nothing SMSubscribe (QRMessaging Nothing) Nothing
|
|
pure RelayQueue {rqSndId, rqRcvId, rqRcvKey, rqClient, rqMsgQ}
|
|
|
|
-- receive and ack one delivered message, so a steady-forwarding phase does not hit QUOTA
|
|
ackOne :: RelayQueue -> IO ()
|
|
ackOne RelayQueue {rqRcvId, rqRcvKey, rqClient, rqMsgQ} = do
|
|
b <- atomically $ readTBQueue rqMsgQ
|
|
case b of
|
|
(_, _, [(_, STEvent (Right (MSG RcvMessage {msgId})))]) ->
|
|
runExceptT' $ ackSMPMessage rqClient rqRcvKey rqRcvId msgId
|
|
_ -> pure ()
|
|
|
|
-- baseline: steady forwarding through the proxy under moderate latency.
|
|
-- proxy_sentCommands (LEAKDIAG srv=5001) should stay flat - every RFWD is answered.
|
|
runProxyFwd :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runProxyFwd g iters cp = do
|
|
rq <- newRelayQueue g
|
|
pc <- proxyClient g 1
|
|
sess <- runExceptT' $ connectSMPProxiedRelay pc NRMInteractive relaySrv Nothing
|
|
setLag proxyLagUs proxyLagUs
|
|
-- a silently failing send would look identical to a clean one in the residency trace,
|
|
-- so the baseline phase must fail loudly instead of counting errors as "flat"
|
|
withCheckpoints "proxyfwd" iters cp $ \i -> do
|
|
runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "hello") >>= \case
|
|
Right (Right ()) -> pure ()
|
|
r -> fail $ "proxyfwd: forward failed at iteration " <> show i <> ": " <> show r
|
|
ackOne rq
|
|
clearLag
|
|
where
|
|
proxyLagUs = 50000 -- 50ms each way
|
|
|
|
-- HEADLINE REPRO: the relay keeps the session up but stops answering, so every RFWD the proxy
|
|
-- forwards times out. getResponse (Client.hs) sets `pending = False` and bumps the error count
|
|
-- but never deletes from `sentCommands` - the only removal is in processMsg when a response
|
|
-- actually arrives. Each stuck entry retains its RFWD command payload (EncFwdTransmission,
|
|
-- paddedProxiedTLength = 16226 bytes), and the session is never torn down because dropping the
|
|
-- client needs timeoutErrorCount >= smpPingCount AND 15 minutes of total silence, while the
|
|
-- proxy (party SSender) never pings.
|
|
--
|
|
-- Expect LEAKDIAG srv=5001 proxy_sentCommands to climb by `concurrency` per round and stay
|
|
-- there, with residency growing ~16 KiB per stuck command.
|
|
runProxyTmo :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runProxyTmo g iters _cp = do
|
|
rq <- newRelayQueue g
|
|
-- establish the proxy->relay session and prove it works before breaking it
|
|
pcs <- mapM (proxyClient g . fromIntegral) [1 .. nClients]
|
|
sess <- runExceptT' $ connectSMPProxiedRelay (head pcs) NRMInteractive relaySrv Nothing
|
|
-- prove forwarding works before breaking it, so a setup failure cannot masquerade as the leak
|
|
runExceptT (proxySMPMessage (head pcs) NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "warmup") >>= \case
|
|
Right (Right ()) -> pure ()
|
|
r -> fail $ "proxytmo: warmup forward failed, topology is broken: " <> show r
|
|
base <- liveBytesMiB
|
|
report "proxytmo" 0 base base
|
|
setDropSnd True
|
|
timeouts <- newTVarIO (0 :: Int)
|
|
let rounds = max 1 (iters `div` batch)
|
|
forM_ ([1 .. rounds] :: [Int]) $ \r -> do
|
|
-- all of these time out together; each leaves one entry in the proxy's sentCommands
|
|
forConcurrently_ ([1 .. batch] :: [Int]) $ \k -> do
|
|
let pc = pcs !! (k `mod` nClients)
|
|
r' <- runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "stuck")
|
|
-- only a response timeout leaves a stuck sentCommands entry; anything else means the
|
|
-- phase is measuring something other than the leak it claims to reproduce
|
|
case r' of
|
|
Left PCEResponseTimeout -> atomically $ modifyTVar' timeouts (+ 1)
|
|
_ -> pure ()
|
|
n <- readTVarIO timeouts
|
|
cur <- liveBytesMiB
|
|
report "proxytmo" (r * batch) base cur
|
|
-- Measured, not assumed: this stays flat at one batch rather than accumulating. The
|
|
-- bench clients give up at 20s (2 * interactive tcpTimeout) but the proxy still answers
|
|
-- them with PROXY (BROKER TIMEOUT) once its own RFWD expires at 30s, and that late
|
|
-- response deletes their entries in processMsg. Only the proxy->relay side leaks, so
|
|
-- process residency is NOT a doubled count of the payload.
|
|
clientStuck <- sum <$> mapM pClientSentCommandsCount pcs
|
|
printf "proxytmo timeouts=%d of %d attempted, benchClient_sentCommands=%d\n" n (r * batch) clientStuck
|
|
setDropSnd False
|
|
where
|
|
nClients = 8
|
|
batch = 64
|
|
|
|
-- PRXY to many distinct relay addresses that refuse the connection. Each failure stores
|
|
-- `Left (err, Just expiry)` in the agent's smpClients map; removal is lazy (only on a later
|
|
-- lookup of the same server), so addresses never requested again are retained.
|
|
-- Expect LEAKDIAG srv=5001 proxy_smpClients to grow monotonically.
|
|
runProxyChurn :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runProxyChurn g iters cp = do
|
|
pc <- proxyClient g 1
|
|
withCheckpoints "proxychurn" iters cp $ \i ->
|
|
void $ runExceptT (connectSMPProxiedRelay pc NRMInteractive (deadSrv i) Nothing)
|
|
|
|
-- PRXY against a relay that accepts TCP but never completes TLS, with the requesting client
|
|
-- disconnecting mid-connect.
|
|
--
|
|
-- NOT A CONFIRMED REPRO. This was written to probe the empty-SessionVar path in
|
|
-- getSMPServerClient'', and it does not reach it: measured over 100 iterations, the proxy ends
|
|
-- with proxy_smpClients=0, proxy_smpSessions=0, clients=0 and a thread count at baseline, both
|
|
-- 5s and 55s after the loop (i.e. before and after the 45s tcpConnectTimeout expires). The
|
|
-- withGetSessVar bracketOnError in Session.hs drops the empty var on the async exception, so
|
|
-- the race stays closed on this path.
|
|
--
|
|
-- Residency does climb ~108 KiB/iter, but with every server-side counter flat that growth is
|
|
-- bench-harness retention, not a server leak - do not read it as one. The phase is kept as
|
|
-- connect-abort churn coverage; reproducing the empty-SessionVar leak still needs the
|
|
-- deterministic unit-test-style race, as bench/MemBench.hs already noted for the proxy leaks.
|
|
runProxySess :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runProxySess g iters cp =
|
|
withStallingServerOn stallPort $ do
|
|
base <- liveBytesMiB
|
|
report "proxysess" 0 base base
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
pc <- proxyClient g (1000 + fromIntegral i)
|
|
-- start the relay connect, then drop the requesting client before it can finish
|
|
withAsync (void $ runExceptT (connectSMPProxiedRelay pc NRMInteractive stallSrv Nothing)) $ \_ ->
|
|
threadDelay 50000
|
|
closeProtocolClient pc
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report "proxysess" i base
|
|
where
|
|
stallPort = "5009"
|
|
stallSrv = SMPServer testHost2 stallPort testKeyHash
|
|
|
|
-- TLS/TCP stack --------------------------------------------------------------
|
|
|
|
-- These phases hold every connection open at once, so they are bounded by file descriptors
|
|
-- rather than by memory. Cap and say so - a silent truncation would read as "20000 connections
|
|
-- were fine" when only a fraction were ever opened.
|
|
maxHeldConns :: Int
|
|
maxHeldConns = 512
|
|
|
|
heldConns :: String -> Int -> IO Int
|
|
heldConns phase iters
|
|
| iters <= maxHeldConns = pure iters
|
|
| otherwise = do
|
|
printf "%s: capping held connections at %d (requested %d) to stay within the fd limit\n" phase maxHeldConns iters
|
|
pure maxHeldConns
|
|
|
|
rawConnect :: N.ServiceName -> IO N.Socket
|
|
rawConnect port = do
|
|
let hints = N.defaultHints {N.addrSocketType = N.Stream}
|
|
addr : _ <- N.getAddrInfo (Just hints) (Just "127.0.0.1") (Just port)
|
|
sock <- N.socket (N.addrFamily addr) (N.addrSocketType addr) (N.addrProtocol addr)
|
|
N.connect sock (N.addrAddress addr)
|
|
pure sock
|
|
|
|
-- Occupancy or leak? Hold `n` connections open at once, measure peak residency, then release
|
|
-- them all and measure again once the server has had time to drop its per-connection state.
|
|
-- Recovery to baseline means the phase measured the legitimate cost of a held connection; a
|
|
-- residency that stays elevated is a leak. Without this second measurement the two are
|
|
-- indistinguishable - the first version of these phases reported peak occupancy alone, which
|
|
-- reads like a leak and is not one.
|
|
holdRelease :: String -> Int -> (IO () -> Int -> IO ()) -> IO ()
|
|
holdRelease phase n conn = do
|
|
base <- liveBytesMiB
|
|
report phase 0 base base
|
|
release <- newTVarIO False
|
|
connected <- newTVarIO (0 :: Int)
|
|
let held = do
|
|
atomically $ modifyTVar' connected (+ 1)
|
|
atomically $ readTVar release >>= \r -> unless r retry
|
|
withAsync (forConcurrently_ ([1 .. n] :: [Int]) (conn held)) $ \as -> do
|
|
atomically $ readTVar connected >>= \c -> when (c < n) retry
|
|
peak <- liveBytesMiB
|
|
report phase n base peak
|
|
atomically $ writeTVar release True
|
|
wait as
|
|
printf "%s: peak=%.1f MiB (%+.2f KiB/conn)\n" phase peak ((peak - base) * 1024 / fromIntegral n)
|
|
-- Sample recovery repeatedly rather than once. A single early sample cannot tell a leak
|
|
-- from state the server has not reaped yet: the relevant server windows are 60s
|
|
-- (tlsSetupTimeout) and 60s (test smpHandshakeTimeout). Retention that keeps falling is
|
|
-- slow reaping; retention that plateaus above baseline is a leak.
|
|
foldM_
|
|
( \prev afterSec -> do
|
|
threadDelay $ (afterSec - prev) * 1000000
|
|
cur <- liveBytesMiB
|
|
printf
|
|
"%s: +%3ds recovered=%.1f MiB retained=%+.2f MiB (%+.3f KiB/conn)\n"
|
|
phase
|
|
afterSec
|
|
cur
|
|
(cur - base)
|
|
((cur - base) * 1024 / fromIntegral n)
|
|
pure afterSec
|
|
)
|
|
(0 :: Int)
|
|
([5, 25, 60, 120] :: [Int])
|
|
|
|
-- TCP connections that never send a ClientHello. Each occupies a server thread, an fd and a
|
|
-- SocketState entry until tlsSetupTimeout (60s) or until the peer closes.
|
|
--
|
|
-- RESULT (200 conns): peak 48.2 KiB/conn, 0.31 KiB/conn retained from +25s onwards. Clean.
|
|
runTlsStall :: Int -> Int -> IO ()
|
|
runTlsStall iters0 _cp = do
|
|
iters <- heldConns "tlsstall" iters0
|
|
holdRelease "tlsstall" iters $ \held _ ->
|
|
E.bracket (rawConnect testPort) N.close $ \_ -> held
|
|
|
|
-- TLS completes but the SMP handshake never starts: held until smpHandshakeTimeout (60s in the
|
|
-- test config) with no Client record ever allocated, so it is invisible to the LEAKDIAG client
|
|
-- counters - watch threads and CPSockets instead.
|
|
--
|
|
-- RESULT (200 conns): peak 203.1 KiB/conn, but 0.71 KiB/conn retained from +25s onwards. Clean.
|
|
-- Note the shape of the recovery curve: at +5s it still reads 124.6 KiB/conn, so a single early
|
|
-- sample reports this as a 24 MiB leak when it is teardown latency (gracefulClose holds each
|
|
-- connection up to 5s). The occupancy is still worth knowing - 200 abandoned half-open
|
|
-- connections pin ~40 MiB for ~25s with no authentication required.
|
|
runTlsHalf :: Int -> Int -> IO ()
|
|
runTlsHalf iters0 _cp = do
|
|
iters <- heldConns "tlshalf" iters0
|
|
holdRelease "tlshalf" iters $ \held _ ->
|
|
runTransportClient tcConfig Nothing (head' testHost) testPort (Just testKeyHash) $
|
|
\(_h :: TLS 'TClient) -> held
|
|
where
|
|
tcConfig = defaultTransportClientConfig {clientALPN = Just alpnSupportedSMPHandshakes} :: TransportClientConfig
|
|
head' (h :| _) = h
|
|
|
|
-- full connect + SMP handshake + disconnect churn. Exercises the accept path, per-connection
|
|
-- TBuffer allocation and the gracefulClose teardown residue.
|
|
runTlsChurn :: Int -> Int -> IO ()
|
|
runTlsChurn iters cp = do
|
|
base <- liveBytesMiB
|
|
report "tlschurn" 0 base base
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
testSMPClient @TLS $ \(_h :: THandleSMP TLS 'TClient) -> pure ()
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report "tlschurn" i base
|
|
|
|
-- post-handshake, send a partial block and idle. The server's transportTimeout is hardcoded
|
|
-- Nothing, so its receive thread blocks in cGet indefinitely; only inactive-client expiry
|
|
-- (6h by default, and only without subscriptions) would ever reap it.
|
|
-- RESULT (200 conns): peak 264.6 KiB/conn, 0.87 KiB/conn retained from +25s onwards. Clean -
|
|
-- the server has no read timeout here (transportTimeout is hardcoded Nothing at
|
|
-- Transport/Server.hs:104) so it never reaps these itself, but it does release everything
|
|
-- promptly once the peer disconnects. The exposure is occupancy while the peer stays connected:
|
|
-- a client that completes the SMP handshake and then sends one byte pins ~265 KiB indefinitely,
|
|
-- reapable only by inactive-client expiry (6h default, and only for clients with no
|
|
-- subscriptions).
|
|
runTlsPartial :: Int -> Int -> IO ()
|
|
runTlsPartial iters0 _cp = do
|
|
iters <- heldConns "tlspartial" iters0
|
|
holdRelease "tlspartial" iters $ \held _ ->
|
|
testSMPClient @TLS $ \h -> cPut (connection h) "partial" >> held
|
|
|
|
-- store config selectable via BENCHSTORE env: pgmsg (default, useCache=False) | pgjournal (useCache=True) | journal
|
|
srvStoreCfg :: Maybe String -> AServerConfig
|
|
srvStoreCfg = \case
|
|
#if defined(dbServerPostgres)
|
|
Just "pgjournal" -> cfgMS (ASType SQSPostgres SMSJournal)
|
|
Just "journal" -> cfgMS (ASType SQSMemory SMSJournal)
|
|
_ -> cfgMS (ASType SQSPostgres SMSPostgres)
|
|
#else
|
|
_ -> cfg
|
|
#endif
|
|
|
|
main :: IO ()
|
|
main = do
|
|
args <- getArgs
|
|
let (phase, iters) = case args of
|
|
(p : n : _) -> (p, read n)
|
|
[p] -> (p, 20000)
|
|
_ -> ("svc", 20000)
|
|
cp = max 1 (iters `div` 20)
|
|
g <- C.newRandom
|
|
storeEnv <- lookupEnv "BENCHSTORE"
|
|
-- ntfexp uses a short notification-expiration so deleteExpiredNtfs fires within the run
|
|
let srvCfg = case phase of
|
|
"ntfexp" -> updateCfg (srvStoreCfg storeEnv) $ \c -> c {notificationExpiration = ExpirationConfig {ttl = 2, checkInterval = 3}}
|
|
_ -> srvStoreCfg storeEnv
|
|
-- LEAKDIAG counters are the only per-server signal in multi-server topologies, so sample
|
|
-- them often enough to be useful over a bench run
|
|
leakDiagSec <- fromMaybe 10 . (>>= readMaybe) <$> lookupEnv "SMP_LEAKDIAG_SEC"
|
|
setEnv "SMP_LEAKDIAG_SEC" (show leakDiagSec)
|
|
setLogLevel LogInfo
|
|
withGlobalLogging LogConfig {lc_file = Nothing, lc_stderr = True} $
|
|
if phase `elem` proxyPhases
|
|
then withProxyTopology storeEnv $ settle leakDiagSec $ case phase of
|
|
"proxyfwd" -> runProxyFwd g iters cp
|
|
"proxytmo" -> runProxyTmo g iters cp
|
|
"proxychurn" -> runProxyChurn g iters cp
|
|
"proxysess" -> runProxySess g iters cp
|
|
_ -> error $ "unknown proxy phase: " <> phase
|
|
else withSmpServerConfigOn (transport @TLS) srvCfg testPort $ \_ -> settle leakDiagSec $ do
|
|
threadDelay 250000
|
|
case phase of
|
|
"plain" -> runPlain g iters cp
|
|
"svc" -> runSvc g iters cp
|
|
"svcrace" -> runSvcRace g iters cp
|
|
"ntf" -> runNtf g iters cp
|
|
"conc" -> runConc g iters cp
|
|
"svcsubs" -> runSvcSubs g iters cp
|
|
"getp" -> runGet g iters cp
|
|
"stuck" -> runStuck g iters cp
|
|
"certchurn" -> runCertChurn g iters cp
|
|
"link" -> runLink g iters cp
|
|
"ntfexp" -> runNtfExp g iters cp
|
|
"tlsstall" -> runTlsStall iters cp
|
|
"tlshalf" -> runTlsHalf iters cp
|
|
"tlschurn" -> runTlsChurn iters cp
|
|
"tlspartial" -> runTlsPartial iters cp
|
|
_ -> error $ "unknown phase: " <> phase
|
|
|
|
proxyPhases :: [String]
|
|
proxyPhases = ["proxyfwd", "proxytmo", "proxychurn", "proxysess"]
|
|
|
|
-- Hold the servers up past one LEAKDIAG interval after the phase finishes, so the end state is
|
|
-- always sampled at least once. Short phases would otherwise exit before any line is emitted,
|
|
-- leaving the per-server counters - the only attribution in a two-server topology - unobservable.
|
|
settle :: Int -> IO a -> IO a
|
|
settle leakDiagSec run = do
|
|
r <- run
|
|
threadDelay $ (leakDiagSec + 2) * 1000000
|
|
pure r
|