mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-12 11:39:56 +00:00
Compare commits
9
Commits
v5.5.1-beta.0
...
v5.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64b6cba4b | ||
|
|
189885c50d | ||
|
|
a516c2f72c | ||
|
|
2ae1c9f79d | ||
|
|
24b84106a6 | ||
|
|
15bc027f23 | ||
|
|
cb64dabf75 | ||
|
|
7a0cd8041b | ||
|
|
fd4eeb36db |
@@ -1,3 +1,20 @@
|
||||
# 5.5.2
|
||||
|
||||
Extensible handshake for clients and SMP/NTF servers (ignore extra data).
|
||||
|
||||
# 5.5.1
|
||||
|
||||
SMP servers:
|
||||
- do not keep stats file open
|
||||
- additional stats about currently stored messages
|
||||
|
||||
Agent:
|
||||
- support multiple notification servers (only one can be used at a time).
|
||||
- expire messages after "quota exceeded" error after 7 days (instead of 21 days previously).
|
||||
- stabilize message delivery, remove unnecessary subscription retries and traffic.
|
||||
- improve database performance for message delivery.
|
||||
- fix sockets/memory leak - a very old bug "activated" by improvements in v5.5.0.
|
||||
|
||||
# 5.5.0
|
||||
|
||||
Code:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 5.5.1.0
|
||||
version: 5.5.2.0
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 5.5.1.0
|
||||
version: 5.5.2.0
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -101,7 +101,7 @@ library
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231222_command_created_at
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231225_failed_work_items
|
||||
Simplex.Messaging.Agent.TAsyncs
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
|
||||
@@ -33,7 +33,7 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
xftpServerVersion :: String
|
||||
xftpServerVersion = "1.2.1.0"
|
||||
xftpServerVersion = "1.2.2.0"
|
||||
|
||||
xftpServerCLI :: FilePath -> FilePath -> IO ()
|
||||
xftpServerCLI cfgPath logPath = do
|
||||
|
||||
@@ -155,6 +155,7 @@ import Data.Text.Encoding
|
||||
import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime)
|
||||
import Data.Time.Clock.System (getSystemTime)
|
||||
import Data.Word (Word16)
|
||||
|
||||
-- import GHC.Conc (unsafeIOToSTM)
|
||||
import Network.Socket (HostName)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..), XFTPClient, XFTPClientConfig (..), XFTPClientError)
|
||||
@@ -171,7 +172,6 @@ import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), withTransaction)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Agent.TAsyncs
|
||||
import Simplex.Messaging.Agent.TRcvQueues (TRcvQueues (getRcvQueues))
|
||||
import qualified Simplex.Messaging.Agent.TRcvQueues as RQ
|
||||
import Simplex.Messaging.Client
|
||||
@@ -228,7 +228,7 @@ data SessionVar a = SessionVar
|
||||
sessionVarId :: Int
|
||||
}
|
||||
|
||||
type ClientVar msg = SessionVar (Either AgentErrorType (Client msg))
|
||||
type ClientVar msg = SessionVar (Either AgentErrorType (Client msg))
|
||||
|
||||
type SMPClientVar = ClientVar SMP.BrokerMsg
|
||||
|
||||
@@ -277,7 +277,6 @@ data AgentClient = AgentClient
|
||||
deleteLock :: Lock,
|
||||
-- smpSubWorkers for SMP servers sessions
|
||||
smpSubWorkers :: TMap SMPTransportSession (SessionVar (Async ())),
|
||||
asyncClients :: TAsyncs,
|
||||
agentStats :: TMap AgentStatsKey (TVar Int),
|
||||
clientId :: Int,
|
||||
agentEnv :: Env
|
||||
@@ -417,7 +416,6 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv =
|
||||
invLocks <- TM.empty
|
||||
deleteLock <- createLock
|
||||
smpSubWorkers <- TM.empty
|
||||
asyncClients <- newTAsyncs
|
||||
agentStats <- TM.empty
|
||||
return
|
||||
AgentClient
|
||||
@@ -451,7 +449,6 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv =
|
||||
invLocks,
|
||||
deleteLock,
|
||||
smpSubWorkers,
|
||||
asyncClients,
|
||||
agentStats,
|
||||
clientId,
|
||||
agentEnv
|
||||
@@ -502,10 +499,11 @@ instance ProtocolServerClient XFTPErrorType FileResponse where
|
||||
getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPTransportSession -> m SMPClient
|
||||
getSMPServerClient c@AgentClient {active, smpClients, msgQ} tSess@(userId, srv, _) = do
|
||||
unlessM (readTVarIO active) . throwError $ INACTIVE
|
||||
atomically (getTSessVar c tSess smpClients)
|
||||
>>= either newClient (waitForProtocolClient c tSess)
|
||||
v <- atomically (getTSessVar c tSess smpClients)
|
||||
either newClient (waitForProtocolClient c tSess) v
|
||||
`catchAgentError` \e -> resubscribeSMPSession c tSess >> throwError e
|
||||
where
|
||||
newClient = newProtocolClient c tSess smpClients connectClient resubscribeSMPSession
|
||||
newClient = newProtocolClient c tSess smpClients connectClient
|
||||
connectClient :: SMPClientVar -> m SMPClient
|
||||
connectClient v = do
|
||||
cfg <- getClientConfig c smpCfg
|
||||
@@ -540,8 +538,13 @@ getSMPServerClient c@AgentClient {active, smpClients, msgQ} tSess@(userId, srv,
|
||||
|
||||
resubscribeSMPSession :: AgentMonad' m => AgentClient -> SMPTransportSession -> m ()
|
||||
resubscribeSMPSession c@AgentClient {smpSubWorkers} tSess =
|
||||
atomically (getTSessVar c tSess smpSubWorkers) >>= either newSubWorker (\_ -> pure ())
|
||||
atomically getWorkerVar >>= mapM_ (either newSubWorker (\_ -> pure ()))
|
||||
where
|
||||
getWorkerVar =
|
||||
ifM
|
||||
(null <$> getPending)
|
||||
(pure Nothing) -- prevent race with cleanup and adding pending queues in another call
|
||||
(Just <$> getTSessVar c tSess smpSubWorkers)
|
||||
newSubWorker v = do
|
||||
a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v)
|
||||
atomically $ putTMVar (sessionVar v) a
|
||||
@@ -549,10 +552,11 @@ resubscribeSMPSession c@AgentClient {smpSubWorkers} tSess =
|
||||
ri <- asks $ reconnectInterval . config
|
||||
timeoutCounts <- newTVarIO 0
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
pending <- atomically . RQ.getSessQueues tSess $ pendingSubs c
|
||||
pending <- atomically getPending
|
||||
forM_ (L.nonEmpty pending) $ \qs -> do
|
||||
void . tryAgentError' $ reconnectSMPClient timeoutCounts c tSess qs
|
||||
loop
|
||||
getPending = RQ.getSessQueues tSess $ pendingSubs c
|
||||
cleanup :: SessionVar (Async ()) -> STM ()
|
||||
cleanup v = do
|
||||
-- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar.
|
||||
@@ -596,7 +600,7 @@ getNtfServerClient c@AgentClient {active, ntfClients} tSess@(userId, srv, _) = d
|
||||
unlessM (readTVarIO active) . throwError $ INACTIVE
|
||||
atomically (getTSessVar c tSess ntfClients)
|
||||
>>= either
|
||||
(newProtocolClient c tSess ntfClients connectClient $ \_ _ -> pure ())
|
||||
(newProtocolClient c tSess ntfClients connectClient)
|
||||
(waitForProtocolClient c tSess)
|
||||
where
|
||||
connectClient :: NtfClientVar -> m NtfClient
|
||||
@@ -616,7 +620,7 @@ getXFTPServerClient c@AgentClient {active, xftpClients, useNetworkConfig} tSess@
|
||||
unlessM (readTVarIO active) . throwError $ INACTIVE
|
||||
atomically (getTSessVar c tSess xftpClients)
|
||||
>>= either
|
||||
(newProtocolClient c tSess xftpClients connectClient $ \_ _ -> pure ())
|
||||
(newProtocolClient c tSess xftpClients connectClient)
|
||||
(waitForProtocolClient c tSess)
|
||||
where
|
||||
connectClient :: XFTPClientVar -> m XFTPClient
|
||||
@@ -665,35 +669,22 @@ newProtocolClient ::
|
||||
TransportSession msg ->
|
||||
TMap (TransportSession msg) (ClientVar msg) ->
|
||||
(ClientVar msg -> m (Client msg)) ->
|
||||
(AgentClient -> TransportSession msg -> m ()) ->
|
||||
ClientVar msg ->
|
||||
m (Client msg)
|
||||
newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient clientConnected v = tryConnectClient pure tryConnectAsync
|
||||
where
|
||||
tryConnectClient :: (Client msg -> m a) -> m () -> m a
|
||||
tryConnectClient successAction retryAction =
|
||||
tryAgentError (connectClient v) >>= \case
|
||||
Right client -> do
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv <> " (user " <> bshow userId <> maybe "" (" for entity " <>) entityId_ <> ")"
|
||||
atomically $ putTMVar (sessionVar v) (Right client)
|
||||
liftIO $ incClientStat c userId client "CLIENT" "OK"
|
||||
atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent CONNECT client)
|
||||
successAction client
|
||||
Left e -> do
|
||||
liftIO $ incServerStat c userId srv "CLIENT" $ strEncode e
|
||||
if temporaryAgentError e
|
||||
then retryAction
|
||||
else atomically $ do
|
||||
putTMVar (sessionVar v) (Left e)
|
||||
removeTSessVar v tSess clients
|
||||
throwError e
|
||||
tryConnectAsync :: m ()
|
||||
tryConnectAsync = newAsyncAction connectAsync $ asyncClients c
|
||||
connectAsync :: Int -> m ()
|
||||
connectAsync aId = do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> void $ tryConnectClient (const $ clientConnected c tSess) loop
|
||||
atomically . removeAsyncAction aId $ asyncClients c
|
||||
newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v =
|
||||
tryAgentError (connectClient v) >>= \case
|
||||
Right client -> do
|
||||
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv <> " (user " <> bshow userId <> maybe "" (" for entity " <>) entityId_ <> ")"
|
||||
atomically $ putTMVar (sessionVar v) (Right client)
|
||||
liftIO $ incClientStat c userId client "CLIENT" "OK"
|
||||
atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent CONNECT client)
|
||||
pure client
|
||||
Left e -> do
|
||||
liftIO $ incServerStat c userId srv "CLIENT" $ strEncode e
|
||||
atomically $ do
|
||||
removeTSessVar v tSess clients
|
||||
putTMVar (sessionVar v) (Left e)
|
||||
throwError e -- signal error to caller
|
||||
|
||||
hostEvent :: forall err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> Client msg -> ACommand 'Agent 'AENone
|
||||
hostEvent event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost
|
||||
@@ -711,7 +702,6 @@ closeAgentClient c = liftIO $ do
|
||||
closeProtocolServerClients c ntfClients
|
||||
closeProtocolServerClients c xftpClients
|
||||
atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect
|
||||
cancelActions . actions $ asyncClients c
|
||||
clearWorkers smpDeliveryWorkers >>= mapM_ (cancelWorker . fst)
|
||||
clearWorkers asyncCmdWorkers >>= mapM_ cancelWorker
|
||||
clear connCmdsQueued
|
||||
@@ -724,7 +714,7 @@ closeAgentClient c = liftIO $ do
|
||||
clearWorkers workers = atomically $ swapTVar (workers c) mempty
|
||||
clear :: Monoid m => (AgentClient -> TVar m) -> IO ()
|
||||
clear sel = atomically $ writeTVar (sel c) mempty
|
||||
cancelReconnect :: SessionVar (Async ()) -> IO ()
|
||||
cancelReconnect :: SessionVar (Async ()) -> IO ()
|
||||
cancelReconnect v = void . forkIO $ atomically (readTMVar $ sessionVar v) >>= uninterruptibleCancel
|
||||
|
||||
cancelWorker :: Worker -> IO ()
|
||||
@@ -763,9 +753,6 @@ closeXFTPServerClient :: AgentMonad' m => AgentClient -> UserId -> XFTPServer ->
|
||||
closeXFTPServerClient c userId server (FileDigest chunkDigest) =
|
||||
mkTransportSession c userId server chunkDigest >>= liftIO . closeClient c xftpClients
|
||||
|
||||
cancelActions :: (Foldable f, Monoid (f (Async ()))) => TVar (f (Async ())) -> IO ()
|
||||
cancelActions as = atomically (swapTVar as mempty) >>= mapM_ (forkIO . uninterruptibleCancel)
|
||||
|
||||
withConnLock :: MonadUnliftIO m => AgentClient -> ConnId -> String -> m a -> m a
|
||||
withConnLock _ "" _ = id
|
||||
withConnLock AgentClient {connLocks} connId name = withLockMap_ connLocks connId name
|
||||
@@ -1556,27 +1543,32 @@ data AgentWorkersDetails = AgentWorkersDetails
|
||||
{ smpClients_ :: [Text],
|
||||
ntfClients_ :: [Text],
|
||||
xftpClients_ :: [Text],
|
||||
smpDeliveryWorkers_ :: Map Text Int,
|
||||
asyncCmdWorkers_ :: Map Text Int,
|
||||
smpDeliveryWorkers_ :: Map Text WorkersDetails,
|
||||
asyncCmdWorkers_ :: Map Text WorkersDetails,
|
||||
smpSubWorkers_ :: [Text],
|
||||
asyncCients_ :: [Int],
|
||||
ntfWorkers_ :: Map Text Int,
|
||||
ntfSMPWorkers_ :: Map Text Int,
|
||||
xftpRcvWorkers_ :: Map Text Int,
|
||||
xftpSndWorkers_ :: Map Text Int,
|
||||
xftpDelWorkers_ :: Map Text Int
|
||||
ntfWorkers_ :: Map Text WorkersDetails,
|
||||
ntfSMPWorkers_ :: Map Text WorkersDetails,
|
||||
xftpRcvWorkers_ :: Map Text WorkersDetails,
|
||||
xftpSndWorkers_ :: Map Text WorkersDetails,
|
||||
xftpDelWorkers_ :: Map Text WorkersDetails
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data WorkersDetails = WorkersDetails
|
||||
{ restarts :: Int,
|
||||
hasWork :: Bool,
|
||||
hasAction :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
getAgentWorkersDetails :: MonadIO m => AgentClient -> m AgentWorkersDetails
|
||||
getAgentWorkersDetails AgentClient {smpClients, ntfClients, xftpClients, smpDeliveryWorkers, asyncCmdWorkers, smpSubWorkers, asyncClients = TAsyncs {actions}, agentEnv} = do
|
||||
getAgentWorkersDetails AgentClient {smpClients, ntfClients, xftpClients, smpDeliveryWorkers, asyncCmdWorkers, smpSubWorkers, agentEnv} = do
|
||||
smpClients_ <- textKeys <$> readTVarIO smpClients
|
||||
ntfClients_ <- textKeys <$> readTVarIO ntfClients
|
||||
xftpClients_ <- textKeys <$> readTVarIO xftpClients
|
||||
smpDeliveryWorkers_ <- workerStats . fmap fst =<< readTVarIO smpDeliveryWorkers
|
||||
asyncCmdWorkers_ <- workerStats =<< readTVarIO asyncCmdWorkers
|
||||
smpSubWorkers_ <- textKeys <$> readTVarIO smpSubWorkers
|
||||
asyncCients_ <- M.keys <$> readTVarIO actions
|
||||
ntfWorkers_ <- workerStats =<< readTVarIO ntfWorkers
|
||||
ntfSMPWorkers_ <- workerStats =<< readTVarIO ntfSMPWorkers
|
||||
xftpRcvWorkers_ <- workerStats =<< readTVarIO xftpRcvWorkers
|
||||
@@ -1590,7 +1582,6 @@ getAgentWorkersDetails AgentClient {smpClients, ntfClients, xftpClients, smpDeli
|
||||
smpDeliveryWorkers_,
|
||||
asyncCmdWorkers_,
|
||||
smpSubWorkers_,
|
||||
asyncCients_,
|
||||
ntfWorkers_,
|
||||
ntfSMPWorkers_,
|
||||
xftpRcvWorkers_,
|
||||
@@ -1602,10 +1593,12 @@ getAgentWorkersDetails AgentClient {smpClients, ntfClients, xftpClients, smpDeli
|
||||
textKeys = map textKey . M.keys
|
||||
textKey :: StrEncoding k => k -> Text
|
||||
textKey = decodeASCII . strEncode
|
||||
workerStats :: (StrEncoding k, MonadIO m) => Map k Worker -> m (Map Text Int)
|
||||
workerStats ws = fmap M.fromList . forM (M.toList ws) $ \(qa, Worker {restarts}) -> do
|
||||
workerStats :: (StrEncoding k, MonadIO m) => Map k Worker -> m (Map Text WorkersDetails)
|
||||
workerStats ws = fmap M.fromList . forM (M.toList ws) $ \(qa, Worker {restarts, doWork, action}) -> do
|
||||
RestartCount {restartCount} <- readTVarIO restarts
|
||||
pure (textKey qa, restartCount)
|
||||
hasWork <- atomically $ not <$> isEmptyTMVar doWork
|
||||
hasAction <- atomically $ not <$> isEmptyTMVar action
|
||||
pure (textKey qa, WorkersDetails {restarts = restartCount, hasWork, hasAction})
|
||||
Env {ntfSupervisor, xftpAgent} = agentEnv
|
||||
NtfSupervisor {ntfWorkers, ntfSMPWorkers} = ntfSupervisor
|
||||
XFTPAgent {xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers} = xftpAgent
|
||||
@@ -1617,7 +1610,6 @@ data AgentWorkersSummary = AgentWorkersSummary
|
||||
smpDeliveryWorkersCount :: Int,
|
||||
asyncCmdWorkersCount :: Int,
|
||||
smpSubWorkersCount :: Int,
|
||||
asyncCientsCount :: Int,
|
||||
ntfWorkersCount :: Int,
|
||||
ntfSMPWorkersCount :: Int,
|
||||
xftpRcvWorkersCount :: Int,
|
||||
@@ -1627,14 +1619,13 @@ data AgentWorkersSummary = AgentWorkersSummary
|
||||
deriving (Show)
|
||||
|
||||
getAgentWorkersSummary :: MonadIO m => AgentClient -> m AgentWorkersSummary
|
||||
getAgentWorkersSummary AgentClient {smpClients, ntfClients, xftpClients, smpDeliveryWorkers, asyncCmdWorkers, smpSubWorkers, asyncClients = TAsyncs {actions}, agentEnv} = do
|
||||
getAgentWorkersSummary AgentClient {smpClients, ntfClients, xftpClients, smpDeliveryWorkers, asyncCmdWorkers, smpSubWorkers, agentEnv} = do
|
||||
smpClientsCount <- M.size <$> readTVarIO smpClients
|
||||
ntfClientsCount <- M.size <$> readTVarIO ntfClients
|
||||
xftpClientsCount <- M.size <$> readTVarIO xftpClients
|
||||
smpDeliveryWorkersCount <- M.size <$> readTVarIO smpDeliveryWorkers
|
||||
asyncCmdWorkersCount <- M.size <$> readTVarIO asyncCmdWorkers
|
||||
smpSubWorkersCount <- M.size <$> readTVarIO smpSubWorkers
|
||||
asyncCientsCount <- M.size <$> readTVarIO actions
|
||||
ntfWorkersCount <- M.size <$> readTVarIO ntfWorkers
|
||||
ntfSMPWorkersCount <- M.size <$> readTVarIO ntfSMPWorkers
|
||||
xftpRcvWorkersCount <- M.size <$> readTVarIO xftpRcvWorkers
|
||||
@@ -1648,7 +1639,6 @@ getAgentWorkersSummary AgentClient {smpClients, ntfClients, xftpClients, smpDeli
|
||||
smpDeliveryWorkersCount,
|
||||
asyncCmdWorkersCount,
|
||||
smpSubWorkersCount,
|
||||
asyncCientsCount,
|
||||
ntfWorkersCount,
|
||||
ntfSMPWorkersCount,
|
||||
xftpRcvWorkersCount,
|
||||
@@ -1670,6 +1660,7 @@ $(J.deriveJSON defaultJSON ''SubInfo)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SubscriptionsInfo)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''WorkersDetails)
|
||||
$(J.deriveJSON defaultJSON {J.fieldLabelModifier = takeWhile (/= '_')} ''AgentWorkersDetails)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''AgentWorkersSummary)
|
||||
|
||||
@@ -34,21 +34,21 @@ import UnliftIO.STM
|
||||
-- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs
|
||||
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> m ()
|
||||
runSMPAgent t cfg initServers store =
|
||||
runSMPAgentBlocking t cfg initServers store =<< newEmptyTMVarIO
|
||||
runSMPAgentBlocking t cfg initServers store 0 =<< newEmptyTMVarIO
|
||||
|
||||
-- | Runs an SMP agent as a TCP service using passed configuration with signalling.
|
||||
--
|
||||
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
|
||||
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
|
||||
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> TMVar Bool -> m ()
|
||||
runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store started = do
|
||||
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> Int -> TMVar Bool -> m ()
|
||||
runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store initClientId started = do
|
||||
liftIO (newSMPAgentEnv cfg store) >>= runReaderT (smpAgent t)
|
||||
where
|
||||
smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' ()
|
||||
smpAgent _ = do
|
||||
-- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation
|
||||
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
|
||||
clientId <- newTVarIO 0
|
||||
clientId <- newTVarIO initClientId
|
||||
runTransportServer started tcpPort tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do
|
||||
liftIO . putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion
|
||||
cId <- atomically $ stateTVar clientId $ \i -> (i + 1, i + 1)
|
||||
|
||||
@@ -1043,18 +1043,31 @@ deletePendingMsgs db connId SndQueue {dbQueueId} =
|
||||
DB.execute db "DELETE FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ?" (connId, dbQueueId)
|
||||
|
||||
getExpiredSndMessages :: DB.Connection -> ConnId -> SndQueue -> UTCTime -> IO [InternalId]
|
||||
getExpiredSndMessages db connId SndQueue {dbQueueId} expireTs =
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT d.internal_id
|
||||
FROM snd_message_deliveries d
|
||||
JOIN messages m ON m.conn_id = d.conn_id AND m.internal_id = d.internal_id
|
||||
WHERE d.conn_id = ? AND d.snd_queue_id = ? AND d.failed = 0 AND m.internal_ts < ?
|
||||
ORDER BY d.internal_id ASC
|
||||
|]
|
||||
(connId, dbQueueId, expireTs)
|
||||
getExpiredSndMessages db connId SndQueue {dbQueueId} expireTs = do
|
||||
-- type is Maybe InternalId because MAX always returns one row, possibly with NULL value
|
||||
maxId :: [Maybe InternalId] <-
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT MAX(internal_id)
|
||||
FROM messages
|
||||
WHERE conn_id = ? AND internal_snd_id IS NOT NULL AND internal_ts < ?
|
||||
|]
|
||||
(connId, expireTs)
|
||||
case maxId of
|
||||
Just msgId : _ ->
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT internal_id
|
||||
FROM snd_message_deliveries
|
||||
WHERE conn_id = ? AND snd_queue_id = ? AND failed = 0 AND internal_id <= ?
|
||||
ORDER BY internal_id ASC
|
||||
|]
|
||||
(connId, dbQueueId, msgId)
|
||||
_ -> pure []
|
||||
|
||||
setMsgUserAck :: DB.Connection -> ConnId -> InternalId -> IO (Either StoreError (RcvQueue, SMP.MsgId))
|
||||
setMsgUserAck db connId agentMsgId = runExceptT $ do
|
||||
|
||||
@@ -67,6 +67,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230814_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231222_command_created_at
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231225_failed_work_items
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -102,7 +103,8 @@ schemaMigrations =
|
||||
("m20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
|
||||
("m20230829_crypto_files", m20230829_crypto_files, Just down_m20230829_crypto_files),
|
||||
("m20231222_command_created_at", m20231222_command_created_at, Just down_m20231222_command_created_at),
|
||||
("m20231225_failed_work_items", m20231225_failed_work_items, Just down_m20231225_failed_work_items)
|
||||
("m20231225_failed_work_items", m20231225_failed_work_items, Just down_m20231225_failed_work_items),
|
||||
("m20240121_message_delivery_indexes", m20240121_message_delivery_indexes, Just down_m20240121_message_delivery_indexes)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20240121_message_delivery_indexes :: Query
|
||||
m20240121_message_delivery_indexes =
|
||||
[sql|
|
||||
CREATE INDEX idx_messages_snd_expired ON messages(conn_id, internal_snd_id, internal_ts);
|
||||
CREATE INDEX idx_snd_message_deliveries_expired ON snd_message_deliveries(conn_id, snd_queue_id, failed, internal_id);
|
||||
|]
|
||||
|
||||
down_m20240121_message_delivery_indexes :: Query
|
||||
down_m20240121_message_delivery_indexes =
|
||||
[sql|
|
||||
DROP INDEX idx_messages_snd_expired;
|
||||
DROP INDEX idx_snd_message_deliveries_expired;
|
||||
|]
|
||||
@@ -497,3 +497,14 @@ CREATE INDEX idx_commands_server_commands ON commands(
|
||||
CREATE INDEX idx_rcv_files_status_created_at ON rcv_files(status, created_at);
|
||||
CREATE INDEX idx_snd_files_status_created_at ON snd_files(status, created_at);
|
||||
CREATE INDEX idx_snd_files_snd_file_entity_id ON snd_files(snd_file_entity_id);
|
||||
CREATE INDEX idx_messages_snd_expired ON messages(
|
||||
conn_id,
|
||||
internal_snd_id,
|
||||
internal_ts
|
||||
);
|
||||
CREATE INDEX idx_snd_message_deliveries_expired ON snd_message_deliveries(
|
||||
conn_id,
|
||||
snd_queue_id,
|
||||
failed,
|
||||
internal_id
|
||||
);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
module Simplex.Messaging.Agent.TAsyncs where
|
||||
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import UnliftIO.Async (Async, async)
|
||||
import UnliftIO.STM
|
||||
|
||||
data TAsyncs = TAsyncs
|
||||
{ actionId :: TVar Int,
|
||||
actions :: TMap Int (Async ())
|
||||
}
|
||||
|
||||
newTAsyncs :: STM TAsyncs
|
||||
newTAsyncs = TAsyncs <$> newTVar 0 <*> TM.empty
|
||||
|
||||
newAsyncAction :: MonadUnliftIO m => (Int -> m ()) -> TAsyncs -> m ()
|
||||
newAsyncAction action as = do
|
||||
aId <- atomically $ stateTVar (actionId as) $ \i -> (i + 1, i + 1)
|
||||
a <- async $ action aId
|
||||
atomically $ TM.insert aId a $ actions as
|
||||
|
||||
removeAsyncAction :: Int -> TAsyncs -> STM ()
|
||||
removeAsyncAction aId = TM.delete aId . actions
|
||||
@@ -346,12 +346,12 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
|
||||
action <-
|
||||
async $
|
||||
runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar)
|
||||
`finally` atomically (putTMVar cVar $ Left PCENetworkError)
|
||||
`finally` atomically (tryPutTMVar cVar $ Left PCENetworkError)
|
||||
c_ <- tcpConnectTimeout `timeout` atomically (takeTMVar cVar)
|
||||
pure $ case c_ of
|
||||
Just (Right c') -> Right c' {action = Just action}
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left PCENetworkError
|
||||
case c_ of
|
||||
Just (Right c') -> pure $ Right c' {action = Just action}
|
||||
Just (Left e) -> pure $ Left e
|
||||
Nothing -> cancel action $> Left PCENetworkError
|
||||
|
||||
useTransport :: (ServiceName, ATransport)
|
||||
useTransport = case port srv of
|
||||
|
||||
@@ -30,7 +30,7 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
ntfServerVersion :: String
|
||||
ntfServerVersion = "1.7.1.0"
|
||||
ntfServerVersion = "1.7.2.0"
|
||||
|
||||
defaultSMPBatchDelay :: Int
|
||||
defaultSMPBatchDelay = 10000
|
||||
|
||||
@@ -65,6 +65,7 @@ import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except (throwE)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Bitraversable (bimapM)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -80,7 +81,7 @@ import qualified Network.TLS.Extra as TE
|
||||
import qualified Paths_simplexmq as SMQ
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Parsers (dropPrefix, parse, parseRead1, sumTypeJSON)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, parseRead1, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Buffer
|
||||
import Simplex.Messaging.Util (bshow, catchAll, catchAll_)
|
||||
import Simplex.Messaging.Version
|
||||
@@ -358,8 +359,9 @@ smpThHandle th v = (th :: THandle c) {thVersion = v, batch = v >= 4}
|
||||
sendHandshake :: (Transport c, Encoding smp) => THandle c -> smp -> ExceptT TransportError IO ()
|
||||
sendHandshake th = ExceptT . tPutBlock th . smpEncode
|
||||
|
||||
-- ignores tail bytes to allow future extensions
|
||||
getHandshake :: (Transport c, Encoding smp) => THandle c -> ExceptT TransportError IO smp
|
||||
getHandshake th = ExceptT $ (parse smpP (TEHandshake PARSE) =<<) <$> tGetBlock th
|
||||
getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP =<<) <$> tGetBlock th
|
||||
|
||||
smpTHandle :: Transport c => c -> THandle c
|
||||
smpTHandle c = THandle {connection = c, sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = 0, batch = False}
|
||||
|
||||
@@ -21,6 +21,7 @@ module Simplex.Messaging.Transport.Client
|
||||
where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
@@ -48,7 +49,7 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll, parseString)
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Simplex.Messaging.Util (bshow, (<$?>))
|
||||
import Simplex.Messaging.Util (bshow, (<$?>), catchAll, tshow)
|
||||
import System.IO.Error
|
||||
import Text.Read (readMaybe)
|
||||
import UnliftIO.Exception (IOException)
|
||||
@@ -135,7 +136,7 @@ runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy,
|
||||
_ -> connectTCPClient hostName
|
||||
c <- liftIO $ do
|
||||
sock <- connectTCP port
|
||||
mapM_ (setSocketKeepAlive sock) tcpKeepAlive
|
||||
mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e)
|
||||
let tCfg = clientTransportConfig cfg
|
||||
connectTLS (Just hostName) tCfg clientParams sock >>= getClientConnection tCfg
|
||||
client c `E.finally` liftIO (closeConnection c)
|
||||
|
||||
+5
-6
@@ -359,7 +359,6 @@ testServerConnectionAfterError t _ = do
|
||||
withAgent2 $ \alice -> do
|
||||
withServer $ do
|
||||
connect (bob, "bob") (alice, "alice")
|
||||
|
||||
bob <#. ("", "", DOWN server ["alice"])
|
||||
alice <#. ("", "", DOWN server ["bob"])
|
||||
alice #: ("1", "bob", "SEND F 5\nhello") #> ("1", "bob", MID 4)
|
||||
@@ -386,10 +385,10 @@ testServerConnectionAfterError t _ = do
|
||||
where
|
||||
server = SMPServer "localhost" testPort2 testKeyHash
|
||||
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
|
||||
withAgent1 = withAgent agentTestPort testDB
|
||||
withAgent2 = withAgent agentTestPort2 testDB2
|
||||
withAgent :: String -> FilePath -> (c -> IO a) -> IO a
|
||||
withAgent agentPort agentDB = withSmpAgentThreadOn_ (ATransport t) (agentPort, testPort2, agentDB) (pure ()) . const . testSMPAgentClientOn agentPort
|
||||
withAgent1 = withAgent agentTestPort testDB 0
|
||||
withAgent2 = withAgent agentTestPort2 testDB2 10
|
||||
withAgent :: String -> FilePath -> Int -> (c -> IO a) -> IO a
|
||||
withAgent agentPort agentDB initClientId = withSmpAgentThreadOn_ (ATransport t) (agentPort, testPort2, agentDB) initClientId (pure ()) . const . testSMPAgentClientOn agentPort
|
||||
|
||||
testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO ()
|
||||
testMsgDeliveryAgentRestart t bob = do
|
||||
@@ -424,7 +423,7 @@ testMsgDeliveryAgentRestart t bob = do
|
||||
removeFile testDB
|
||||
where
|
||||
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
|
||||
withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) (pure ()) . const . testSMPAgentClientOn agentTestPort
|
||||
withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) 0 (pure ()) . const . testSMPAgentClientOn agentTestPort
|
||||
|
||||
testConcurrentMsgDelivery :: Transport c => TProxy c -> c -> c -> IO ()
|
||||
testConcurrentMsgDelivery _ alice bob = do
|
||||
|
||||
@@ -225,15 +225,15 @@ fastMessageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow =
|
||||
|
||||
type AgentTestMonad m = (MonadUnliftIO m, MonadRandom m, MonadFail m)
|
||||
|
||||
withSmpAgentThreadOn_ :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> m () -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn_ t (port', smpPort', db') afterProcess =
|
||||
withSmpAgentThreadOn_ :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> Int -> m () -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn_ t (port', smpPort', db') initClientId afterProcess =
|
||||
let cfg' = agentCfg {tcpPort = port'}
|
||||
initServers' = initAgentServers {smp = userServers [ProtoServerWithAuth (SMPServer "localhost" smpPort' testKeyHash) Nothing]}
|
||||
in serverBracket
|
||||
( \started -> do
|
||||
Right st <- liftIO $ createAgentStore db' "" False MCError
|
||||
when (dbNew st) . liftIO $ withTransaction' st (`SQL.execute_` "INSERT INTO users (user_id) VALUES (1)")
|
||||
runSMPAgentBlocking t cfg' initServers' st started
|
||||
runSMPAgentBlocking t cfg' initServers' st initClientId started
|
||||
)
|
||||
afterProcess
|
||||
|
||||
@@ -241,7 +241,7 @@ userServers :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ProtoSe
|
||||
userServers srvs = M.fromList [(1, srvs)]
|
||||
|
||||
withSmpAgentThreadOn :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> (ThreadId -> m a) -> m a
|
||||
withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a $ removeFile db'
|
||||
withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a 0 $ removeFile db'
|
||||
|
||||
withSmpAgentOn :: AgentTestMonad m => ATransport -> (ServiceName, ServiceName, FilePath) -> m a -> m a
|
||||
withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ serverBracket process afterProcess f = do
|
||||
E.bracket
|
||||
(forkIOWithUnmask ($ process started))
|
||||
(\t -> killThread t >> afterProcess >> waitFor started "stop")
|
||||
(\t -> waitFor started "start" >> f t)
|
||||
(\t -> waitFor started "start" >> f t >>= \r -> r <$ threadDelay 100000)
|
||||
where
|
||||
waitFor started s =
|
||||
5_000_000 `timeout` atomically (takeTMVar started) >>= \case
|
||||
|
||||
Reference in New Issue
Block a user