From e73efaf5dfdfda1eb52bd515283ac3b7b47d9dce Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 30 Sep 2026 11:26:14 +0000 Subject: [PATCH 01/10] smp-server: hold command slot until completion --- src/Simplex/Messaging/Server.hs | 10 +++++++--- tests/RSLVTests.hs | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index e2cef6f50..dbe67b8ae 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1495,11 +1495,15 @@ client -- Run a slow command on a thread forkCmd :: (ServerConfig s -> Int) -> CorrId -> EntityId -> M s BrokerMsg -> M s (Maybe a) forkCmd concurrency corrId entId cmdAction = do - bracket_ wait signal . forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " cmd") $ - -- commands MUST be processed under a reasonable timeout or the client would halt - cmdAction >>= \t -> atomically $ writeTBQueue sndQ ([(corrId, entId, t)], []) + -- the forked thread releases the slot when the command completes, the caller only if the fork failed + mask $ \restore -> do + wait + forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " cmd") (restore cmd `finally` signal) + `onException` signal pure Nothing where + -- commands MUST be processed under a reasonable timeout or the client would halt + cmd = cmdAction >>= \t -> atomically $ writeTBQueue sndQ ([(corrId, entId, t)], []) wait = do limit <- asks (concurrency . config) atomically $ do diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index 9263b6953..96bd0639f 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -11,11 +11,14 @@ module RSLVTests (rslvTests) where +import Control.Concurrent (threadDelay) +import Control.Monad (forM_) import Control.Monad.Trans.Except (ExceptT, runExceptT) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB import Data.IORef (IORef, readIORef) import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as L import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (getCurrentTime) @@ -47,6 +50,7 @@ import Simplex.Messaging.Protocol tPut, ) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.SimplexName (SimplexDomain) import Simplex.Messaging.Transport import Simplex.Messaging.Version (mkVersionRange) @@ -102,6 +106,8 @@ rslvTests = do it "RSLV sends the 2LD as its hash" testRslvSendsTheHash it "a name with subnames is sent as text" testSubnameKeepsItsLabels it "a record naming a different name is rejected" testRslvWrongName + describe "RSLV resource use" $ + it "one connection has at most resolver_concurrency lookups in flight" testRslvConnectionCap -- | /v2/resolve answers 200, 400 or 502, so a 404 is a resolver that predates -- the route, not a name that does not exist. @@ -275,5 +281,34 @@ testRslvWrongName = Left (PCEUnexpectedResponse _) -> pure () _ -> expectationFailure $ "expected Left (PCEUnexpectedResponse ..), got: " <> show r +-- The resolver answers after 3s and the server gives up after 1s, so a request +-- that reached the resolver stays in flight for the whole check. +testRslvConnectionCap :: IO () +testRslvConnectionCap = + NRS.withResolverServerDelayed 3000 (NRS.resolveResp status200 "{}") $ \port reqs -> + withSmpServerConfigOn (transport @TLS) (updateCfg (withNames port memCfg) $ \c -> c {serverResolverConcurrency = connCap}) testPort $ const $ + testSMPClient @TLS $ \h -> do + sendRslvs h "cap" 16 + threadDelay 800000 + length <$> resolvePaths reqs `shouldReturn` connCap + recvResponses h 16 `shouldReturn` replicate 16 (Right (ERR (NAME (RESOLVER "timeout")))) + where + connCap = 4 + +-- | One RSLV per block, so no batch limit applies. +sendRslvs :: THandleSMP TLS 'TClient -> String -> Int -> IO () +sendRslvs h@THandle {params} prefix n = + forM_ [1 .. n] $ \i -> do + let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId (B.pack $ prefix <> show i), NoEntity, Cmd SResolver (RSLV (NQDomain (domain "alice.simplex")))) + [Right ()] <- tPut h (Right (Nothing, tToSend) :| []) + pure () + +recvResponses :: THandleSMP TLS 'TClient -> Int -> IO [Either ErrorType BrokerMsg] +recvResponses h n + | n <= 0 = pure [] + | otherwise = do + rs <- map (\(_, _, r) -> r) . L.toList <$> tGetClient h + (rs <>) <$> recvResponses h (n - length rs) + runExceptT' :: Show e => ExceptT e IO a -> IO a runExceptT' a = runExceptT a >>= either (fail . show) pure From e32892b0b38f011739d49da226e8c4556d245205 Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 14 Oct 2026 15:29:01 +0000 Subject: [PATCH 02/10] smp-server: command slot wait timeout and stats --- src/Simplex/Messaging/Server.hs | 72 +++++++++++++++------- src/Simplex/Messaging/Server/Prometheus.hs | 17 +++++ src/Simplex/Messaging/Server/Stats.hs | 33 ++++++++++ tests/RSLVTests.hs | 59 +++++++++++++++++- 4 files changed, 159 insertions(+), 22 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index dbe67b8ae..c2da6f586 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -11,6 +11,7 @@ {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -53,7 +54,7 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Control.Monad.Trans.Except -import Control.Monad.STM (retry) +import Control.Monad.STM (check) import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first, second) import qualified Data.Aeson as J @@ -98,8 +99,8 @@ import Network.Socket (ServiceName, Socket, socketToHandle) import qualified Network.TLS as TLS import Numeric.Natural (Natural) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, clientHandlers, forwardSMPTransmission, smpProxyError, temporaryClientError) -import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient, getConnectedSMPServerClient) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), ProtocolClient (thParams), ProtocolClientConfig (..), ProtocolClientError (..), SMPClient, SMPClientError, clientHandlers, forwardSMPTransmission, netTimeoutInt, smpProxyError, temporaryClientError, pattern NRMInteractive) +import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentConfig (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient, getConnectedSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -1212,7 +1213,17 @@ sendAfterDelay c@Client {sndQ, clientTHParams = THandleParams {sessionId}} stats | otherwise = write where write = atomically $ writeTBQueue sndQ rs - microseconds (MkSystemTime s ns) = s * 1000000 + fromIntegral (ns `div` 1000) + +updateSlotWaitStats :: ServerStats -> SystemTime -> Bool -> IO () +updateSlotWaitStats ServerStats {cmdSlotWaits, cmdSlotWaitMs, cmdSlotWaitMaxMs, cmdSlotWaitTimeouts} since taken = do + waitMs <- fromIntegral . (`div` 1000) . subtract (microseconds since) . microseconds <$> getSystemTime + incStat cmdSlotWaits + atomicModifyIORef'_ cmdSlotWaitMs (+ waitMs) + atomicModifyIORef'_ cmdSlotWaitMaxMs (max waitMs) + unless taken $ incStat cmdSlotWaitTimeouts + +microseconds :: SystemTime -> Int64 +microseconds (MkSystemTime s ns) = s * 1000000 + fromIntegral (ns `div` 1000) send :: Transport c => MVar (THandleSMP c 'TServer) -> Client s -> IO () send th c@Client {sndQ, msgQ, clientTHParams = THandleParams {sessionId}} = do @@ -1491,25 +1502,42 @@ client Nothing -> inc False pRequests >> inc False pErrorsConnect $> Just (ERR $ PROXY NO_SESSION) where forkProxiedCmd :: M s BrokerMsg -> M s (Maybe BrokerMsg) - forkProxiedCmd = forkCmd serverClientConcurrency corrId (EntityId sessId) - -- Run a slow command on a thread - forkCmd :: (ServerConfig s -> Int) -> CorrId -> EntityId -> M s BrokerMsg -> M s (Maybe a) - forkCmd concurrency corrId entId cmdAction = do + forkProxiedCmd = forkCmd serverClientConcurrency corrId (EntityId sessId) (ERR $ smpProxyError PCEResponseTimeout) + -- Run a slow command on a thread, responding with timeoutErr if it was not started before the client timeout + forkCmd :: (ServerConfig s -> Int) -> CorrId -> EntityId -> BrokerMsg -> M s BrokerMsg -> M s (Maybe a) + forkCmd concurrency corrId entId timeoutErr cmdAction = + -- commands MUST be processed under a reasonable timeout or the client would halt + forkCmd_ concurrency NRMInteractive (sendResp timeoutErr) $ cmdAction >>= sendResp + where + sendResp t = atomically $ writeTBQueue sndQ ([(corrId, entId, t)], []) + -- Run a slow command on a thread that sends its response + forkCmd_ :: (ServerConfig s -> Int) -> NetworkRequestMode -> M s () -> M s () -> M s (Maybe a) + forkCmd_ concurrency nm onTimeout cmd = do -- the forked thread releases the slot when the command completes, the caller only if the fork failed - mask $ \restore -> do - wait - forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " cmd") (restore cmd `finally` signal) - `onException` signal + mask $ \restore -> + wait >>= \case + True -> + forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " cmd") (restore cmd `finally` signal) + `onException` signal + False -> restore onTimeout pure Nothing where - -- commands MUST be processed under a reasonable timeout or the client would halt - cmd = cmdAction >>= \t -> atomically $ writeTBQueue sndQ ([(corrId, entId, t)], []) + -- the wait is limited by the client timeout of the command, after which its response is not needed wait = do limit <- asks (concurrency . config) - atomically $ do - used <- readTVar procThreads - when (used >= limit) retry - writeTVar procThreads $! used + 1 + ifM (atomically $ takeSlot limit) (pure True) $ do + clientTimeout <- asks $ (`netTimeoutInt` nm) . tcpTimeout . networkConfig . smpCfg . smpAgentCfg . config + waitStarted <- liftIO getSystemTime + expired <- liftIO $ registerDelay clientTimeout + taken <- atomically $ (True <$ (takeSlot limit >>= check)) `orElse` (False <$ (readTVar expired >>= check)) + stats <- asks serverStats + liftIO $ updateSlotWaitStats stats waitStarted taken + pure taken + takeSlot limit = do + used <- readTVar procThreads + let free = used < limit + when free $ writeTVar procThreads $! used + 1 + pure free signal = atomically $ modifyTVar' procThreads (\t -> t - 1) rslvNamesEnv :: M s (Maybe NamesEnv) rslvNamesEnv = do @@ -1549,7 +1577,7 @@ client Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER)) - Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolveNameMsg (thVersion thParams') nenv d) + Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (ERR $ NAME $ RESOLVER "timeout") (resolveNameMsg (thVersion thParams') nenv d) Cmd SSenderLink command -> case command of LKEY k -> withQueue $ \q qr -> checkMode QMMessaging qr $ secureQueue_ q k $>> getQueueLink_ q qr LGET -> withQueue $ \q qr -> checkContact qr $ getQueueLink_ q qr @@ -2185,9 +2213,11 @@ client Right t''@(_, (corrId', entId', cmd')) -> case cmd' of Cmd SResolver (RSLV d) -> lift $ rslvNamesEnv >>= \case Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER)) - Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity $ do + -- no response when the slot wait timed out, as the proxy also times out the forward + Just nenv -> forkCmd_ serverResolverConcurrency NRMBackground (pure ()) $ do msg <- resolveNameMsg (thVersion clntTHParams) nenv d - either ERR id <$> runExceptT (encodeResp (corrId', entId', msg)) + rres <- either ERR id <$> runExceptT (encodeResp (corrId', entId', msg)) + atomically $ writeTBQueue sndQ ([(corrId, NoEntity, rres)], []) -- INTERNAL because processCommand never returns Nothing for sender commands; -- `fst` drops the empty message only returned for SUB. _ -> Just . maybe (corrId', entId', ERR INTERNAL) fst <$> lift (processCommand Nothing (Right (M.empty, M.empty, M.empty)) t'') diff --git a/src/Simplex/Messaging/Server/Prometheus.hs b/src/Simplex/Messaging/Server/Prometheus.hs index 864a2014f..940407569 100644 --- a/src/Simplex/Messaging/Server/Prometheus.hs +++ b/src/Simplex/Messaging/Server/Prometheus.hs @@ -127,6 +127,10 @@ prometheusMetrics sm rtm ts = _rcvServicesSubMsg, _rcvServicesSubDuplicate, _authDelayExceeded, + _cmdSlotWaits, + _cmdSlotWaitMs, + _cmdSlotWaitMaxMs, + _cmdSlotWaitTimeouts, _qCount, _msgCount, _ntfCount, @@ -197,6 +201,19 @@ prometheusMetrics sm rtm ts = \# HELP simplex_smp_auth_delay_exceeded Responses with AUTH errors sent without delay, as processing took longer than the delay.\n\ \# TYPE simplex_smp_auth_delay_exceeded counter\n\ \simplex_smp_auth_delay_exceeded " <> mshow _authDelayExceeded <> "\n# authDelayExceeded\n\ + \\n\ + \# HELP simplex_smp_cmd_slot_wait_ms Time commands waited for a free slot of the connection concurrency limit, in ms.\n\ + \# TYPE simplex_smp_cmd_slot_wait_ms summary\n\ + \simplex_smp_cmd_slot_wait_ms_sum " <> mshow _cmdSlotWaitMs <> "\n# cmdSlotWaitMs\n\ + \simplex_smp_cmd_slot_wait_ms_count " <> mshow _cmdSlotWaits <> "\n# cmdSlotWaits\n\ + \\n\ + \# HELP simplex_smp_cmd_slot_wait_ms_max The longest wait for a free command slot, in ms.\n\ + \# TYPE simplex_smp_cmd_slot_wait_ms_max gauge\n\ + \simplex_smp_cmd_slot_wait_ms_max " <> mshow _cmdSlotWaitMaxMs <> "\n# cmdSlotWaitMaxMs\n\ + \\n\ + \# HELP simplex_smp_cmd_slot_wait_timeouts Commands not processed because the client timed out while they waited for a free slot.\n\ + \# TYPE simplex_smp_cmd_slot_wait_timeouts counter\n\ + \simplex_smp_cmd_slot_wait_timeouts " <> mshow _cmdSlotWaitTimeouts <> "\n# cmdSlotWaitTimeouts\n\ \\n" subscriptions = "# Subscriptions\n\ diff --git a/src/Simplex/Messaging/Server/Stats.hs b/src/Simplex/Messaging/Server/Stats.hs index c1c599822..a1c9dfa29 100644 --- a/src/Simplex/Messaging/Server/Stats.hs +++ b/src/Simplex/Messaging/Server/Stats.hs @@ -129,6 +129,11 @@ data ServerStats = ServerStats rcvServicesSubMsg :: IORef Int, rcvServicesSubDuplicate :: IORef Int, authDelayExceeded :: IORef Int, + -- commands that waited for a free slot in forkCmd, total and max wait in ms, and waits that timed out + cmdSlotWaits :: IORef Int, + cmdSlotWaitMs :: IORef Int, + cmdSlotWaitMaxMs :: IORef Int, + cmdSlotWaitTimeouts :: IORef Int, qCount :: IORef Int, msgCount :: IORef Int, ntfCount :: IORef Int, @@ -192,6 +197,10 @@ data ServerStatsData = ServerStatsData _rcvServicesSubMsg :: Int, _rcvServicesSubDuplicate :: Int, _authDelayExceeded :: Int, + _cmdSlotWaits :: Int, + _cmdSlotWaitMs :: Int, + _cmdSlotWaitMaxMs :: Int, + _cmdSlotWaitTimeouts :: Int, _qCount :: Int, _msgCount :: Int, _ntfCount :: Int, @@ -257,6 +266,10 @@ newServerStats ts = do rcvServicesSubMsg <- newIORef 0 rcvServicesSubDuplicate <- newIORef 0 authDelayExceeded <- newIORef 0 + cmdSlotWaits <- newIORef 0 + cmdSlotWaitMs <- newIORef 0 + cmdSlotWaitMaxMs <- newIORef 0 + cmdSlotWaitTimeouts <- newIORef 0 qCount <- newIORef 0 msgCount <- newIORef 0 ntfCount <- newIORef 0 @@ -319,6 +332,10 @@ newServerStats ts = do rcvServicesSubMsg, rcvServicesSubDuplicate, authDelayExceeded, + cmdSlotWaits, + cmdSlotWaitMs, + cmdSlotWaitMaxMs, + cmdSlotWaitTimeouts, qCount, msgCount, ntfCount, @@ -383,6 +400,10 @@ getServerStatsData s = do _rcvServicesSubMsg <- readIORef $ rcvServicesSubMsg s _rcvServicesSubDuplicate <- readIORef $ rcvServicesSubDuplicate s _authDelayExceeded <- readIORef $ authDelayExceeded s + _cmdSlotWaits <- readIORef $ cmdSlotWaits s + _cmdSlotWaitMs <- readIORef $ cmdSlotWaitMs s + _cmdSlotWaitMaxMs <- readIORef $ cmdSlotWaitMaxMs s + _cmdSlotWaitTimeouts <- readIORef $ cmdSlotWaitTimeouts s _qCount <- readIORef $ qCount s _msgCount <- readIORef $ msgCount s _ntfCount <- readIORef $ ntfCount s @@ -445,6 +466,10 @@ getServerStatsData s = do _rcvServicesSubMsg, _rcvServicesSubDuplicate, _authDelayExceeded, + _cmdSlotWaits, + _cmdSlotWaitMs, + _cmdSlotWaitMaxMs, + _cmdSlotWaitTimeouts, _qCount, _msgCount, _ntfCount, @@ -510,6 +535,10 @@ setServerStats s d = do writeIORef (rcvServicesSubMsg s) $! _rcvServicesSubMsg d writeIORef (rcvServicesSubDuplicate s) $! _rcvServicesSubDuplicate d writeIORef (authDelayExceeded s) $! _authDelayExceeded d + writeIORef (cmdSlotWaits s) $! _cmdSlotWaits d + writeIORef (cmdSlotWaitMs s) $! _cmdSlotWaitMs d + writeIORef (cmdSlotWaitMaxMs s) $! _cmdSlotWaitMaxMs d + writeIORef (cmdSlotWaitTimeouts s) $! _cmdSlotWaitTimeouts d writeIORef (qCount s) $! _qCount d writeIORef (msgCount s) $! _msgCount d writeIORef (ntfCount s) $! _ntfCount d @@ -713,6 +742,10 @@ instance StrEncoding ServerStatsData where _rcvServicesSubMsg = 0, _rcvServicesSubDuplicate = 0, _authDelayExceeded = 0, + _cmdSlotWaits = 0, + _cmdSlotWaitMs = 0, + _cmdSlotWaitMaxMs = 0, + _cmdSlotWaitTimeouts = 0, _qCount, _msgCount = 0, _ntfCount = 0, diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index 96bd0639f..87361a91d 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -27,6 +27,7 @@ import NamesResolverServer (memCfg, memCfg2, memProxyCfg, withNames) import qualified NamesResolverServer as NRS import SMPClient import Simplex.Messaging.Client +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (strDecode) import SMPNamesTests (availableBody, registeredBody, reservedBody, resolved, testNameRecord, testPricing) @@ -106,8 +107,10 @@ rslvTests = do it "RSLV sends the 2LD as its hash" testRslvSendsTheHash it "a name with subnames is sent as text" testSubnameKeepsItsLabels it "a record naming a different name is rejected" testRslvWrongName - describe "RSLV resource use" $ + describe "RSLV resource use" $ do it "one connection has at most resolver_concurrency lookups in flight" testRslvConnectionCap + it "a lookup waiting for a slot runs when a slot is released" testRslvSlotWait + it "a lookup waiting for a slot fails when the client times out" testRslvSlotWaitTimeout -- | /v2/resolve answers 200, 400 or 502, so a 404 is a resolver that predates -- the route, not a name that does not exist. @@ -295,6 +298,60 @@ testRslvConnectionCap = where connCap = 4 +-- 4 lookups take the slots for the 1s resolver timeout, the 5th waits for a slot. +testRslvSlotWait :: IO () +testRslvSlotWait = + NRS.withResolverServerDelayed 3000 (NRS.resolveResp status200 "{}") $ \port reqs -> + withSmpServerConfigOn (transport @TLS) (slotWaitCfg port 10000000) testPort $ const $ + testSMPClient @TLS $ \h -> do + sendRslvs h "wait" 5 + recvResponses h 5 `shouldReturn` replicate 5 (Right (ERR (NAME (RESOLVER "timeout")))) + length <$> resolvePaths reqs `shouldReturn` 5 + threadDelay 1500000 + SlotWaitMetrics {waits, waitTimeouts, waitMaxMs, waitSumMs} <- readSlotWaitMetrics + (waits, waitTimeouts) `shouldBe` (1, 0) + waitMaxMs `shouldSatisfy` (\t -> t >= 500 && t < 2000) + waitSumMs `shouldBe` waitMaxMs + +-- The client times out after 0.2s, so the 2 waiting lookups fail before the slots are released. +-- The connection does not process further commands while one waits, so the 6th waits after the 5th times out. +testRslvSlotWaitTimeout :: IO () +testRslvSlotWaitTimeout = + NRS.withResolverServerDelayed 3000 (NRS.resolveResp status200 "{}") $ \port reqs -> + withSmpServerConfigOn (transport @TLS) (slotWaitCfg port 200000) testPort $ const $ + testSMPClient @TLS $ \h -> do + sendRslvs h "wait" 6 + recvResponses h 6 `shouldReturn` replicate 6 (Right (ERR (NAME (RESOLVER "timeout")))) + threadDelay 1500000 + length <$> resolvePaths reqs `shouldReturn` 4 + SlotWaitMetrics {waits, waitTimeouts, waitMaxMs, waitSumMs} <- readSlotWaitMetrics + (waits, waitTimeouts) `shouldBe` (2, 2) + waitMaxMs `shouldSatisfy` (\t -> t >= 200 && t < 1000) + waitSumMs `shouldSatisfy` (>= 400) + +slotWaitCfg :: Int -> Int -> AServerConfig +slotWaitCfg port clientTimeout = + updateCfg (withNames port memCfg) $ \c -> + let aCfg = smpAgentCfg c + cCfg = smpCfg aCfg + nt = NetworkTimeout {backgroundTimeout = clientTimeout, interactiveTimeout = clientTimeout} + in c {serverResolverConcurrency = 4, prometheusInterval = Just 1, smpAgentCfg = aCfg {smpCfg = cCfg {networkConfig = (networkConfig cCfg) {tcpTimeout = nt}}}} + +data SlotWaitMetrics = SlotWaitMetrics {waits :: Int, waitTimeouts :: Int, waitMaxMs :: Int, waitSumMs :: Int} + +readSlotWaitMetrics :: IO SlotWaitMetrics +readSlotWaitMetrics = do + ms <- map words . lines <$> readFile testPrometheusMetricsFile + -- metric lines are "name value timestamp" + let metric name = case [v | n : v : _ <- ms, n == name] of + [v] -> pure $ read v + _ -> fail $ "no metric " <> name + SlotWaitMetrics + <$> metric "simplex_smp_cmd_slot_wait_ms_count" + <*> metric "simplex_smp_cmd_slot_wait_timeouts" + <*> metric "simplex_smp_cmd_slot_wait_ms_max" + <*> metric "simplex_smp_cmd_slot_wait_ms_sum" + -- | One RSLV per block, so no batch limit applies. sendRslvs :: THandleSMP TLS 'TClient -> String -> Int -> IO () sendRslvs h@THandle {params} prefix n = From aefd087390f42f485abb0a067c0f43ceeebebff1 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 06:57:21 +0000 Subject: [PATCH 03/10] smp-server: clarify command slot comments --- src/Simplex/Messaging/Server.hs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index c2da6f586..cda83237d 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1513,7 +1513,8 @@ client -- Run a slow command on a thread that sends its response forkCmd_ :: (ServerConfig s -> Int) -> NetworkRequestMode -> M s () -> M s () -> M s (Maybe a) forkCmd_ concurrency nm onTimeout cmd = do - -- the forked thread releases the slot when the command completes, the caller only if the fork failed + -- Exceptions are masked so the slot is never lost between taking it and starting the thread. + -- The thread frees the slot when the command finishes; the caller frees it only if forking failed. mask $ \restore -> wait >>= \case True -> @@ -1522,7 +1523,9 @@ client False -> restore onTimeout pure Nothing where - -- the wait is limited by the client timeout of the command, after which its response is not needed + -- Takes a free slot at once if there is one. Otherwise waits for a slot until the client + -- timeout, after which the client no longer expects the response. + -- Other commands of this client are not processed while it waits. wait = do limit <- asks (concurrency . config) ifM (atomically $ takeSlot limit) (pure True) $ do @@ -2213,7 +2216,8 @@ client Right t''@(_, (corrId', entId', cmd')) -> case cmd' of Cmd SResolver (RSLV d) -> lift $ rslvNamesEnv >>= \case Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER)) - -- no response when the slot wait timed out, as the proxy also times out the forward + -- The proxy waits for forwarded commands with the background timeout. + -- If no slot is free before it, there is no response, as the proxy has already timed out. Just nenv -> forkCmd_ serverResolverConcurrency NRMBackground (pure ()) $ do msg <- resolveNameMsg (thVersion clntTHParams) nenv d rres <- either ERR id <$> runExceptT (encodeResp (corrId', entId', msg)) From 619a09883b83765f260b53110121cc921f9d5538 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 07:34:26 +0000 Subject: [PATCH 04/10] smp-server: simplify command slot fork --- src/Simplex/Messaging/Server.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index cda83237d..d091445e2 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1513,14 +1513,14 @@ client -- Run a slow command on a thread that sends its response forkCmd_ :: (ServerConfig s -> Int) -> NetworkRequestMode -> M s () -> M s () -> M s (Maybe a) forkCmd_ concurrency nm onTimeout cmd = do + let label = B.unpack $ "client $" <> encode sessionId <> " cmd" -- Exceptions are masked so the slot is never lost between taking it and starting the thread. -- The thread frees the slot when the command finishes; the caller frees it only if forking failed. mask $ \restore -> - wait >>= \case - True -> - forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " cmd") (restore cmd `finally` signal) - `onException` signal - False -> restore onTimeout + ifM + wait + (forkClient clnt label (restore cmd `finally` signal) `onException` signal) + (restore onTimeout) pure Nothing where -- Takes a free slot at once if there is one. Otherwise waits for a slot until the client From 8535a0e2590dfabbd2b7b9e80b8fd34561d4e301 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 07:43:07 +0000 Subject: [PATCH 05/10] smp-server: limit direct RSLV per client --- src/Simplex/Messaging/Server.hs | 4 ++-- src/Simplex/Messaging/Server/Env/STM.hs | 2 +- src/Simplex/Messaging/Server/Main/Init.hs | 4 ++-- tests/RSLVTests.hs | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index d091445e2..c3f4c8e45 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1550,7 +1550,7 @@ client Nothing -> incStat (rslvDisabled st) $> Nothing Just nenv -> pure (Just nenv) -- Runs on a forked thread so RSLV does not block other commands; - -- concurrency is limited by serverResolverConcurrency in forkCmd. + -- concurrency is limited in forkCmd by serverClientConcurrency for direct and serverResolverConcurrency for forwarded RSLV. resolveNameMsg :: VersionSMP -> NamesEnv -> NameQuery -> M s BrokerMsg resolveNameMsg v nenv q = do st <- asks (rslvStats . serverStats) @@ -1580,7 +1580,7 @@ client Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER)) - Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (ERR $ NAME $ RESOLVER "timeout") (resolveNameMsg (thVersion thParams') nenv d) + Just nenv -> forkCmd serverClientConcurrency corrId NoEntity (ERR $ NAME $ RESOLVER "timeout") (resolveNameMsg (thVersion thParams') nenv d) Cmd SSenderLink command -> case command of LKEY k -> withQueue $ \q qr -> checkMode QMMessaging qr $ secureQueue_ q k $>> getQueueLink_ q qr LGET -> withQueue $ \q qr -> checkContact qr $ getQueueLink_ q qr diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 2c1843e51..d06c56e4e 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -201,7 +201,7 @@ data ServerConfig s = ServerConfig smpAgentCfg :: SMPClientAgentConfig, allowSMPProxy :: Bool, -- auth is the same with `newQueueBasicAuth` serverClientConcurrency :: Int, - -- | max concurrent name resolutions per connection, enforced in forkCmd. + -- | max concurrent forwarded name resolutions per proxy connection, enforced in forkCmd. -- Much higher than serverClientConcurrency: forwarded RSLVs from many clients -- aggregate over a single proxy->relay connection (only servers send proxied -- requests), so bounding them by the per-client limit would throttle unduly. diff --git a/src/Simplex/Messaging/Server/Main/Init.hs b/src/Simplex/Messaging/Server/Main/Init.hs index b82061ca3..d2938821c 100644 --- a/src/Simplex/Messaging/Server/Main/Init.hs +++ b/src/Simplex/Messaging/Server/Main/Init.hs @@ -154,7 +154,7 @@ iniFileContent cfgPath logPath opts host basicAuth controlPortPwds = \# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\ \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ \# socks_mode = onion\n\n\ - \# Limit number of threads a client can spawn to process proxy commands in parrallel.\n" + \# Limit number of threads a client can spawn to process proxy commands and name resolutions in parallel.\n" <> ("# client_concurrency = " <> tshow defaultProxyClientConcurrency) <> "\n\n\ \[NAMES]\n\ @@ -169,7 +169,7 @@ iniFileContent cfgPath logPath opts host basicAuth controlPortPwds = \# resolver_auth: basic :\n\ \# resolver_timeout_ms: 3000\n\ \# resolver_max_response_bytes: 16000\n\ - \# Max concurrent name resolutions per connection (forwarded RSLVs from many\n\ + \# Max concurrent forwarded name resolutions per proxy connection (forwarded RSLVs from many\n\ \# clients share one proxy connection, so this is much higher than PROXY client_concurrency).\n" <> ("# resolver_concurrency = " <> tshow defaultNameResolverConcurrency) <> "\n\n\ diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index 87361a91d..a3bbd6d07 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -108,7 +108,7 @@ rslvTests = do it "a name with subnames is sent as text" testSubnameKeepsItsLabels it "a record naming a different name is rejected" testRslvWrongName describe "RSLV resource use" $ do - it "one connection has at most resolver_concurrency lookups in flight" testRslvConnectionCap + it "one connection has at most client_concurrency lookups in flight" testRslvConnectionCap it "a lookup waiting for a slot runs when a slot is released" testRslvSlotWait it "a lookup waiting for a slot fails when the client times out" testRslvSlotWaitTimeout @@ -289,7 +289,7 @@ testRslvWrongName = testRslvConnectionCap :: IO () testRslvConnectionCap = NRS.withResolverServerDelayed 3000 (NRS.resolveResp status200 "{}") $ \port reqs -> - withSmpServerConfigOn (transport @TLS) (updateCfg (withNames port memCfg) $ \c -> c {serverResolverConcurrency = connCap}) testPort $ const $ + withSmpServerConfigOn (transport @TLS) (updateCfg (withNames port memCfg) $ \c -> c {serverClientConcurrency = connCap}) testPort $ const $ testSMPClient @TLS $ \h -> do sendRslvs h "cap" 16 threadDelay 800000 @@ -335,7 +335,7 @@ slotWaitCfg port clientTimeout = let aCfg = smpAgentCfg c cCfg = smpCfg aCfg nt = NetworkTimeout {backgroundTimeout = clientTimeout, interactiveTimeout = clientTimeout} - in c {serverResolverConcurrency = 4, prometheusInterval = Just 1, smpAgentCfg = aCfg {smpCfg = cCfg {networkConfig = (networkConfig cCfg) {tcpTimeout = nt}}}} + in c {serverClientConcurrency = 4, prometheusInterval = Just 1, smpAgentCfg = aCfg {smpCfg = cCfg {networkConfig = (networkConfig cCfg) {tcpTimeout = nt}}}} data SlotWaitMetrics = SlotWaitMetrics {waits :: Int, waitTimeouts :: Int, waitMaxMs :: Int, waitSumMs :: Int} From e74398c5c835878fcf72f6967f98360ec2970813 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 07:43:07 +0000 Subject: [PATCH 06/10] smp-server: distinct error for RSLV slot timeout --- src/Simplex/Messaging/Server.hs | 2 +- tests/RSLVTests.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index c3f4c8e45..79404f1f9 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1580,7 +1580,7 @@ client Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER)) - Just nenv -> forkCmd serverClientConcurrency corrId NoEntity (ERR $ NAME $ RESOLVER "timeout") (resolveNameMsg (thVersion thParams') nenv d) + Just nenv -> forkCmd serverClientConcurrency corrId NoEntity (ERR $ NAME $ RESOLVER "too many requests") (resolveNameMsg (thVersion thParams') nenv d) Cmd SSenderLink command -> case command of LKEY k -> withQueue $ \q qr -> checkMode QMMessaging qr $ secureQueue_ q k $>> getQueueLink_ q qr LGET -> withQueue $ \q qr -> checkContact qr $ getQueueLink_ q qr diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index a3bbd6d07..1731f8886 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -321,7 +321,7 @@ testRslvSlotWaitTimeout = withSmpServerConfigOn (transport @TLS) (slotWaitCfg port 200000) testPort $ const $ testSMPClient @TLS $ \h -> do sendRslvs h "wait" 6 - recvResponses h 6 `shouldReturn` replicate 6 (Right (ERR (NAME (RESOLVER "timeout")))) + recvResponses h 6 `shouldReturn` (replicate 2 (Right (ERR (NAME (RESOLVER "too many requests")))) <> replicate 4 (Right (ERR (NAME (RESOLVER "timeout"))))) threadDelay 1500000 length <$> resolvePaths reqs `shouldReturn` 4 SlotWaitMetrics {waits, waitTimeouts, waitMaxMs, waitSumMs} <- readSlotWaitMetrics From 5d33bd98d29f97522a680be57845e855f84990b9 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 08:10:26 +0000 Subject: [PATCH 07/10] smp-server: client command slot wait timeout --- src/Simplex/Messaging/Server.hs | 16 +++++++++------- src/Simplex/Messaging/Server/Env/STM.hs | 5 +++++ src/Simplex/Messaging/Server/Main.hs | 1 + tests/RSLVTests.hs | 7 +------ tests/SMPClient.hs | 1 + 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 79404f1f9..122b2a28c 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -11,7 +11,6 @@ {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -99,7 +98,7 @@ import Network.Socket (ServiceName, Socket, socketToHandle) import qualified Network.TLS as TLS import Numeric.Natural (Natural) import Simplex.Messaging.Agent.Lock -import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), ProtocolClient (thParams), ProtocolClientConfig (..), ProtocolClientError (..), SMPClient, SMPClientError, clientHandlers, forwardSMPTransmission, netTimeoutInt, smpProxyError, temporaryClientError, pattern NRMInteractive) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), ProtocolClient (thParams), ProtocolClientConfig (..), ProtocolClientError (..), SMPClient, SMPClientError, clientHandlers, forwardSMPTransmission, netTimeoutInt, smpProxyError, temporaryClientError) import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentConfig (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient, getConnectedSMPServerClient) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -1222,6 +1221,9 @@ updateSlotWaitStats ServerStats {cmdSlotWaits, cmdSlotWaitMs, cmdSlotWaitMaxMs, atomicModifyIORef'_ cmdSlotWaitMaxMs (max waitMs) unless taken $ incStat cmdSlotWaitTimeouts +forwardedCmdWait :: ServerConfig s -> Int +forwardedCmdWait = (`netTimeoutInt` NRMBackground) . tcpTimeout . networkConfig . smpCfg . smpAgentCfg + microseconds :: SystemTime -> Int64 microseconds (MkSystemTime s ns) = s * 1000000 + fromIntegral (ns `div` 1000) @@ -1507,12 +1509,12 @@ client forkCmd :: (ServerConfig s -> Int) -> CorrId -> EntityId -> BrokerMsg -> M s BrokerMsg -> M s (Maybe a) forkCmd concurrency corrId entId timeoutErr cmdAction = -- commands MUST be processed under a reasonable timeout or the client would halt - forkCmd_ concurrency NRMInteractive (sendResp timeoutErr) $ cmdAction >>= sendResp + forkCmd_ concurrency maxClientCmdWait (sendResp timeoutErr) $ cmdAction >>= sendResp where sendResp t = atomically $ writeTBQueue sndQ ([(corrId, entId, t)], []) -- Run a slow command on a thread that sends its response - forkCmd_ :: (ServerConfig s -> Int) -> NetworkRequestMode -> M s () -> M s () -> M s (Maybe a) - forkCmd_ concurrency nm onTimeout cmd = do + forkCmd_ :: (ServerConfig s -> Int) -> (ServerConfig s -> Int) -> M s () -> M s () -> M s (Maybe a) + forkCmd_ concurrency waitTimeout onTimeout cmd = do let label = B.unpack $ "client $" <> encode sessionId <> " cmd" -- Exceptions are masked so the slot is never lost between taking it and starting the thread. -- The thread frees the slot when the command finishes; the caller frees it only if forking failed. @@ -1529,7 +1531,7 @@ client wait = do limit <- asks (concurrency . config) ifM (atomically $ takeSlot limit) (pure True) $ do - clientTimeout <- asks $ (`netTimeoutInt` nm) . tcpTimeout . networkConfig . smpCfg . smpAgentCfg . config + clientTimeout <- asks $ waitTimeout . config waitStarted <- liftIO getSystemTime expired <- liftIO $ registerDelay clientTimeout taken <- atomically $ (True <$ (takeSlot limit >>= check)) `orElse` (False <$ (readTVar expired >>= check)) @@ -2218,7 +2220,7 @@ client Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER)) -- The proxy waits for forwarded commands with the background timeout. -- If no slot is free before it, there is no response, as the proxy has already timed out. - Just nenv -> forkCmd_ serverResolverConcurrency NRMBackground (pure ()) $ do + Just nenv -> forkCmd_ serverResolverConcurrency forwardedCmdWait (pure ()) $ do msg <- resolveNameMsg (thVersion clntTHParams) nenv d rres <- either ERR id <$> runExceptT (encodeResp (corrId', entId', msg)) atomically $ writeTBQueue sndQ ([(corrId, NoEntity, rres)], []) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index d06c56e4e..7f6e71749 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -68,6 +68,7 @@ module Simplex.Messaging.Server.Env.STM defaultInactiveClientExpiration, defaultProxyClientConcurrency, defaultNameResolverConcurrency, + defaultMaxClientCmdWait, defaultAuthErrorDelayMs, defaultMaxJournalMsgCount, defaultMaxJournalStateLines, @@ -206,6 +207,7 @@ data ServerConfig s = ServerConfig -- aggregate over a single proxy->relay connection (only servers send proxied -- requests), so bounding them by the per-client limit would throttle unduly. serverResolverConcurrency :: Int, + maxClientCmdWait :: Int, -- | public-namespace resolver config; Nothing disables the names role namesConfig :: Maybe NamesConfig, -- | server public information @@ -257,6 +259,9 @@ defaultProxyClientConcurrency = 32 defaultNameResolverConcurrency :: Int defaultNameResolverConcurrency = 1000 +defaultMaxClientCmdWait :: Int +defaultMaxClientCmdWait = 60000000 -- microseconds + defaultAuthErrorDelayMs :: Int64 defaultAuthErrorDelayMs = 50 diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 4a3c909ed..64b4af287 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -611,6 +611,7 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath = allowSMPProxy = True, serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini, serverResolverConcurrency = readIniDefault defaultNameResolverConcurrency "NAMES" "resolver_concurrency" ini, + maxClientCmdWait = defaultMaxClientCmdWait, namesConfig = readNamesConfig ini, information = serverPublicInfo ini, startOptions diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index 1731f8886..a52aa2a86 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -27,7 +27,6 @@ import NamesResolverServer (memCfg, memCfg2, memProxyCfg, withNames) import qualified NamesResolverServer as NRS import SMPClient import Simplex.Messaging.Client -import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (strDecode) import SMPNamesTests (availableBody, registeredBody, reservedBody, resolved, testNameRecord, testPricing) @@ -331,11 +330,7 @@ testRslvSlotWaitTimeout = slotWaitCfg :: Int -> Int -> AServerConfig slotWaitCfg port clientTimeout = - updateCfg (withNames port memCfg) $ \c -> - let aCfg = smpAgentCfg c - cCfg = smpCfg aCfg - nt = NetworkTimeout {backgroundTimeout = clientTimeout, interactiveTimeout = clientTimeout} - in c {serverClientConcurrency = 4, prometheusInterval = Just 1, smpAgentCfg = aCfg {smpCfg = cCfg {networkConfig = (networkConfig cCfg) {tcpTimeout = nt}}}} + updateCfg (withNames port memCfg) $ \c -> c {serverClientConcurrency = 4, prometheusInterval = Just 1, maxClientCmdWait = clientTimeout} data SlotWaitMetrics = SlotWaitMetrics {waits :: Int, waitTimeouts :: Int, waitMaxMs :: Int, waitSumMs :: Int} diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 2db4a9819..dd84bd6e9 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -278,6 +278,7 @@ cfgMS msType = withStoreCfg (testServerStoreConfig msType) $ \serverStoreCfg -> allowSMPProxy = False, serverClientConcurrency = 2, serverResolverConcurrency = defaultNameResolverConcurrency, + maxClientCmdWait = defaultMaxClientCmdWait, namesConfig = Nothing, information = Nothing, startOptions = defaultStartOptions From 8742475182c37c1cbfc95a2834af6dcb73dcd91b Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 08:15:56 +0000 Subject: [PATCH 08/10] smp-server: reply to forwarded RSLV slot timeout --- src/Simplex/Messaging/Server.hs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 122b2a28c..ffdfbb039 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1221,6 +1221,9 @@ updateSlotWaitStats ServerStats {cmdSlotWaits, cmdSlotWaitMs, cmdSlotWaitMaxMs, atomicModifyIORef'_ cmdSlotWaitMaxMs (max waitMs) unless taken $ incStat cmdSlotWaitTimeouts +rslvTooManyRequests :: BrokerMsg +rslvTooManyRequests = ERR $ NAME $ RESOLVER "too many requests" + forwardedCmdWait :: ServerConfig s -> Int forwardedCmdWait = (`netTimeoutInt` NRMBackground) . tcpTimeout . networkConfig . smpCfg . smpAgentCfg @@ -1582,7 +1585,7 @@ client Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER)) - Just nenv -> forkCmd serverClientConcurrency corrId NoEntity (ERR $ NAME $ RESOLVER "too many requests") (resolveNameMsg (thVersion thParams') nenv d) + Just nenv -> forkCmd serverClientConcurrency corrId NoEntity rslvTooManyRequests (resolveNameMsg (thVersion thParams') nenv d) Cmd SSenderLink command -> case command of LKEY k -> withQueue $ \q qr -> checkMode QMMessaging qr $ secureQueue_ q k $>> getQueueLink_ q qr LGET -> withQueue $ \q qr -> checkContact qr $ getQueueLink_ q qr @@ -2218,12 +2221,12 @@ client Right t''@(_, (corrId', entId', cmd')) -> case cmd' of Cmd SResolver (RSLV d) -> lift $ rslvNamesEnv >>= \case Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER)) - -- The proxy waits for forwarded commands with the background timeout. - -- If no slot is free before it, there is no response, as the proxy has already timed out. - Just nenv -> forkCmd_ serverResolverConcurrency forwardedCmdWait (pure ()) $ do - msg <- resolveNameMsg (thVersion clntTHParams) nenv d - rres <- either ERR id <$> runExceptT (encodeResp (corrId', entId', msg)) - atomically $ writeTBQueue sndQ ([(corrId, NoEntity, rres)], []) + Just nenv -> do + let sendResolved msg = do + rres <- either ERR id <$> runExceptT (encodeResp (corrId', entId', msg)) + atomically $ writeTBQueue sndQ ([(corrId, NoEntity, rres)], []) + forkCmd_ serverResolverConcurrency forwardedCmdWait (sendResolved rslvTooManyRequests) $ + resolveNameMsg (thVersion clntTHParams) nenv d >>= sendResolved -- INTERNAL because processCommand never returns Nothing for sender commands; -- `fst` drops the empty message only returned for SUB. _ -> Just . maybe (corrId', entId', ERR INTERNAL) fst <$> lift (processCommand Nothing (Right (M.empty, M.empty, M.empty)) t'') From ac02b7bc81a4760a7dd45ec39f1cb808088eed3f Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 08:15:56 +0000 Subject: [PATCH 09/10] smp-server: raise default concurrency limits --- src/Simplex/Messaging/Server/Env/STM.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 7f6e71749..79bfe21f4 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -254,10 +254,10 @@ defaultInactiveClientExpiration = } defaultProxyClientConcurrency :: Int -defaultProxyClientConcurrency = 32 +defaultProxyClientConcurrency = 500 defaultNameResolverConcurrency :: Int -defaultNameResolverConcurrency = 1000 +defaultNameResolverConcurrency = 5000 defaultMaxClientCmdWait :: Int defaultMaxClientCmdWait = 60000000 -- microseconds From 33883f2aa5aa173002fcac3fe7ee0f64a04eeb57 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 15 Oct 2026 08:21:54 +0000 Subject: [PATCH 10/10] smp-server: simplify command slot wait --- src/Simplex/Messaging/Server.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index ffdfbb039..c26a8b0a6 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -53,7 +53,7 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Control.Monad.Trans.Except -import Control.Monad.STM (check) +import Control.Monad.STM (retry) import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first, second) import qualified Data.Aeson as J @@ -1537,7 +1537,7 @@ client clientTimeout <- asks $ waitTimeout . config waitStarted <- liftIO getSystemTime expired <- liftIO $ registerDelay clientTimeout - taken <- atomically $ (True <$ (takeSlot limit >>= check)) `orElse` (False <$ (readTVar expired >>= check)) + taken <- atomically $ ifM (takeSlot limit) (pure True) (ifM (readTVar expired) (pure False) retry) stats <- asks serverStats liftIO $ updateSlotWaitStats stats waitStarted taken pure taken