mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-06 14:17:20 +00:00
1955 lines
99 KiB
Haskell
1955 lines
99 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
|
|
-- ntfloop | ntfdeliver | subslice | conns | load | prodmix | cpsave -- PostgreSQL build only
|
|
-- tlsstall | tlshalf | tlschurn | tlspartial -- TLS/TCP stack
|
|
--
|
|
-- Two-server phases (proxy on testPort, lagged destination relay on testPort2):
|
|
-- proxyfwd | proxytmo | proxychurn | subtmo | pfwdbig
|
|
--
|
|
-- 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 (async, concurrently_, forConcurrently, 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.Foldable (toList)
|
|
import Data.List.NonEmpty (NonEmpty (..), fromList)
|
|
import qualified Data.Map.Strict as M
|
|
import qualified Data.List.NonEmpty as L
|
|
import Data.Functor ((<&>))
|
|
import GHC.Conc (listThreads)
|
|
import Data.Maybe (fromMaybe)
|
|
import Data.Time.Clock (diffUTCTime, getCurrentTime)
|
|
import qualified Data.X509.Validation as XV
|
|
import GHC.Profiling (requestHeapCensus)
|
|
import GHC.Stats
|
|
import qualified Network.Socket as N
|
|
import NetLag (LagTLS, clearLag, setDropEvery, setDropSnd, setLag)
|
|
import SMPClient
|
|
import Simplex.Messaging.Client
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Protocol
|
|
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (msgQSize, persistErrorInterval))
|
|
import Simplex.Messaging.Server.Env.STM (AStoreType (..), ServerConfig (controlPort, controlPortAdminAuth, maxJournalMsgCount, msgQueueQuota, notificationExpiration, ntfDeliveryInterval, serverClientConcurrency, smpAgentCfg, storeNtfsFile, tbqSize))
|
|
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
|
|
import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..))
|
|
import Simplex.Messaging.Transport
|
|
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), defaultTransportClientConfig, runTransportClient)
|
|
import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials)
|
|
import Simplex.Messaging.Version (mkVersionRange)
|
|
import System.Environment (getArgs, lookupEnv, setEnv)
|
|
import System.IO (BufferMode (..), IOMode (..), hClose, hGetLine, hPutStrLn, hSetBuffering, hSetNewlineMode, universalNewlineMode, withFile)
|
|
#if defined(dbServerPostgres)
|
|
import Database.PostgreSQL.Simple (ConnectInfo (..))
|
|
import Network.Socket (ServiceName)
|
|
import Simplex.Messaging.Agent.Store.Postgres.Options (DBOpts (..))
|
|
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..))
|
|
import Simplex.Messaging.Server.Env.STM (ServerStoreCfg (..), serverStoreCfg)
|
|
import Simplex.Messaging.Server.QueueStore.Postgres.Config (PostgresStoreCfg (..))
|
|
import qualified Data.ByteString.Builder as BLD
|
|
import Data.List (unfoldr)
|
|
import Data.Time.Clock.System (getSystemTime)
|
|
import Data.Word (Word64)
|
|
import Simplex.Messaging.Encoding.String (strEncode)
|
|
import Simplex.Messaging.Server.NtfStore (MsgNtf (..), NtfLogRecord (..))
|
|
import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile)
|
|
import System.IO.Unsafe (unsafePerformIO)
|
|
import System.Environment (getExecutablePath)
|
|
import System.IO (Handle, stdout)
|
|
import System.Process (CreateProcess (..), StdStream (..), callProcess, createProcess, proc, waitForProcess)
|
|
#endif
|
|
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 pk serviceKey_ t = do
|
|
Right () <- tPut1 h (signTransmission h pk serviceKey_ t)
|
|
tGetClient h
|
|
|
|
signTransmission :: forall p. PartyI p => H -> C.APrivateAuthKey -> Maybe C.PrivateKeyEd25519 -> (ByteString, EntityId, Command p) -> SentRawTransmission
|
|
signTransmission THandle {params} (C.APrivateAuthKey a pk) serviceKey_ (corrId, qId, cmd) =
|
|
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
|
in (authorize tForAuth, tToSend)
|
|
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
|
|
-- a major GC schedules the finalizers of everything that died since the last one (crypton keys
|
|
-- are finalized ScrubbedBytes); let them run, or their weak pointers and closures count as live
|
|
performMajorGC
|
|
threadDelay 200000
|
|
-- with +RTS -hT this makes the census coincide with the measurement; ignored otherwise
|
|
requestHeapCensus
|
|
performMajorGC
|
|
s <- getRTSStats
|
|
pure $ fromIntegral (gcdetails_live_bytes (gc s)) / (1024 * 1024)
|
|
|
|
-- memory in partially used blocks (mostly pinned blocks kept by a few live objects), as of the last GC
|
|
fragmentationMiB :: IO Double
|
|
fragmentationMiB = (/ (1024 * 1024)) . fromIntegral . gcdetails_block_fragmentation_bytes . gc <$> getRTSStats
|
|
|
|
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 ()
|
|
|
|
#if defined(dbServerPostgres)
|
|
-- Notification delivery loop cost with N stored notifiers.
|
|
--
|
|
-- deliverNtfsThread (Server.hs:380) runs every ntfDeliveryInterval, 1.5s in production. Each tick
|
|
-- takes every NtfStore key, including keys whose list is already empty, and resolves their services
|
|
-- with one `notifier_id IN ?` query read in full (QueueStore/Postgres.hs:538).
|
|
--
|
|
-- Seeds N notifier queues with SQL and N stored notifications through the server's own restore
|
|
-- file, then samples RTS counters with no client traffic. NTFLOOP_DELIVER=0 disables the loop, so
|
|
-- the difference between the two runs is the loop's own cost.
|
|
runNtfLoop :: Int -> IO ()
|
|
runNtfLoop n = postgressBracket benchDBConnectInfo $ do
|
|
deliver <- (/= Just "0") <$> lookupEnv "NTFLOOP_DELIVER"
|
|
secs <- fromMaybe 30 . (>>= readMaybe) <$> lookupEnv "NTFLOOP_SEC"
|
|
let productionInterval = 1500000
|
|
disabledInterval = 86400 * 1000000
|
|
srvCfg = updateCfg benchPgCfg $ \c ->
|
|
c {storeNtfsFile = Just testStoreNtfsFile, ntfDeliveryInterval = if deliver then productionInterval else disabledInterval}
|
|
createDirectoryIfMissing True "tests/tmp"
|
|
doesFileExist testStoreNtfsFile >>= (`when` removeFile testStoreNtfsFile)
|
|
-- the first start runs the migrations that create msg_queues
|
|
withSmpServerConfigOn (transport @TLS) srvCfg benchPort $ \_ -> threadDelay 500000
|
|
seedNotifierQueues n
|
|
writeNtfsFile n
|
|
printf "ntfloop: n=%d deliver=%s window=%ds\n" n (show deliver) secs
|
|
withSmpServerConfigOn (transport @TLS) srvCfg benchPort $ \_ -> do
|
|
performMajorGC
|
|
base <- getRTSStats
|
|
let mib :: Word64 -> Double
|
|
mib b = fromIntegral b / (1024 * 1024)
|
|
cpuNs s = mutator_cpu_ns s + gc_cpu_ns s
|
|
printf "ntfloop: after restore live=%.1f MiB mem_in_use=%.1f MiB max_mem_in_use=%.1f MiB\n"
|
|
(mib $ gcdetails_live_bytes $ gc base) (mib $ gcdetails_mem_in_use_bytes $ gc base) (mib $ max_mem_in_use_bytes base)
|
|
peak <- newTVarIO (0 :: Word64)
|
|
forM_ ([1 .. secs] :: [Int]) $ \t -> do
|
|
threadDelay 1000000
|
|
s <- getRTSStats
|
|
atomically $ modifyTVar' peak (max $ gcdetails_mem_in_use_bytes $ gc s)
|
|
when (t `mod` 5 == 0) $
|
|
printf "ntfloop: t=%3ds alloc=%8.1f MiB/s cpu=%5.2f cores mem_in_use=%8.1f MiB major_gcs=%d\n"
|
|
t
|
|
(mib (allocated_bytes s - allocated_bytes base) / fromIntegral t)
|
|
(fromIntegral (cpuNs s - cpuNs base) / (fromIntegral t * 1e9) :: Double)
|
|
(mib $ gcdetails_mem_in_use_bytes $ gc s)
|
|
(major_gcs s - major_gcs base)
|
|
end <- getRTSStats
|
|
pk <- readTVarIO peak
|
|
performMajorGC
|
|
after <- getRTSStats
|
|
printf "ntfloop: SUMMARY n=%d deliver=%s alloc=%.1f MiB/s (%.1f MiB per 1.5s tick) cpu=%.2f cores peak_mem_in_use=%.1f MiB max_mem_in_use=%.1f MiB live_end=%.1f MiB\n"
|
|
n
|
|
(show deliver)
|
|
(mib (allocated_bytes end - allocated_bytes base) / fromIntegral secs)
|
|
(1.5 * mib (allocated_bytes end - allocated_bytes base) / fromIntegral secs)
|
|
(fromIntegral (cpuNs end - cpuNs base) / (fromIntegral secs * 1e9) :: Double)
|
|
(mib pk)
|
|
(mib $ max_mem_in_use_bytes end)
|
|
(mib $ gcdetails_live_bytes $ gc after)
|
|
|
|
-- Its own database, user and port, so a bench run cannot collide with a concurrent test-suite run,
|
|
-- which binds testPort and drops and recreates test_server_db and test_server_user.
|
|
-- BENCHID (default 0) offsets the ports and names the database, so concurrent runs do not collide;
|
|
-- child processes inherit it.
|
|
benchId :: Int
|
|
benchId = unsafePerformIO $ fromMaybe 0 . (>>= readMaybe) <$> lookupEnv "BENCHID"
|
|
{-# NOINLINE benchId #-}
|
|
|
|
benchDBConnectInfo :: ConnectInfo
|
|
benchDBConnectInfo = testServerDBConnectInfo {connectUser = "mem_bench_user" <> show benchId, connectDatabase = "mem_bench_db" <> show benchId}
|
|
|
|
benchPort :: ServiceName
|
|
benchPort = show $ 15001 + 100 * benchId
|
|
|
|
benchPgCfg :: AServerConfig
|
|
benchPgCfg = case cfgMS (ASType SQSPostgres SMSPostgres) of
|
|
ASrvCfg SQSPostgres SMSPostgres c -> ASrvCfg SQSPostgres SMSPostgres c {serverStoreCfg = SSCDatabase storeCfg}
|
|
c -> c
|
|
where
|
|
storeCfg =
|
|
PostgresStoreCfg
|
|
{ dbOpts = testStoreDBOpts {connstr = B.pack $ "postgresql://" <> connectUser benchDBConnectInfo <> "@/" <> connectDatabase benchDBConnectInfo},
|
|
dbStoreLogPath = Nothing,
|
|
confirmMigrations = MCYesUp,
|
|
deletedTTL = 86400
|
|
}
|
|
|
|
benchClient :: (H -> IO a) -> IO a
|
|
benchClient client = do
|
|
Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost
|
|
testSMPClient_ useHost benchPort supportedClientSMPRelayVRange Nothing client
|
|
|
|
-- NtfStore keys left behind by delivered notifications.
|
|
--
|
|
-- With a subscribed ntf client, each tick flushes a notifier's list to [] (addNtfs) but keeps the
|
|
-- key, and every key is re-sent to getQueueNtfServices on every tick until deleteExpiredNtfs
|
|
-- removes it (hourly on this branch, never on stable). Creates `iters` notifier queues, subscribes
|
|
-- one ntf client to all of them, delivers one notification to each, then samples RTS counters while
|
|
-- nothing else happens. LEAKDIAG ntfStore_keys shows the retained keys.
|
|
runNtfDeliver :: TVar ChaChaDRG -> Int -> IO ()
|
|
runNtfDeliver g n = do
|
|
secs <- fromMaybe 30 . (>>= readMaybe) <$> lookupEnv "NTFLOOP_SEC"
|
|
benchClient $ \recip -> benchClient $ \sndr -> benchClient $ \nh -> do
|
|
sIds <- forM ([1 .. n] :: [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 _ _ (SOK Nothing) <- signSendRecv nh nKey (corr, nId, NSUB)
|
|
pure sId
|
|
forM_ (zip [1 :: Int ..] sIds) $ \(i, sId) -> do
|
|
Resp _ _ OK <- sendRecv sndr (Nothing, B.pack (show i), sId, _SEND' "hi")
|
|
pure ()
|
|
let awaitNtfs k = when (k > 0) $ do
|
|
rs :: NonEmpty (Transmission (Either ErrorType BrokerMsg)) <- tGetClient nh
|
|
awaitNtfs (k - length [() | (_, _, Right NMSG {}) <- toList rs])
|
|
awaitNtfs n
|
|
printf "ntfdeliver: n=%d notifications delivered, sampling %ds\n" n secs
|
|
performMajorGC
|
|
base <- getRTSStats
|
|
threadDelay $ secs * 1000000
|
|
end <- getRTSStats
|
|
let mib :: Word64 -> Double
|
|
mib b = fromIntegral b / (1024 * 1024)
|
|
cpuNs st = mutator_cpu_ns st + gc_cpu_ns st
|
|
printf "ntfdeliver: SUMMARY n=%d idle alloc=%.1f MiB/s cpu=%.2f cores\n"
|
|
n
|
|
(mib (allocated_bytes end - allocated_bytes base) / fromIntegral secs)
|
|
(fromIntegral (cpuNs end - cpuNs base) / (fromIntegral secs * 1e9) :: Double)
|
|
|
|
-- Control-port `save` on the PostgreSQL store.
|
|
--
|
|
-- CPSave (Server.hs:1153) runs saveServer False, and its closeMsgStore closes the DB pool
|
|
-- (closeDBStore drains and closes every pooled connection). The server keeps running, so the next
|
|
-- withConnectionPool waits forever on the empty pool while holding dbSem.
|
|
runCpSave :: TVar ChaChaDRG -> IO ()
|
|
runCpSave g = benchClient $ \h -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids _ _ _) <- signSendRecv h rKey ("1", NoEntity, New rPub dhPub)
|
|
putStrLn "cpsave: NEW before save: IDS"
|
|
r <- cpCommand benchCpPort "save" 2 30000000
|
|
printf "cpsave: save replied %s\n" (show r)
|
|
(rPub', rKey', dhPub') <- genKeys g
|
|
res <- timeout 10000000 $ signSendRecv h rKey' ("2", NoEntity, New rPub' dhPub')
|
|
putStrLn $ "cpsave: NEW after save: " <> maybe "no response in 10s (DB access blocked)" (const "responded") res
|
|
|
|
benchCpPort :: ServiceName
|
|
benchCpPort = show $ 15010 + 100 * benchId
|
|
|
|
-- The IDs are a tag byte followed by i in 23 big-endian bytes, the same bytes the SQL builds with
|
|
-- lpad(to_hex(i), 46, '0'), so file entries and rows match without passing IDs between them.
|
|
seqId :: Char -> Int -> ByteString
|
|
seqId tag i = B.cons tag $ B.replicate (23 - B.length be) '\0' <> be
|
|
where
|
|
be = B.pack . reverse $ unfoldr (\x -> if x == 0 then Nothing else Just (toEnum (x `mod` 256), x `div` 256)) i
|
|
|
|
seedNotifierQueues :: Int -> IO ()
|
|
seedNotifierQueues n =
|
|
callProcess "psql" ["-q", "-v", "ON_ERROR_STOP=1", "-U", "postgres", "-d", connectDatabase benchDBConnectInfo, "-c", sql]
|
|
where
|
|
hexId :: String -> String
|
|
hexId tag = "decode('" <> tag <> "' || lpad(to_hex(i), 46, '0'), 'hex')"
|
|
sql =
|
|
"INSERT INTO smp_server.msg_queues (recipient_id, recipient_keys, rcv_dh_secret, sender_id, notifier_id, notifier_key, rcv_ntf_dh_secret, status, updated_at) SELECT "
|
|
<> hexId "01" <> ", '\\x00', '\\x00', " <> hexId "02" <> ", " <> hexId "03" <> ", '\\x00', '\\x00', 'active', 0 FROM generate_series(1, "
|
|
<> show n <> ") AS i"
|
|
|
|
-- MsgNtf field sizes match mkMessageNotification: 24-byte msg id and nonce, 128-byte padded meta plus 16-byte tag
|
|
writeNtfsFile :: Int -> IO ()
|
|
writeNtfsFile n = do
|
|
ts <- getSystemTime
|
|
let ntf = MsgNtf {ntfMsgId = B.replicate 24 'm', ntfTs = ts, ntfNonce = C.cbNonce (B.replicate 24 'n'), ntfEncMeta = B.replicate 144 'e'}
|
|
withFile testStoreNtfsFile WriteMode $ \h ->
|
|
forM_ ([1 .. n] :: [Int]) $ \i ->
|
|
BLD.hPutBuilder h $ BLD.byteString (strEncode $ NLRv1 (EntityId $ seqId '\3' i) ntf) <> BLD.char8 '\n'
|
|
#endif
|
|
|
|
-- 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 ()
|
|
|
|
#if defined(dbServerPostgres)
|
|
-- Server memory per subscription.
|
|
--
|
|
-- The server stores the parsed entity ID as the subscription key (Server.hs:1839, :1971, and
|
|
-- queueSubscribers via subQ). The ID is an attoparsec slice of the decrypted ~16 KB block, so while
|
|
-- any key from a block is subscribed, the whole block stays live. NEW subscriptions are keyed by the
|
|
-- server-generated randomId instead.
|
|
--
|
|
-- The client runs in a child process (phase subclient), so the live bytes measured here are the
|
|
-- server's alone. SUBMODE=sub (default) creates `iters` queues without subscribing, then subscribes
|
|
-- them from a second connection, SUBBATCH per block (default 1); SUBKEEP=k keeps k subscriptions per
|
|
-- block and deletes the other queues. SUBMODE=new subscribes with NEW; NEWSUB=0 only creates.
|
|
runSubSlice :: Int -> IO ()
|
|
runSubSlice iters = do
|
|
mode <- fromMaybe "sub" <$> lookupEnv "SUBMODE"
|
|
base <- liveBytesMiB
|
|
fragBase <- fragmentationMiB
|
|
report "subslice" 0 base base
|
|
exe <- getExecutablePath
|
|
(Just hIn, Just hOut, _, ph) <- createProcess (proc exe ["subclient", show iters]) {std_in = CreatePipe, std_out = CreatePipe}
|
|
kept <- awaitReady hOut
|
|
cur <- liveBytesMiB
|
|
frag <- fragmentationMiB
|
|
report "subslice" iters base cur
|
|
printf "subslice: SUMMARY mode=%s subscribed=%d server_live_delta=%.1f MiB per_subscription=%.2f KiB block_fragmentation_per_subscription=%.2f KiB\n"
|
|
mode kept (cur - base) ((cur - base) * 1024 / fromIntegral (max 1 kept)) ((frag - fragBase) * 1024 / fromIntegral (max 1 kept))
|
|
hClose hIn
|
|
void $ waitForProcess ph
|
|
-- the server drops the connection's subscriptions on disconnect
|
|
threadDelay 1000000
|
|
after <- liveBytesMiB
|
|
printf "subslice: after disconnect server_live_delta=%.1f MiB\n" (after - base)
|
|
where
|
|
awaitReady :: Handle -> IO Int
|
|
awaitReady h = hGetLine h >>= \l -> case words l of
|
|
["READY", k] -> pure $ read k
|
|
_ -> awaitReady h
|
|
|
|
-- Server memory per idle connection. The child (phase connclient) opens `iters` SMP connections,
|
|
-- completes the handshakes, prints READY and holds them until stdin closes, so the live bytes
|
|
-- measured here are the server's alone.
|
|
runConns :: Int -> IO ()
|
|
runConns iters = do
|
|
base <- liveBytesMiB
|
|
report "conns" 0 base base
|
|
exe <- getExecutablePath
|
|
(Just hIn, Just hOut, _, ph) <- createProcess (proc exe ["connclient", show iters]) {std_in = CreatePipe, std_out = CreatePipe}
|
|
n <- awaitReadyLine hOut
|
|
cur <- liveBytesMiB
|
|
report "conns" n base cur
|
|
printf "conns: SUMMARY connections=%d server_live_delta=%.1f MiB per_connection=%.2f KiB\n" n (cur - base) ((cur - base) * 1024 / fromIntegral (max 1 n))
|
|
hClose hIn
|
|
void $ waitForProcess ph
|
|
threadDelay 2000000
|
|
after <- liveBytesMiB
|
|
printf "conns: after disconnect server_live_delta=%.1f MiB\n" (after - base)
|
|
|
|
runConnClient :: Int -> IO ()
|
|
runConnClient iters = do
|
|
hSetBuffering stdout LineBuffering
|
|
opened <- newTVarIO (0 :: Int)
|
|
release <- newEmptyTMVarIO
|
|
let hold = benchClient $ \_ -> do
|
|
atomically $ modifyTVar' opened (+ 1)
|
|
atomically $ readTMVar release
|
|
withAsync (forConcurrently_ ([1 .. iters] :: [Int]) $ \_ -> hold) $ \a -> do
|
|
atomically $ readTVar opened >>= \n -> when (n < iters) retry
|
|
putStrLn $ "READY " <> show iters
|
|
void (E.try getLine :: IO (Either E.IOException String))
|
|
atomically $ putTMVar release ()
|
|
wait a
|
|
|
|
awaitReadyLine :: Handle -> IO Int
|
|
awaitReadyLine h = hGetLine h >>= \l -> case words l of
|
|
["READY", k] -> pure $ read k
|
|
_ -> awaitReadyLine h
|
|
|
|
-- Server CPU under a fixed mixed workload, to compare RTS settings. The child (phase loadclient)
|
|
-- runs LOAD_WORKERS workers (default 16) for LOAD_SEC seconds (default 60); each worker reconnects
|
|
-- every 50 steps so TLS handshakes are part of the load, and every third step also uses notifications.
|
|
-- The server keeps the RTS flags given to this process; the child keeps its defaults.
|
|
runLoad :: IO ()
|
|
runLoad = do
|
|
performMajorGC
|
|
base <- getRTSStats
|
|
exe <- getExecutablePath
|
|
(_, Just hOut, _, ph) <- createProcess (proc exe ["loadclient", "0"]) {std_out = CreatePipe}
|
|
(ops, secs) <- awaitDone hOut
|
|
end <- getRTSStats
|
|
void $ waitForProcess ph
|
|
let cpu f = fromIntegral (f end - f base) / 1e9 :: Double
|
|
mutCpu = cpu mutator_cpu_ns
|
|
gcCpu = cpu gc_cpu_ns
|
|
printf "load: SUMMARY ops=%d secs=%.1f ops_per_sec=%.1f server_cpu_ms_per_op=%.3f mutator_cpu=%.1fs gc_cpu=%.1fs gc_share=%.1f%% gcs=%d major_gcs=%d max_mem_in_use=%.1f MiB\n"
|
|
ops secs (fromIntegral ops / secs) ((mutCpu + gcCpu) * 1000 / fromIntegral ops) mutCpu gcCpu (100 * gcCpu / (mutCpu + gcCpu))
|
|
(gcs end - gcs base) (major_gcs end - major_gcs base) (fromIntegral (max_mem_in_use_bytes end) / (1024 * 1024) :: Double)
|
|
where
|
|
awaitDone :: Handle -> IO (Int, Double)
|
|
awaitDone h = hGetLine h >>= \l -> case words l of
|
|
["DONE", o, t] -> pure (read o, read t)
|
|
_ -> awaitDone h
|
|
|
|
runLoadClient :: TVar ChaChaDRG -> IO ()
|
|
runLoadClient g = do
|
|
hSetBuffering stdout LineBuffering
|
|
workers <- fromMaybe 16 . (>>= readMaybe) <$> lookupEnv "LOAD_WORKERS"
|
|
secs <- fromMaybe 60 . (>>= readMaybe) <$> lookupEnv "LOAD_SEC"
|
|
ops <- newTVarIO (0 :: Int)
|
|
start <- getCurrentTime
|
|
let deadline = fromIntegral (secs :: Int)
|
|
worker w = do
|
|
t <- getCurrentTime
|
|
when (diffUTCTime t start < deadline) $ do
|
|
benchClient $ \recip -> benchClient $ \sndr ->
|
|
forM_ ([1 .. 50] :: [Int]) $ \i -> do
|
|
(if i `mod` 3 == 0 then ntfStep else plainStep) g recip sndr (w * 1000000 + i)
|
|
atomically $ modifyTVar' ops (+ 1)
|
|
worker w
|
|
forConcurrently_ ([1 .. workers] :: [Int]) worker
|
|
end <- getCurrentTime
|
|
n <- readTVarIO ops
|
|
putStrLn $ "DONE " <> show n <> " " <> show (realToFrac (diffUTCTime end start) :: Double)
|
|
|
|
-- Production-shaped server memory: C connections (PROD_CONNS, default 1000) each subscribing
|
|
-- Q queues (PROD_QUEUES, default 20) in batches of up to 100 per block, as the agent resubscribes,
|
|
-- plus PROD_NEW subscriptions per connection created with NEW; then PROD_SEC seconds (default 60)
|
|
-- of traffic where PROD_SENDERS connections (default 16) send to random queues and every recipient
|
|
-- acknowledges its messages. The client runs in a child process (phase prodclient); this process
|
|
-- reports the server's live heap, memory in use and RSS when idle and at peak under traffic.
|
|
runProdMix :: IO ()
|
|
runProdMix = do
|
|
base <- liveBytesMiB
|
|
rssBase <- rssMiB
|
|
exe <- getExecutablePath
|
|
(Just hIn, Just hOut, _, ph) <- createProcess (proc exe ["prodclient", "0"]) {std_in = CreatePipe, std_out = CreatePipe}
|
|
hSetBuffering hIn LineBuffering
|
|
(conns, subs) <- awaitLine hOut "READY" >>= \case
|
|
[c, n] -> pure (read c, read n) :: IO (Int, Int)
|
|
_ -> fail "bad READY line"
|
|
idle <- liveBytesMiB
|
|
idleStats <- getRTSStats
|
|
idleRss <- rssMiB
|
|
printf "prodmix: IDLE connections=%d subscriptions=%d live=%.1f MiB (%.2f KiB/conn) mem_in_use=%.1f MiB rss=%.1f MiB (base live=%.1f rss=%.1f)\n"
|
|
conns subs (idle - base) ((idle - base) * 1024 / fromIntegral conns) (mib $ gcdetails_mem_in_use_bytes $ gc idleStats) idleRss base rssBase
|
|
hPutStrLn hIn "GO"
|
|
peak <- newTVarIO (0 :: Double, 0 :: Double, 0 :: Double)
|
|
done <- newEmptyTMVarIO
|
|
let sampler = do
|
|
threadDelay 2000000
|
|
st <- getRTSStats
|
|
r <- rssMiB
|
|
atomically $ modifyTVar' peak $ \(m, l, rs) -> (max m (mib $ gcdetails_mem_in_use_bytes $ gc st), max l (mib $ gcdetails_live_bytes $ gc st), max rs r)
|
|
atomically (isEmptyTMVar done) >>= (`when` sampler)
|
|
msgs <- withAsync sampler $ \_ -> do
|
|
[n] <- awaitLine hOut "DONE"
|
|
atomically $ putTMVar done ()
|
|
pure (read n :: Int)
|
|
(pkMem, pkLive, pkRss) <- readTVarIO peak
|
|
end <- getRTSStats
|
|
after <- liveBytesMiB
|
|
printf "prodmix: TRAFFIC messages=%d peak_mem_in_use=%.1f MiB peak_live_after_gc=%.1f MiB peak_rss=%.1f MiB live_after=%.1f MiB major_gcs=%d gc_cpu=%.1fs mutator_cpu=%.1fs\n"
|
|
msgs pkMem pkLive pkRss (after - base) (major_gcs end - major_gcs idleStats)
|
|
(fromIntegral (gc_cpu_ns end - gc_cpu_ns idleStats) / 1e9 :: Double) (fromIntegral (mutator_cpu_ns end - mutator_cpu_ns idleStats) / 1e9 :: Double)
|
|
hClose hIn
|
|
void $ waitForProcess ph
|
|
where
|
|
mib :: Word64 -> Double
|
|
mib b = fromIntegral b / (1024 * 1024)
|
|
awaitLine :: Handle -> String -> IO [String]
|
|
awaitLine h tag = hGetLine h >>= \l -> case words l of
|
|
(t : rest) | t == tag -> pure rest
|
|
_ -> awaitLine h tag
|
|
|
|
-- resident set size of this process, from /proc
|
|
rssMiB :: IO Double
|
|
rssMiB = do
|
|
ls <- lines <$> readFile "/proc/self/status"
|
|
pure $ case [w | l <- ls, ("VmRSS:" : w : _) <- [words l]] of
|
|
(kb : _) -> read kb / 1024
|
|
_ -> 0
|
|
|
|
runProdClient :: TVar ChaChaDRG -> IO ()
|
|
runProdClient g = do
|
|
hSetBuffering stdout LineBuffering
|
|
let env k d = fromMaybe d . (>>= readMaybe) <$> lookupEnv k
|
|
conns <- env "PROD_CONNS" 1000
|
|
nq <- env "PROD_QUEUES" 20
|
|
newPer <- env "PROD_NEW" 2
|
|
secs <- env "PROD_SEC" 60
|
|
senders <- env "PROD_SENDERS" 16
|
|
let creators = 8 :: Int
|
|
total = conns * nq
|
|
-- queues are created on a few connections first, as if by earlier sessions
|
|
created <- forConcurrently ([0 .. creators - 1] :: [Int]) $ \w -> benchClient $ \h ->
|
|
forM [i | i <- [0 .. total - 1], i `mod` creators == w] $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids rId sId _) <- signSendRecv h rKey (B.pack $ "c" <> show i, NoEntity, New0 rPub dhPub)
|
|
pure (rId, sId, rKey)
|
|
let qs = concat created
|
|
perConn = chunksOf nq qs
|
|
subscribed <- newTVarIO (0 :: Int)
|
|
go <- newEmptyTMVarIO
|
|
stop <- newEmptyTMVarIO
|
|
sIdsVar <- newTVarIO ([] :: [SenderId])
|
|
received <- newTVarIO (0 :: Int)
|
|
let recipient cqs = benchClient $ \h -> do
|
|
forM_ (chunksOf 100 cqs) $ \b -> do
|
|
let sub (rId, _, rKey) = Right $ signTransmission h rKey Nothing (B.pack "s", rId, SUB)
|
|
void $ tPut h (fromList $ map sub b)
|
|
awaitResponses h (length b)
|
|
newQs <- forM ([1 .. newPer] :: [Int]) $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids rId sId _) <- signSendRecv h rKey (B.pack $ "n" <> show i, NoEntity, New rPub dhPub)
|
|
pure (rId, sId, rKey)
|
|
let keys = M.fromList [(rId, rKey) | (rId, _, rKey) <- cqs ++ newQs]
|
|
atomically $ do
|
|
modifyTVar' sIdsVar ([sId | (_, sId, _) <- cqs ++ newQs] ++)
|
|
modifyTVar' subscribed (+ (length cqs + length newQs))
|
|
-- acknowledge every delivered message until the run is released
|
|
withAsync (forever $ tGetClient h >>= mapM_ (ack h keys)) $ \_ -> atomically $ readTMVar stop
|
|
ack h keys = \case
|
|
(_, rId, Right (MSG RcvMessage {msgId})) | Just rKey <- M.lookup rId keys -> do
|
|
let t = signTransmission h rKey Nothing (B.pack "a", rId, ACK msgId)
|
|
void $ tPut h [Right t]
|
|
atomically $ modifyTVar' received (+ 1)
|
|
_ -> pure ()
|
|
sender w = benchClient $ \h -> do
|
|
atomically $ readTMVar go
|
|
sIds <- readTVarIO sIdsVar
|
|
let n = length sIds
|
|
loop i t0 = do
|
|
t <- getCurrentTime
|
|
when (diffUTCTime t t0 < fromIntegral (secs :: Int)) $ do
|
|
let sId = sIds !! ((i * 7919 + w * 104729) `mod` n)
|
|
void $ sendRecv h (Nothing, B.pack (show i), sId, _SEND "hello")
|
|
loop (i + 1) t0
|
|
getCurrentTime >>= loop 0
|
|
withAsync (forConcurrently_ perConn recipient) $ \rs -> do
|
|
atomically $ readTVar subscribed >>= \n -> when (n < total + conns * newPer) retry
|
|
putStrLn $ "READY " <> show conns <> " " <> show (total + conns * newPer)
|
|
"GO" <- getLine
|
|
atomically $ putTMVar go ()
|
|
forConcurrently_ ([1 .. senders] :: [Int]) sender
|
|
n <- readTVarIO received
|
|
putStrLn $ "DONE " <> show n
|
|
void (E.try getLine :: IO (Either E.IOException String))
|
|
atomically $ putTMVar stop ()
|
|
wait rs
|
|
where
|
|
chunksOf k xs = case splitAt k xs of
|
|
(c, []) -> [c | not (null c)]
|
|
(c, rest) -> c : chunksOf k rest
|
|
awaitResponses :: H -> Int -> IO ()
|
|
awaitResponses h n = when (n > 0) $ do
|
|
rs :: NonEmpty (Transmission (Either ErrorType BrokerMsg)) <- tGetClient h
|
|
awaitResponses h (n - length rs)
|
|
|
|
-- Client side of subslice: creates the subscriptions, prints "READY <subscribed>", and holds the
|
|
-- connection until stdin is closed.
|
|
runSubClient :: TVar ChaChaDRG -> Int -> IO ()
|
|
runSubClient g iters = do
|
|
hSetBuffering stdout LineBuffering
|
|
lookupEnv "SUBMODE" >>= \case
|
|
Just "new" -> do
|
|
subscribe <- (/= Just "0") <$> lookupEnv "NEWSUB"
|
|
benchClient $ \h -> do
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ Ids {} <- signSendRecv h rKey (B.pack $ "n" <> show i, NoEntity, (if subscribe then New else New0) rPub dhPub)
|
|
pure ()
|
|
ready $ if subscribe then iters else 0
|
|
-- create without subscribing, then SUB on the same connection
|
|
Just "newthensub" -> benchClient $ \h -> do
|
|
forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids rId _ _) <- signSendRecv h rKey (B.pack $ "n" <> show i, NoEntity, New0 rPub dhPub)
|
|
Resp _ _ (SOK Nothing) <- signSendRecv h rKey (B.pack $ "s" <> show i, rId, SUB)
|
|
pure ()
|
|
ready iters
|
|
_ -> do
|
|
batch <- fromMaybe 1 . (>>= readMaybe) <$> lookupEnv "SUBBATCH"
|
|
keep <- fromMaybe batch . (>>= readMaybe) <$> lookupEnv "SUBKEEP"
|
|
qs <- benchClient $ \h -> forM ([1 .. iters] :: [Int]) $ \i -> do
|
|
(rPub, rKey, dhPub) <- genKeys g
|
|
Resp _ _ (Ids rId _ _) <- signSendRecv h rKey (B.pack $ "c" <> show i, NoEntity, New0 rPub dhPub)
|
|
pure (i, rId, rKey)
|
|
benchClient $ \h -> do
|
|
forM_ (chunks batch qs) $ \b -> do
|
|
let sub (i, rId, rKey) = Right $ signTransmission h rKey Nothing (B.pack $ "s" <> show i, rId, SUB)
|
|
void $ tPut h (fromList $ map sub b)
|
|
awaitResponses h (length b)
|
|
forM_ (drop keep b) $ \(i, rId, rKey) -> do
|
|
Resp _ _ OK <- signSendRecv h rKey (B.pack $ "d" <> show i, rId, DEL)
|
|
pure ()
|
|
ready $ sum $ map (min keep . length) (chunks batch qs)
|
|
where
|
|
ready :: Int -> IO ()
|
|
ready k = do
|
|
putStrLn $ "READY " <> show k
|
|
void (E.try getLine :: IO (Either E.IOException String))
|
|
chunks n xs = case splitAt n xs of
|
|
(c, []) -> [c]
|
|
(c, rest) -> c : chunks n rest
|
|
awaitResponses :: H -> Int -> IO ()
|
|
awaitResponses h n = when (n > 0) $ do
|
|
rs :: NonEmpty (Transmission (Either ErrorType BrokerMsg)) <- tGetClient h
|
|
awaitResponses h (n - length rs)
|
|
#endif
|
|
|
|
-- 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 = withProxyTopologyCfg (proxySrvCfg storeEnv) storeEnv
|
|
|
|
withProxyTopologyCfg :: AServerConfig -> Maybe String -> IO a -> IO a
|
|
withProxyTopologyCfg pCfg storeEnv action =
|
|
withSmpServerConfigOn (transport @TLS) pCfg 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.
|
|
-- Bounded: the recipient also talks to the lagged relay, so delivery is delayed by the same
|
|
-- lag, and an unbounded wait would hang the high-latency runs.
|
|
ackOne :: Int -> RelayQueue -> IO ()
|
|
ackOne tmo RelayQueue {rqRcvId, rqRcvKey, rqClient, rqMsgQ} =
|
|
timeout tmo (atomically $ readTBQueue rqMsgQ) >>= \case
|
|
Just (_, _, [(_, STEvent (Right (MSG RcvMessage {msgId})))]) ->
|
|
void $ 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.
|
|
--
|
|
-- CONNECTIVITY UNDER LATENCY, swept with BENCHLAG_MS (one way, so a request/response pair costs
|
|
-- twice this). Sockets counted from /proc/<pid>/fd while the phase ran:
|
|
--
|
|
-- lag/way delivered sockets proxy->relay connects reconnects timeouts
|
|
-- 0ms 12/12 8 1 0 0
|
|
-- 500ms 10/10 8 1 0 0
|
|
-- 5s 6/6 8 1 0 0
|
|
-- 16s 4/4 8 1 0 0
|
|
-- 40s 0/2 8 1 0 1
|
|
--
|
|
-- Nothing accumulates. The socket count is identical whether forwards succeed or time out, the
|
|
-- proxy->relay session is opened once and reused throughout, and there are no reconnects at any
|
|
-- latency. proxy_smpClients and proxy_smpSessions stay at 1.
|
|
--
|
|
-- That robustness is exactly what makes Leak 1 unbounded: the session that holds the stuck
|
|
-- sentCommands entries never drops, so nothing ever frees them. A connection that failed under
|
|
-- latency would at least bound the damage.
|
|
--
|
|
-- Forwards succeed up to 16s each way and fail at 40s; the governing limit is the 30s RFWD
|
|
-- timeout. The exact cutoff is not pinned down, because the lag is applied per read/write cycle
|
|
-- on the relay rather than per message, so configured lag does not map exactly onto observed
|
|
-- round trip.
|
|
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
|
|
-- one-way lag added to every relay read and write, so a request/response cycle costs 2x this.
|
|
-- BENCHLAG_MS overrides it to sweep latency; see the connectivity notes below.
|
|
lagMs <- fromMaybe 50 . (>>= readMaybe) <$> lookupEnv "BENCHLAG_MS"
|
|
printf "proxyfwd: lag=%dms each way (%dms added per request/response)\n" lagMs (2 * lagMs :: Int)
|
|
setLag (lagMs * 1000) (lagMs * 1000)
|
|
ok <- newTVarIO (0 :: Int)
|
|
failed <- newTVarIO (0 :: Int)
|
|
-- Under latency a forward can fail without the phase being broken, so record outcomes
|
|
-- instead of aborting: the point is which latency band still delivers.
|
|
withCheckpoints "proxyfwd" iters cp $ \_i -> do
|
|
runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "hello") >>= \case
|
|
Right (Right ()) -> atomically (modifyTVar' ok (+ 1)) >> ackOne (4 * lagMs * 1000 + 20000000) rq
|
|
_ -> atomically $ modifyTVar' failed (+ 1)
|
|
clearLag
|
|
(o, f) <- (,) <$> readTVarIO ok <*> readTVarIO failed
|
|
printf "proxyfwd: delivered=%d failed=%d\n" o f
|
|
|
|
-- 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 ~20 KiB per stuck command.
|
|
--
|
|
-- MEASURED, three cases, and only the last one is a real leak:
|
|
--
|
|
-- 1. Relay slow but still replying (proxyfwd BENCHLAG_MS=16000): the late reply deletes the
|
|
-- entry, so the counter oscillates 1,0,1,0 and ends at 0. Latency alone does not leak.
|
|
-- 2. Relay stops replying, then traffic stops (BENCHHOLD_SEC, with or without BENCHDROP_EVERY):
|
|
-- counter holds, then goes to 0 at ~20 minutes with a disconnect. monitor (Client.hs:668)
|
|
-- exits once timeoutErrorCount >= smpPingCount and nothing has arrived for 900s, and it sits
|
|
-- in raceAny_ ... `finally` disconnected (Client.hs:649), so the client is torn down and the
|
|
-- map goes with it. Bounded.
|
|
-- 3. Sustained traffic with some replies dropped (BENCHDROP_EVERY=3, large iters so the phase
|
|
-- keeps sending): counter climbs 64,128,...,1280 over 20 minutes, linear, zero disconnects.
|
|
-- Received transmissions keep resetting lastReceived and timeoutErrorCount, so the drop
|
|
-- condition is never reached. Unbounded.
|
|
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
|
|
-- BENCHDROP_EVERY=n drops only every nth relay write instead of all of them. The replies that
|
|
-- do get through reset timeoutErrorCount and lastReceived on the proxy's client, so monitor
|
|
-- never reaches its drop condition and the session stays healthy while the unanswered
|
|
-- commands accumulate. This is the unbounded case; dropping everything is not.
|
|
dropEvery <- fromMaybe 0 . (>>= readMaybe) <$> lookupEnv "BENCHDROP_EVERY"
|
|
if dropEvery > 0 then setDropEvery dropEvery else 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
|
|
-- BENCHHOLD_SEC keeps the relay silent afterwards to see whether the proxy ever drops the
|
|
-- session and frees the map. monitor (Client.hs:668) exits, and so tears the client down, when
|
|
-- timeoutErrorCount >= smpPingCount and nothing has been received for recoverWindow (900s),
|
|
-- checked on a 600s loop. So a fully silent relay should be dropped at about 20 minutes.
|
|
-- Anything received resets both, which is why a relay that answers selectively is the
|
|
-- unbounded case rather than a silent one.
|
|
holdSec <- fromMaybe 0 . (>>= readMaybe) <$> lookupEnv "BENCHHOLD_SEC"
|
|
when (holdSec > 0) $ do
|
|
printf "proxytmo: holding relay silent for %ds to test session drop\n" (holdSec :: Int)
|
|
forM_ ([1 .. holdSec `div` 60] :: [Int]) $ \m -> do
|
|
threadDelay 60000000
|
|
cur <- liveBytesMiB
|
|
printf "proxytmo: hold +%dmin live=%.1f MiB\n" m cur
|
|
setDropSnd False
|
|
where
|
|
nClients = 8
|
|
batch = 64
|
|
|
|
-- The batched-subscribe leak, which is how the ntf server is exposed to the same bug as the
|
|
-- proxy. subscribeSMPQueues/subscribeSMPQueuesNtfs go through sendBatch, which calls getResponse
|
|
-- once per request (Client.hs:1344), so every queue in an unanswered batch leaves its own
|
|
-- sentCommands entry. The ntf server batches 1360 at a time (agentSubsBatchSize).
|
|
--
|
|
-- Measured on a bench-owned client so the count can be read directly with
|
|
-- pClientSentCommandsCount, rather than needing a full ntf server and its Postgres store: the
|
|
-- code path under test is identical.
|
|
runSubTmo :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runSubTmo g iters _cp = do
|
|
ts <- getCurrentTime
|
|
rc <-
|
|
getProtocolClient g NRMInteractive (7, relaySrv, Nothing) benchClientCfg [] Nothing ts (\_ -> pure ())
|
|
>>= either (fail . show) pure
|
|
-- create the queues while the relay still answers
|
|
qs <- forM ([1 .. iters] :: [Int]) $ \_ -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _dh :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
QIK {rcvId} <- runExceptT' $ createSMPQueue rc NRMInteractive Nothing (rPub, rKey) dhPub Nothing SMOnlyCreate (QRMessaging Nothing) Nothing
|
|
pure (rcvId, rKey)
|
|
before <- pClientSentCommandsCount rc
|
|
base <- liveBytesMiB
|
|
report "subtmo" 0 base base
|
|
setDropSnd True
|
|
rs <- subscribeSMPQueues rc (fromList qs)
|
|
let timedOut = length [() | Left PCEResponseTimeout <- toList rs]
|
|
stuck <- pClientSentCommandsCount rc
|
|
cur <- liveBytesMiB
|
|
report "subtmo" iters base cur
|
|
printf
|
|
"subtmo: queues=%d timedOut=%d sentCommands before=%d after=%d (%+.3f KiB/entry)\n"
|
|
iters
|
|
timedOut
|
|
before
|
|
stuck
|
|
(if stuck > before then (cur - base) * 1024 / fromIntegral (stuck - before) else 0)
|
|
setDropSnd False
|
|
|
|
-- Bug 4 reachability: forkClient registers the thread in endThreads AFTER forkIO, so an action
|
|
-- that finishes before the parent's insert leaves a permanently stale entry. Every ordinary
|
|
-- forked command (PFWD, PRXY, RSLV) blocks on the network, and a standalone reproduction of the
|
|
-- same registration order showed 0% staleness for any action that blocks even 1ms.
|
|
--
|
|
-- This phase probed the fast path I expected to reach it: a PFWD whose encBlock is large enough
|
|
-- that re-wrapping it as RFWD exceeds blockSize, so sendProtocolCommand_ would return TELargeMsg
|
|
-- at Client.hs:1368 without any IO.
|
|
--
|
|
-- RESULT: that path is NOT reachable. The client's own transmission limit caps encBlock before
|
|
-- the server's re-wrap can overflow. Largest block the client will send is ~16270 bytes (16275
|
|
-- is rejected by tPut), and at 16270 the proxy still forwards successfully - the relay answers
|
|
-- PROXY (PROTOCOL CRYPTO) on the garbage payload, which means the command did full IO. So no
|
|
-- size both fits the client and overflows the server. Kept as a boundary check in case block
|
|
-- sizes change; BENCHFWD_SZ sets the payload size.
|
|
runFastFwd :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runFastFwd g iters _cp =
|
|
testSMPClient @TLS $ \h -> do
|
|
(kPub, _kPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
r <- sendRecv h (Nothing, "prxy", NoEntity, PRXY relaySrv Nothing)
|
|
sessId <- case r of
|
|
Resp _ _ (PKEY sId _ _) -> pure sId
|
|
_ -> fail $ "fastfwd: PRXY did not return PKEY: " <> show r
|
|
-- oversized: the proxy re-wraps this into RFWD, which then exceeds blockSize
|
|
sz <- fromMaybe 16200 . (>>= readMaybe) <$> lookupEnv "BENCHFWD_SZ"
|
|
let big = EncTransmission $ B.replicate sz 'x'
|
|
base <- liveBytesMiB
|
|
report "fastfwd" 0 base base
|
|
oks <- forM ([1 .. iters] :: [Int]) $ \i -> do
|
|
r' <- E.try @E.SomeException $ sendRecv h (Nothing, B.pack ('f' : show i), EntityId sessId, PFWD currentClientSMPRelayVersion kPub big)
|
|
pure $ case r' of
|
|
Right (_, _, Right (ERR e)) -> Right (show e)
|
|
Right (_, _, resp) -> Right (take 40 $ show resp)
|
|
Left e -> Left (take 60 $ show e)
|
|
let sent = length [() | Right _ <- oks]
|
|
printf "fastfwd: size=%d sent=%d clientRejected=%d sample=%s\n" sz sent (iters - sent) (show $ take 1 oks)
|
|
liveBytesMiB >>= report "fastfwd" iters base
|
|
-- hold the connection open so LEAKDIAG can sample this client's endThreads
|
|
threadDelay 20000000
|
|
|
|
-- Bug 3: serverClientConcurrency is meant to cap concurrent proxied commands per connection,
|
|
-- but forkCmd is written `bracket_ wait signal . forkClient clnt label $ action`, which releases
|
|
-- the slot as soon as the thread is forked rather than when the work finishes.
|
|
--
|
|
-- With the cap set to 1 and the relay silent, N concurrent PFWDs on ONE connection would have to
|
|
-- serialise if the cap worked: each would hold the slot for the 30s RFWD timeout, and `wait`
|
|
-- blocks the client's whole command loop, so command k+1 could not even be read until k finished.
|
|
-- Completion times clustered together instead of spread ~30s apart mean the cap does nothing.
|
|
runConcLimit :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runConcLimit g iters _cp = do
|
|
rq <- newRelayQueue g
|
|
pc <- proxyClient g 1
|
|
sess <- runExceptT' $ connectSMPProxiedRelay pc NRMInteractive relaySrv Nothing
|
|
runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "warmup") >>= \case
|
|
Right (Right ()) -> pure ()
|
|
r -> fail $ "conclimit: warmup failed, topology broken: " <> show r
|
|
setDropSnd True
|
|
t0 <- getCurrentTime
|
|
ends <- forConcurrently ([1 .. iters] :: [Int]) $ \_ -> do
|
|
_ <- runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "x")
|
|
getCurrentTime
|
|
setDropSnd False
|
|
let secs = map (\t -> realToFrac (diffUTCTime t t0) :: Double) ends
|
|
printf
|
|
"conclimit: n=%d cap=1 completions first=%.1fs last=%.1fs spread=%.1fs\n"
|
|
iters
|
|
(minimum secs)
|
|
(maximum secs)
|
|
(maximum secs - minimum secs)
|
|
printf "conclimit: serialised would need ~%.0fs; clustered means the cap is not enforced\n" (fromIntegral iters * 30 :: Double)
|
|
|
|
-- 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)
|
|
|
|
-- Note: the empty-SessionVar leak is NOT covered here. It was fixed in c9ebf72e by the
|
|
-- bracketOnError/dropEmptySessVar in Session.hs withGetSessVar', and SMPProxyTests already has
|
|
-- deterministic regression tests for it ("reconnects to relay after sender disconnects
|
|
-- mid-connection" and "reconnects after a connect is cancelled mid-flight"). A load phase
|
|
-- cannot reproduce a fixed race, and an earlier attempt here only measured harness growth.
|
|
|
|
|
|
-- 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
|
|
|
|
-- Query the server control port for socket accounting. Used to check Bug 5 directly rather
|
|
-- than inferring it: socketsLeaked = accepted - closed - active, and closeConn removes a
|
|
-- connection from `active` before gracefulClose (up to 5s) and before it increments `closed`,
|
|
-- so a connection in teardown is counted in neither and shows up as leaked.
|
|
cpSockets :: N.ServiceName -> IO [String]
|
|
cpSockets port = fromMaybe [] <$> cpCommand port "sockets" 5 5000000 -- "Sockets for port N:" + accepted/closed/active/leaked
|
|
|
|
-- Run one admin command on the control port and read `n` reply lines, or Nothing on timeout.
|
|
cpCommand :: N.ServiceName -> String -> Int -> Int -> IO (Maybe [String])
|
|
cpCommand port cmd n tmo = do
|
|
sock <- rawConnect port
|
|
h <- N.socketToHandle sock ReadWriteMode
|
|
hSetBuffering h LineBuffering
|
|
hSetNewlineMode h universalNewlineMode
|
|
r <- timeout tmo $ do
|
|
_ <- hGetLine h -- banner line 1
|
|
_ <- hGetLine h -- banner line 2
|
|
hPutStrLn h "auth bench"
|
|
_ <- hGetLine h
|
|
hPutStrLn h cmd
|
|
replicateM n (hGetLine h)
|
|
hClose h `E.catch` \(_ :: E.SomeException) -> pure ()
|
|
pure r
|
|
|
|
cpPort :: N.ServiceName
|
|
cpPort = "5010"
|
|
|
|
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
|
|
beforeRel <- cpSockets cpPort
|
|
putStrLn $ phase <> ": sockets before release: " <> unwords (map (dropWhile (== ' ')) beforeRel)
|
|
atomically $ writeTVar release True
|
|
wait as
|
|
-- immediately after n simultaneous teardowns: the widest possible window for the
|
|
-- accounting gap in closeConn (active decremented before closed is incremented)
|
|
afterRel <- cpSockets cpPort
|
|
putStrLn $ phase <> ": sockets right after release: " <> unwords (map (dropWhile (== ' ')) afterRel)
|
|
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
|
|
-- sample the server's own socket accounting while churn is in flight, then again once it
|
|
-- has quiesced, to see whether socketsLeaked is a real leak or a teardown artefact
|
|
churning <- newTVarIO True
|
|
let sampler = do
|
|
threadDelay 1500000
|
|
readTVarIO churning >>= \go -> when go $ do
|
|
ls <- cpSockets cpPort
|
|
putStrLn $ "tlschurn: during churn: " <> unwords (map (dropWhile (== ' ')) ls)
|
|
sampler
|
|
withAsync sampler $ \_ -> forM_ ([1 .. iters] :: [Int]) $ \i -> do
|
|
testSMPClient @TLS $ \(_h :: THandleSMP TLS 'TClient) -> pure ()
|
|
when (i `mod` cp == 0) $ liveBytesMiB >>= report "tlschurn" i base
|
|
atomically $ writeTVar churning False
|
|
-- gracefulClose holds each connection up to 5s, so wait past that before the settled sample
|
|
threadDelay 8000000
|
|
ls <- cpSockets cpPort
|
|
putStrLn $ "tlschurn: after settling: " <> unwords (map (dropWhile (== ' ')) ls)
|
|
|
|
-- 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}}
|
|
-- tlschurn reads the server's own socket counters over the control port
|
|
p | p `elem` (["tlschurn", "tlsstall", "tlshalf", "tlspartial"] :: [String]) ->
|
|
updateCfg (srvStoreCfg storeEnv) $ \c -> c {controlPort = Just cpPort, controlPortAdminAuth = Just "bench"}
|
|
_ -> 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)
|
|
-- msgqfill: proxy agent msgQ size. Default 2 so the bound is reachable; set it to the
|
|
-- production 2048 to run the same phase as a control that must not stall.
|
|
msgQSz <- fromMaybe 2 . (>>= readMaybe) <$> lookupEnv "BENCHMSGQ"
|
|
setLogLevel LogInfo
|
|
withGlobalLogging LogConfig {lc_file = Nothing, lc_stderr = True} $
|
|
if phase `elem` proxyPhases
|
|
then
|
|
pgBracket
|
|
. ( case phase of
|
|
"conclimit" -> withProxyTopologyCfg (updateCfg (proxySrvCfg storeEnv) $ \c -> c {serverClientConcurrency = 1}) storeEnv
|
|
-- shrink the proxy agent's msgQ so its bound is reachable in one run
|
|
"msgqfill" -> withProxyTopologyCfg (updateCfg (proxySrvCfg storeEnv) $ \c -> c {smpAgentCfg = (smpAgentCfg c) {msgQSize = Just msgQSz}}) storeEnv
|
|
_ -> withProxyTopology storeEnv
|
|
)
|
|
$ settle leakDiagSec
|
|
$ case phase of
|
|
"proxyfwd" -> runProxyFwd g iters cp
|
|
"proxytmo" -> runProxyTmo g iters cp
|
|
"proxychurn" -> runProxyChurn g iters cp
|
|
"subtmo" -> runSubTmo g iters cp
|
|
"conclimit" -> runConcLimit g iters cp
|
|
"fastfwd" -> runFastFwd g iters cp
|
|
"msgqfill" -> runMsgQFill g iters cp
|
|
"pfwdbig" -> runPfwdBig g iters cp
|
|
_ -> error $ "unknown proxy phase: " <> phase
|
|
#if defined(dbServerPostgres)
|
|
else if phase == "ntfloop" then runNtfLoop iters
|
|
else if phase == "ntfdeliver" then postgressBracket benchDBConnectInfo $ withSmpServerConfigOn (transport @TLS) (updateCfg benchPgCfg $ \c -> c {ntfDeliveryInterval = 1500000}) benchPort $ \_ -> runNtfDeliver g iters
|
|
else if phase == "cpsave" then
|
|
postgressBracket benchDBConnectInfo $
|
|
withSmpServerConfigOn (transport @TLS) (updateCfg benchPgCfg $ \c -> c {controlPort = Just benchCpPort, controlPortAdminAuth = Just "bench"}) benchPort $ \_ -> runCpSave g
|
|
else if phase == "subslice" then postgressBracket benchDBConnectInfo $ withSmpServerConfigOn (transport @TLS) benchPgCfg benchPort $ \_ -> runSubSlice iters
|
|
else if phase == "subclient" then runSubClient g iters
|
|
else if phase == "conns" then postgressBracket benchDBConnectInfo $ withSmpServerConfigOn (transport @TLS) benchPgCfg benchPort $ \_ -> runConns iters
|
|
else if phase == "connclient" then runConnClient iters
|
|
else if phase == "load" then postgressBracket benchDBConnectInfo $ withSmpServerConfigOn (transport @TLS) benchPgCfg benchPort $ \_ -> runLoad
|
|
else if phase == "loadclient" then runLoadClient g
|
|
else if phase == "prodmix" then postgressBracket benchDBConnectInfo $ withSmpServerConfigOn (transport @TLS) benchPgCfg benchPort $ \_ -> runProdMix
|
|
else if phase == "prodclient" then runProdClient g
|
|
else if phase == "srvchild" then runSrvChild (args !! 1)
|
|
else if phase == "mesh" then postgressBracket benchDBConnectInfo $ runMesh g iters
|
|
#endif
|
|
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
|
|
|
|
-- Leak 3: the proxy agent's msgQ has no reader.
|
|
--
|
|
-- newSMPClientAgent creates one msgQ (Client/Agent.hs:194) and connectClient passes that same
|
|
-- queue to every relay client (:296). The ntf server drains its copy (Notifications/Server.hs:537);
|
|
-- the SMP server does not - receiveFromProxyAgent reads agentQ only (Server.hs:475). So on a
|
|
-- proxy the queue only ever fills.
|
|
--
|
|
-- What fills it: processMsg routes a response to msgQ when the request is found but `pending` is
|
|
-- already False (Client.hs:713), i.e. the reply arrived after the proxy's own RFWD timeout. A
|
|
-- relay that answers late therefore deposits one entry per late reply, permanently.
|
|
--
|
|
-- Once full, `process` blocks in writeTBQueue (Client.hs:694). It is the only reader of rcvQ, so
|
|
-- every later response from that relay goes unprocessed and every forward times out. msgQ is
|
|
-- shared across relays, so this is not confined to the relay that caused it.
|
|
--
|
|
-- Phase: force late replies until the queue is full, then clear the lag and try to forward again.
|
|
-- A healthy proxy answers; a stalled one cannot. Run with BENCHMSGQ=2048 as the control.
|
|
runMsgQFill :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runMsgQFill g iters _cp = do
|
|
rq <- newRelayQueue g
|
|
pc <- proxyClient g 1
|
|
sess <- runExceptT' $ connectSMPProxiedRelay pc NRMInteractive relaySrv Nothing
|
|
-- must exceed the proxy's 30s RFWD timeout each way, or the reply is matched while still
|
|
-- pending and never reaches msgQ (measured: 20s each way is too little, 40s is enough)
|
|
lagMs <- fromMaybe 40000 . (>>= readMaybe) <$> lookupEnv "BENCHLAG_MS"
|
|
printf "msgqfill: lag=%dms each way, %d forwards to strand\n" lagMs iters
|
|
setLag (lagMs * 1000) (lagMs * 1000)
|
|
forM_ ([1 .. iters] :: [Int]) $ \_ ->
|
|
void $ runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "late")
|
|
clearLag
|
|
-- let the stranded replies arrive and land in msgQ
|
|
printf "msgqfill: lag cleared, waiting %ds for late replies\n" (3 * lagMs `div` 1000)
|
|
threadDelay $ 3 * lagMs * 1000
|
|
-- recovery probe: no lag now, so a proxy whose process thread still runs must answer
|
|
ok <- newTVarIO (0 :: Int)
|
|
forM_ ([1 .. 3] :: [Int]) $ \_ ->
|
|
runExceptT (proxySMPMessage pc NRMInteractive sess Nothing (rqSndId rq) noMsgFlags "probe") >>= \case
|
|
Right (Right ()) -> atomically $ modifyTVar' ok (+ 1)
|
|
_ -> pure ()
|
|
o <- readTVarIO ok
|
|
printf "msgqfill: recovery forwards succeeded=%d/3 (0 means the proxy stalled)\n" o
|
|
|
|
-- Leak 1 without relay cooperation: an oversized PFWD.
|
|
--
|
|
-- A client that declares proxyServer = True in its handshake gets no block encryption, so a PFWD
|
|
-- whose encBlock is 16260-16266 bytes still fits its block (the PFWD length is not validated). The
|
|
-- proxy's RFWD to the relay is then too large for the relay block: sendRecv returns TELargeMsg
|
|
-- before sending, and the Request stays in the proxy's sentCommands for the life of the
|
|
-- proxy-relay session. LEAKDIAG proxy_sentCommands (srv=5001) counts the entries.
|
|
runPfwdBig :: TVar ChaChaDRG -> Int -> Int -> IO ()
|
|
runPfwdBig g iters cp = do
|
|
size <- fromMaybe 16260 . (>>= readMaybe) <$> lookupEnv "PFWDSIZE"
|
|
ts <- getCurrentTime
|
|
pc <- getProtocolClient g NRMInteractive (1, proxySrv, Nothing) benchClientCfg {proxyServer = True} [] Nothing ts (\_ -> pure ())
|
|
>>= either (fail . show) pure
|
|
ProxiedRelay {prSessionId, prVersion} <- runExceptT' $ connectSMPProxiedRelay pc NRMInteractive relaySrv Nothing
|
|
(k, _) <- atomically $ C.generateKeyPair @'C.X25519 g
|
|
let pfwd = Cmd SProxiedClient $ PFWD prVersion k $ EncTransmission $ B.replicate size 'a'
|
|
r <- runExceptT $ sendProtocolCommand pc NRMInteractive Nothing (EntityId prSessionId) pfwd
|
|
printf "pfwdbig: size=%d first response: %s\n" size (show r)
|
|
withCheckpoints "pfwdbig" iters cp $ \_ ->
|
|
void $ runExceptT $ sendProtocolCommand pc NRMInteractive Nothing (EntityId prSessionId) pfwd
|
|
|
|
#if defined(dbServerPostgres)
|
|
-- Proxy mesh with each server in its own process, so the live bytes of the proxy and of the relay
|
|
-- are measured separately and without the bench clients. Both use the pure PostgreSQL store and the
|
|
-- production queue sizes (tbqSize 128, client_concurrency 32 unless MESH_CONC is set, quota 128).
|
|
-- Ports and database follow BENCHID, so runs do not need the test lock.
|
|
--
|
|
-- MESH selects the scenario, `iters` its size:
|
|
-- sessions PRXY to `iters` aliases of the relay (same host and port, distinct second host), so the
|
|
-- proxy opens `iters` relay sessions and the relay holds `iters` proxy connections
|
|
-- inflight MESH_CONNS client connections each with MESH_K concurrent PFWDs; the relay stops
|
|
-- answering (MESH_RELAY=drop, default) or stops reading (MESH_RELAY=stall)
|
|
-- rate MESH_CONNS connections sending `iters` PFWD/s in total for MESH_SEC seconds with
|
|
-- MESH_LAG_MS one-way relay lag
|
|
-- throughput MESH_CONNS connections x MESH_K concurrent PFWDs back to back for MESH_SEC seconds, then
|
|
-- the same concurrency sending SEND directly to the relay
|
|
-- slowreader `iters` raw connections sending PFWD without ever reading a response
|
|
runMesh :: TVar ChaChaDRG -> Int -> IO ()
|
|
runMesh g n = do
|
|
hSetBuffering stdout LineBuffering
|
|
scen <- fromMaybe "inflight" <$> lookupEnv "MESH"
|
|
withSrvChild "relay" $ \relay -> withSrvChild "proxy" $ \proxy -> do
|
|
let measureBoth tag = do
|
|
p <- childMeasure proxy
|
|
r <- childMeasure relay
|
|
printf "mesh %s: proxy %s | relay %s\n" (tag :: String) (showM p) (showM r)
|
|
pure (p, r)
|
|
case scen of
|
|
"sessions" -> meshSessions g n measureBoth
|
|
"inflight" -> meshInflight g relay measureBoth
|
|
"rate" -> meshRate g n relay measureBoth
|
|
"throughput" -> meshThroughput g measureBoth
|
|
"slowreader" -> meshSlowReader g n measureBoth
|
|
_ -> error $ "unknown MESH scenario: " <> scen
|
|
|
|
data ChildMem = ChildMem {cmLive :: Double, cmFrag :: Double, cmInUse :: Double, cmThreads :: Int}
|
|
|
|
showM :: ChildMem -> String
|
|
showM ChildMem {cmLive, cmFrag, cmInUse, cmThreads} = printf "live=%.1f frag=%.1f in_use=%.1f threads=%d" cmLive cmFrag cmInUse cmThreads
|
|
|
|
deltaPer :: String -> Int -> (ChildMem, ChildMem) -> (ChildMem, ChildMem) -> IO ()
|
|
deltaPer unit k (p0, r0) (p1, r1) =
|
|
printf "mesh SUMMARY per %s (n=%d): proxy %+.2f KiB live, %+.2f KiB in_use, %+.2f threads | relay %+.2f KiB live, %+.2f KiB in_use, %+.2f threads\n"
|
|
unit k (per cmLive p0 p1) (per cmInUse p0 p1) (perT p0 p1) (per cmLive r0 r1) (per cmInUse r0 r1) (perT r0 r1)
|
|
where
|
|
per f a b = (f b - f a) * 1024 / fromIntegral (max 1 k)
|
|
perT a b = fromIntegral (cmThreads b - cmThreads a) / fromIntegral (max 1 k) :: Double
|
|
|
|
data SrvChild = SrvChild {chIn :: Handle, chOut :: Handle}
|
|
|
|
withSrvChild :: String -> (SrvChild -> IO a) -> IO a
|
|
withSrvChild role action = do
|
|
exe <- getExecutablePath
|
|
rts <- maybe ["+RTS", "-N", "-F1.2", "-A16m", "-I0.01", "-Iw15", "-T", "-RTS"] words <$> lookupEnv ("MESH_RTS_" <> role)
|
|
(Just hIn, Just hOut, _, ph) <- createProcess (proc exe (["srvchild", role] <> rts)) {std_in = CreatePipe, std_out = CreatePipe}
|
|
hSetBuffering hIn LineBuffering
|
|
let c = SrvChild hIn hOut
|
|
(childReply c >> action c) `E.finally` (hClose hIn >> waitForProcess ph)
|
|
|
|
childReply :: SrvChild -> IO [String]
|
|
childReply c@SrvChild {chOut} = hGetLine chOut >>= \l -> case words l of
|
|
"R" : ws -> pure ws
|
|
_ -> childReply c
|
|
|
|
childCmd :: SrvChild -> String -> IO [String]
|
|
childCmd c@SrvChild {chIn} cmd = hPutStrLn chIn cmd >> childReply c
|
|
|
|
childMeasure :: SrvChild -> IO ChildMem
|
|
childMeasure c = childCmd c "m" >>= \case
|
|
[l, f, u, t] -> pure $ ChildMem (read l) (read f) (read u) (read t)
|
|
r -> fail $ "bad child reply: " <> unwords r
|
|
|
|
-- a server of the mesh; replies to each stdin command with one line starting with "R"
|
|
runSrvChild :: String -> IO ()
|
|
runSrvChild role = do
|
|
hSetBuffering stdout LineBuffering
|
|
case role of
|
|
"proxy" -> withSmpServerConfigOn (transport @TLS) meshProxyCfg benchPort $ \_ -> serve
|
|
"relay" -> withSmpServerConfigOn (transport @LagTLS) meshRelayCfg meshRelayPort $ \_ -> serve
|
|
_ -> error $ "unknown server role: " <> role
|
|
where
|
|
serve = putStrLn "R ready" >> loop
|
|
loop = (E.try getLine :: IO (Either E.IOException String)) >>= either (const $ pure ()) (\l -> cmd (words l) >> loop)
|
|
cmd = \case
|
|
["m"] -> do
|
|
live <- liveBytesMiB
|
|
frag <- fragmentationMiB
|
|
inUse <- (/ (1024 * 1024)) . fromIntegral . gcdetails_mem_in_use_bytes . gc <$> getRTSStats
|
|
ts <- length <$> listThreads
|
|
printf "R %.3f %.3f %.3f %d\n" live frag (inUse :: Double) ts
|
|
["lag", r, s] -> setLag (read r * 1000) (read s * 1000) >> putStrLn "R ok"
|
|
["drop", b] -> setDropSnd (b == "1") >> putStrLn "R ok"
|
|
["clear"] -> clearLag >> putStrLn "R ok"
|
|
c -> putStrLn $ "R unknown " <> unwords c
|
|
|
|
meshSrvCfg :: Int -> ServerConfig s -> ServerConfig s
|
|
meshSrvCfg conc c = c {tbqSize = 128, msgQueueQuota = 128, serverClientConcurrency = conc}
|
|
|
|
meshConc :: Int
|
|
meshConc = unsafePerformIO $ fromMaybe 32 . (>>= readMaybe) <$> lookupEnv "MESH_CONC"
|
|
{-# NOINLINE meshConc #-}
|
|
|
|
meshProxyCfg :: AServerConfig
|
|
meshProxyCfg = updateCfg (meshPgCfg "smp_server" $ proxyCfgMS (ASType SQSPostgres SMSPostgres)) $ \c ->
|
|
c {smpAgentCfg = (smpAgentCfg c) {persistErrorInterval = 30}}
|
|
|
|
meshRelayCfg :: AServerConfig
|
|
meshRelayCfg = meshPgCfg "smp_server2" $ cfgMS (ASType SQSPostgres SMSPostgres)
|
|
|
|
meshPgCfg :: String -> AServerConfig -> AServerConfig
|
|
meshPgCfg sch = \case
|
|
ASrvCfg SQSPostgres SMSPostgres c -> ASrvCfg SQSPostgres SMSPostgres (meshSrvCfg meshConc c) {serverStoreCfg = SSCDatabase (meshStoreCfg sch)}
|
|
c -> c
|
|
|
|
-- the BENCHID database, one schema per server
|
|
meshStoreCfg :: String -> PostgresStoreCfg
|
|
meshStoreCfg sch = PostgresStoreCfg {dbOpts = testStoreDBOpts {connstr, schema = B.pack sch}, dbStoreLogPath = Nothing, confirmMigrations = MCYesUp, deletedTTL = 86400}
|
|
where
|
|
connstr = B.pack $ "postgresql://" <> connectUser benchDBConnectInfo <> "@/" <> connectDatabase benchDBConnectInfo
|
|
|
|
meshRelayPort :: ServiceName
|
|
meshRelayPort = show $ 15002 + 100 * benchId
|
|
|
|
meshProxySrv :: SMPServer
|
|
meshProxySrv = SMPServer testHost benchPort testKeyHash
|
|
|
|
meshRelaySrv :: SMPServer
|
|
meshRelaySrv = SMPServer testHost2 meshRelayPort testKeyHash
|
|
|
|
envInt :: String -> Int -> IO Int
|
|
envInt name def = fromMaybe def . (>>= readMaybe) <$> lookupEnv name
|
|
|
|
-- sender IDs of fresh unsubscribed queues on the relay, created directly
|
|
relayQueues :: TVar ChaChaDRG -> Int -> IO [SenderId]
|
|
relayQueues g k = do
|
|
ts <- getCurrentTime
|
|
rc <- getProtocolClient g NRMInteractive (98, meshRelaySrv, Nothing) benchClientCfg [] Nothing ts (\_ -> pure ()) >>= either (fail . show) pure
|
|
qs <- forM ([1 .. k] :: [Int]) $ \_ -> do
|
|
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(dhPub, _ :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
|
QIK {sndId} <- runExceptT' $ createSMPQueue rc NRMInteractive Nothing (rPub, rKey) dhPub Nothing SMOnlyCreate (QRMessaging Nothing) Nothing
|
|
pure sndId
|
|
closeProtocolClient rc
|
|
pure qs
|
|
|
|
meshProxyClient :: TVar ChaChaDRG -> Int64 -> IO SMPClient
|
|
meshProxyClient g n = do
|
|
ts <- getCurrentTime
|
|
getProtocolClient g NRMInteractive (n, meshProxySrv, Nothing) benchClientCfg [] Nothing ts (\_ -> pure ()) >>= either (fail . show) pure
|
|
|
|
-- client connections to the proxy, each with its own PRXY session reference
|
|
proxyClients :: TVar ChaChaDRG -> Int -> IO [(SMPClient, ProxiedRelay)]
|
|
proxyClients g k = forM ([1 .. k] :: [Int]) $ \i -> do
|
|
pc <- meshProxyClient g (fromIntegral i)
|
|
sess <- runExceptT' $ connectSMPProxiedRelay pc NRMInteractive meshRelaySrv Nothing
|
|
pure (pc, sess)
|
|
|
|
forwardOnce :: SMPClient -> ProxiedRelay -> SenderId -> IO Bool
|
|
forwardOnce pc sess sId =
|
|
runExceptT (proxySMPMessage pc NRMInteractive sess Nothing sId noMsgFlags "hello") <&> \case
|
|
Right (Right ()) -> True
|
|
Left (PCEProtocolError QUOTA) -> True
|
|
_ -> False
|
|
|
|
meshSessions :: TVar ChaChaDRG -> Int -> (String -> IO (ChildMem, ChildMem)) -> IO ()
|
|
meshSessions g n measureBoth = do
|
|
pc <- meshProxyClient g 1
|
|
base <- measureBoth "before"
|
|
forM_ ([1 .. n] :: [Int]) $ \i -> runExceptT (connectSMPProxiedRelay pc NRMInteractive (alias i) Nothing) >>= \case
|
|
Right _ -> pure ()
|
|
Left e -> fail $ "PRXY " <> show i <> ": " <> show e
|
|
cur <- measureBoth (show n <> " relay sessions")
|
|
deltaPer "relay session" n base cur
|
|
where
|
|
alias i = SMPServer (L.head testHost2 :| [THDomainName $ "alias" <> show i <> ".invalid"]) meshRelayPort testKeyHash
|
|
|
|
meshInflight :: TVar ChaChaDRG -> SrvChild -> (String -> IO (ChildMem, ChildMem)) -> IO ()
|
|
meshInflight g relay measureBoth = do
|
|
conns <- envInt "MESH_CONNS" 16
|
|
k <- envInt "MESH_K" 32
|
|
mode <- fromMaybe "drop" <$> lookupEnv "MESH_RELAY"
|
|
sIds <- relayQueues g 1
|
|
pcs <- proxyClients g conns
|
|
forM_ pcs $ \(pc, sess) -> forwardOnce pc sess (head sIds)
|
|
base <- measureBoth "before"
|
|
void $ childCmd relay $ if mode == "stall" then "lag 60000 0" else "drop 1"
|
|
as <- forM pcs $ \(pc, sess) -> forM ([1 .. k] :: [Int]) $ \_ -> async $ forwardOnce pc sess (head sIds)
|
|
threadDelay 8000000
|
|
cur <- measureBoth $ printf "%d x %d forwards in flight, relay %s" conns k mode
|
|
deltaPer "in-flight forward" (conns * min k meshConc) base cur
|
|
mapM_ (mapM_ wait) as
|
|
void $ childCmd relay "clear"
|
|
threadDelay 45000000
|
|
void $ measureBoth "45s after the clients timed out"
|
|
|
|
meshRate :: TVar ChaChaDRG -> Int -> SrvChild -> (String -> IO (ChildMem, ChildMem)) -> IO ()
|
|
meshRate g rate relay measureBoth = do
|
|
conns <- envInt "MESH_CONNS" 64
|
|
secs <- envInt "MESH_SEC" 30
|
|
lagMs <- envInt "MESH_LAG_MS" 100
|
|
sIds <- relayQueues g 256
|
|
pcs <- proxyClients g conns
|
|
forM_ pcs $ \(pc, sess) -> forwardOnce pc sess (head sIds)
|
|
base <- measureBoth "before"
|
|
void $ childCmd relay $ "lag " <> show lagMs <> " " <> show lagMs
|
|
ok <- newTVarIO (0 :: Int)
|
|
failed <- newTVarIO (0 :: Int)
|
|
let perConn = fromIntegral rate / fromIntegral conns :: Double
|
|
intervalUs = round (1000000 / perConn) :: Int
|
|
sender i (pc, sess) = forM_ ([1 .. round (perConn * fromIntegral secs)] :: [Int]) $ \j -> do
|
|
void $ async $ forwardOnce pc sess (sIds !! ((i * 7 + j) `mod` length sIds)) >>= \r -> atomically $ modifyTVar' (if r then ok else failed) (+ 1)
|
|
threadDelay intervalUs
|
|
sampler = forever $ do
|
|
threadDelay 5000000
|
|
(o, f) <- (,) <$> readTVarIO ok <*> readTVarIO failed
|
|
void $ measureBoth $ printf "rate=%d/s lag=%dms ok=%d failed=%d" rate lagMs o f
|
|
cur <- withAsync sampler $ \_ -> do
|
|
mapConcurrently_ (uncurry sender) (zip [0 ..] pcs)
|
|
measureBoth "end of sending"
|
|
deltaPer "forward/s" rate base cur
|
|
void $ childCmd relay "clear"
|
|
threadDelay 35000000
|
|
(o, f) <- (,) <$> readTVarIO ok <*> readTVarIO failed
|
|
printf "mesh rate: ok=%d failed=%d\n" o f
|
|
|
|
meshThroughput :: TVar ChaChaDRG -> (String -> IO (ChildMem, ChildMem)) -> IO ()
|
|
meshThroughput g measureBoth = do
|
|
conns <- envInt "MESH_CONNS" 16
|
|
k <- envInt "MESH_K" 32
|
|
secs <- envInt "MESH_SEC" 20
|
|
sIds <- relayQueues g 1024
|
|
pcs <- proxyClients g conns
|
|
forM_ pcs $ \(pc, sess) -> forwardOnce pc sess (head sIds)
|
|
void $ measureBoth "before"
|
|
let run label go = do
|
|
cnt <- newTVarIO (0 :: Int)
|
|
t0 <- getCurrentTime
|
|
let worker w = loop (0 :: Int)
|
|
where
|
|
loop j = do
|
|
t <- getCurrentTime
|
|
when (diffUTCTime t t0 < fromIntegral secs) $ do
|
|
r <- go w (sIds !! ((w * 31 + j) `mod` length sIds))
|
|
when r $ atomically $ modifyTVar' cnt (+ 1)
|
|
loop (j + 1)
|
|
forConcurrently_ ([0 .. conns * k - 1] :: [Int]) worker
|
|
c <- readTVarIO cnt
|
|
printf "mesh throughput %s: %d ok in %ds = %.0f/s (%d connections x %d concurrent)\n" (label :: String) c secs (fromIntegral c / fromIntegral secs :: Double) conns k
|
|
run "via proxy" $ \w sId -> let (pc, sess) = pcs !! (w `mod` conns) in forwardOnce pc sess sId
|
|
void $ measureBoth "after proxy run"
|
|
ts <- getCurrentTime
|
|
rcs <- forM ([1 .. conns] :: [Int]) $ \i -> getProtocolClient g NRMInteractive (fromIntegral i, meshRelaySrv, Nothing) benchClientCfg [] Nothing ts (\_ -> pure ()) >>= either (fail . show) pure
|
|
run "direct SEND" $ \w sId ->
|
|
runExceptT (sendSMPMessage (rcs !! (w `mod` conns)) NRMInteractive Nothing sId noMsgFlags "hello") <&> \case
|
|
Right () -> True
|
|
Left (PCEProtocolError QUOTA) -> True
|
|
_ -> False
|
|
|
|
-- An unauthenticated client that sends valid PFWDs and never reads: the proxy forwards them, and the
|
|
-- responses accumulate in its sndQ, then in the forked command threads, then in its rcvQ.
|
|
meshSlowReader :: TVar ChaChaDRG -> Int -> (String -> IO (ChildMem, ChildMem)) -> IO ()
|
|
meshSlowReader g n measureBoth = do
|
|
secs <- envInt "MESH_SEC" 30
|
|
sIds <- relayQueues g 1
|
|
(pc, sess) <- head <$> proxyClients g 1
|
|
_ <- forwardOnce pc sess (head sIds)
|
|
base <- measureBoth "before"
|
|
sent <- newTVarIO (0 :: Int)
|
|
let attacker = benchClient $ \h -> do
|
|
t <- pfwdTransmission g (thParams' h) sess (head sIds)
|
|
forever $ tPut1 h (Nothing, t) >> atomically (modifyTVar' sent (+ 1))
|
|
thParams' THandle {params} = params
|
|
-- not awaited: closing TLS sends an alert, which blocks as the proxy no longer reads, so the
|
|
-- connections stay until the process exits
|
|
void $ async $ forConcurrently_ ([1 .. n] :: [Int]) $ \_ -> attacker
|
|
threadDelay $ secs * 1000000
|
|
s <- readTVarIO sent
|
|
cur <- measureBoth $ printf "%d non-reading clients, %d PFWD accepted by TCP" n s
|
|
deltaPer "non-reading client" n base cur
|
|
|
|
-- the PFWD a client builds in proxySMPCommand, as one transmission for a raw connection
|
|
pfwdTransmission :: TVar ChaChaDRG -> THandleParams SMPVersion 'TClient -> ProxiedRelay -> SenderId -> IO ByteString
|
|
pfwdTransmission g params ProxiedRelay {prSessionId, prVersion, prServerKey} sId = do
|
|
let serverThAuth = (\ta -> ta {peerServerPubKey = prServerKey}) <$> thAuth params
|
|
serverThParams = smpTHParamsSetVersion prVersion params {sessionId = prSessionId, thAuth = serverThAuth}
|
|
(cmdPubKey, cmdPrivKey) <- atomically $ C.generateKeyPair @'C.X25519 g
|
|
nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g
|
|
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND noMsgFlags "hello")
|
|
b <- case batchTransmissions serverThParams [Right (Nothing, tToSend)] of
|
|
TBTransmission s _ : _ -> pure s
|
|
TBTransmissions s _ _ : _ -> pure s
|
|
_ -> fail "pfwdTransmission: batch"
|
|
et <- either (fail . show) (pure . EncTransmission) $ C.cbEncrypt (C.dh' prServerKey cmdPrivKey) nonce b paddedProxiedTLength
|
|
let TransmissionForAuth {tToSend = t} = encodeTransmissionForAuth params (CorrId corrId, EntityId prSessionId, Cmd SProxiedClient $ PFWD prVersion cmdPubKey et)
|
|
pure t
|
|
#endif
|
|
|
|
proxyPhases :: [String]
|
|
proxyPhases =["proxyfwd", "proxytmo", "proxychurn", "subtmo", "conclimit", "fastfwd", "msgqfill", "pfwdbig"]
|
|
|
|
-- proxy topologies use the test databases; create them for the run and drop them after
|
|
pgBracket :: IO a -> IO a
|
|
#if defined(dbServerPostgres)
|
|
pgBracket = postgressBracket testServerDBConnectInfo
|
|
#else
|
|
pgBracket = id
|
|
#endif
|
|
|
|
-- 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
|