Files
simplexmq/src/Simplex/Messaging/Server/StatsStore.hs
T

441 lines
15 KiB
Haskell

{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.Messaging.Server.StatsStore
( StatsStore (..),
loadStats,
saveStats,
#if defined(dbServerPostgres)
PeriodSet (..),
getDBStats,
#endif
)
where
import qualified Control.Exception as E
import Control.Logger.Simple
import Control.Monad
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Server.Stats
import Simplex.Messaging.Util (ifM, tshow)
import System.Directory (doesFileExist, removeFile, renameFile)
import UnliftIO.Exception (tryAny)
#if defined(dbServerPostgres)
import Control.Concurrent.STM (readTVarIO)
import Data.Bifunctor (bimap)
import Data.IORef (IORef)
import Data.Int (Int16, Int64)
import Data.IntSet (IntSet)
import qualified Data.IntSet as IS
import Data.List (find, intersperse)
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
import Database.PostgreSQL.Simple (Only (..), Query)
import qualified Database.PostgreSQL.Simple as DB
import Database.PostgreSQL.Simple.FromRow (FromRow (..), field)
import Database.PostgreSQL.Simple.ToField (ToField (..))
import Simplex.Messaging.Agent.Store.Postgres.Common (DBStore (..), withTransaction)
import Simplex.Messaging.Util (toChunks, whenM)
import UnliftIO.Exception (isSyncException)
#endif
data StatsStore
= StatsFile FilePath
#if defined(dbServerPostgres)
| StatsDB DBStore FilePath
#endif
loadStats :: StatsStore -> IO (Maybe ServerStatsData)
loadStats = \case
StatsFile f -> ifM (doesFileExist f) (Just <$> readStatsFile f) (pure Nothing)
#if defined(dbServerPostgres)
StatsDB st f ->
getDBStats st >>= \case
Just d -> do
whenM (doesFileExist f) $ logWarn $ "server stats file " <> T.pack f <> " is ignored, stats are restored from the database"
pure $ Just d
Nothing -> ifM (doesFileExist f) (Just <$> importStatsFile st f) (pure Nothing)
#endif
saveStats :: StatsStore -> ServerStats -> IO ()
saveStats = \case
StatsFile f -> saveStatsFile f
#if defined(dbServerPostgres)
StatsDB st _ -> saveDBStats st
#endif
readStatsFile :: FilePath -> IO ServerStatsData
readStatsFile f = do
logNote $ "restoring server stats from file " <> T.pack f
strDecode <$> B.readFile f >>= \case
Right d -> pure d
Left e -> E.throwIO $ userError $ "error parsing server stats file " <> f <> ": " <> e
saveStatsFile :: FilePath -> ServerStats -> IO ()
saveStatsFile f ss = do
d <- getServerStatsData ss
tryAny (B.writeFile tmp (strEncode d) >> renameFile tmp f) >>= \case
Right () -> pure ()
Left e -> do
logError $ "error saving server stats to file " <> T.pack f <> ": " <> tshow e
void $ tryAny $ removeFile tmp
where
tmp = f <> ".tmp"
#if defined(dbServerPostgres)
data PeriodSet = PSMsgDay | PSMsgWeek | PSMsgMonth | PSNtfDay | PSNtfWeek | PSNtfMonth
deriving (Bounded, Enum, Eq, Ord, Show)
-- stored in server_stats_queues.period_set, the values must not change
periodSetId :: PeriodSet -> Int16
periodSetId = \case
PSMsgDay -> 1
PSMsgWeek -> 2
PSMsgMonth -> 3
PSNtfDay -> 4
PSNtfWeek -> 5
PSNtfMonth -> 6
instance ToField PeriodSet where
toField = toField . periodSetId
periodSetFromId :: Int16 -> Maybe PeriodSet
periodSetFromId i = find ((i ==) . periodSetId) [minBound .. maxBound]
periodSetHashes :: ServerStatsData -> PeriodSet -> IntSet
periodSetHashes ServerStatsData {_activeQueues = q, _activeQueuesNtf = n} = \case
PSMsgDay -> _day q
PSMsgWeek -> _week q
PSMsgMonth -> _month q
PSNtfDay -> _day n
PSNtfWeek -> _week n
PSNtfMonth -> _month n
periodSetPending :: ServerStats -> PeriodSet -> Maybe (IORef PendingHashes)
periodSetPending ServerStats {activeQueues, activeQueuesNtf} = \case
PSMsgDay -> pendingDay <$> pendingPeriods activeQueues
PSMsgWeek -> pendingWeek <$> pendingPeriods activeQueues
PSMsgMonth -> pendingMonth <$> pendingPeriods activeQueues
PSNtfDay -> pendingDay <$> pendingPeriods activeQueuesNtf
PSNtfWeek -> pendingWeek <$> pendingPeriods activeQueuesNtf
PSNtfMonth -> pendingMonth <$> pendingPeriods activeQueuesNtf
getDBStats :: DBStore -> IO (Maybe ServerStatsData)
getDBStats st =
withTransaction st $ \db ->
DB.query_ db selectStatsQuery >>= \case
StatsRow d : _ -> Just . withPeriodSets d <$> DB.fold_ db "SELECT period_set, queue_hash FROM server_stats_queues" M.empty addHash
[] -> pure Nothing
where
addHash sets (psId :: Int16, h :: Int64) = case periodSetFromId psId of
Just ps -> pure $! M.alter (Just . maybe (IS.singleton h') (IS.insert h')) ps sets
Nothing -> E.throwIO $ userError $ "unknown period_set " <> show psId
where
h' = fromIntegral h
withPeriodSets d sets =
d
{ _activeQueues = periodData PSMsgDay PSMsgWeek PSMsgMonth,
_activeQueuesNtf = periodData PSNtfDay PSNtfWeek PSNtfMonth
}
where
periodData dp wp mp = PeriodStatsData {_day = get dp, _week = get wp, _month = get mp}
get p = M.findWithDefault IS.empty p sets
importStatsFile :: DBStore -> FilePath -> IO ServerStatsData
importStatsFile st f = do
d <- readStatsFile f
withTransaction st $ \db -> do
void $ DB.execute_ db "DELETE FROM server_stats_queues"
forM_ [minBound .. maxBound] $ \ps -> insertHashes db ps $ periodSetHashes d ps
upsertStats db d
renameFile f $ f <> ".bak"
logNote "server stats imported to the database"
pure d
saveDBStats :: DBStore -> ServerStats -> IO ()
saveDBStats st ss = ifM (readTVarIO $ dbClosed st) (logWarn "server stats are not saved: database store is closed") $ E.mask $ \unmask -> do
taken <- fmap catMaybes $ forM [minBound .. maxBound] $ \ps ->
forM (periodSetPending ss ps) $ \ref -> (ps,ref,) <$> takePendingHashes ref
d <- getServerStatsData ss
E.try @E.SomeException (unmask $ withTransaction st $ \db -> mapM_ (savePending db) taken >> upsertStats db d) >>= \case
Right () -> pure ()
Left e -> do
forM_ taken $ \(_, ref, p) -> restorePendingHashes ref p
if isSyncException e
then logError $ "error saving server stats to the database: " <> tshow e
else E.throwIO e
where
savePending db (ps, _, PendingHashes {pendingReset, pendingAdded}) = do
when pendingReset $ void $ DB.execute db "DELETE FROM server_stats_queues WHERE period_set = ?" (Only ps)
insertHashes db ps pendingAdded
insertHashes :: DB.Connection -> PeriodSet -> IntSet -> IO ()
insertHashes db ps hs =
forM_ (toChunks hashInsertBatch $ IS.toList hs) $ \hs' ->
void $
DB.executeMany
db
"INSERT INTO server_stats_queues (period_set, queue_hash) VALUES (?, ?) ON CONFLICT DO NOTHING"
(map (\h -> (ps, fromIntegral h :: Int64)) $ L.toList hs')
-- rows per INSERT statement, bounds the query size of the one-time file import
hashInsertBatch :: Int
hashInsertBatch = 10000
upsertStats :: DB.Connection -> ServerStatsData -> IO ()
upsertStats db d = do
savedAt <- systemSeconds <$> getSystemTime
void $ DB.execute db upsertStatsQuery $ savedAt : map (($ d) . snd) statsFields
selectStatsQuery :: Query
selectStatsQuery = "SELECT " <> commaSep (map fst statsFields) <> " FROM server_stats WHERE server_stats_id = 1"
upsertStatsQuery :: Query
upsertStatsQuery =
"INSERT INTO server_stats (server_stats_id, saved_at, "
<> commaSep cols
<> ") VALUES (1, ?, "
<> commaSep (map (const "?") cols)
<> ") ON CONFLICT (server_stats_id) DO UPDATE SET "
<> commaSep (map (\c -> c <> " = EXCLUDED." <> c) ("saved_at" : cols))
where
cols = map fst statsFields
commaSep :: [Query] -> Query
commaSep = mconcat . intersperse ", "
-- the order must match the FromRow StatsRow instance
statsFields :: [(Query, ServerStatsData -> Int64)]
statsFields =
("from_time", round . utcTimeToPOSIXSeconds . _fromTime)
: map
(fmap (fromIntegral .))
( [ ("q_created", _qCreated),
("q_secured", _qSecured),
("q_deleted_all", _qDeletedAll),
("q_deleted_new", _qDeletedNew),
("q_deleted_secured", _qDeletedSecured),
("q_deleted_all_b", _qDeletedAllB),
("q_blocked", _qBlocked),
("q_sub", _qSub),
("q_sub_all_b", _qSubAllB),
("q_sub_auth", _qSubAuth),
("q_sub_duplicate", _qSubDuplicate),
("q_sub_prohibited", _qSubProhibited),
("q_sub_end", _qSubEnd),
("q_sub_end_b", _qSubEndB),
("ntf_created", _ntfCreated),
("ntf_new_created", _ntfNewCreated),
("ntf_deleted", _ntfDeleted),
("ntf_deleted_b", _ntfDeletedB),
("ntf_sub", _ntfSub),
("ntf_sub_b", _ntfSubB),
("ntf_sub_auth", _ntfSubAuth),
("ntf_sub_duplicate", _ntfSubDuplicate),
("msg_sent", _msgSent),
("msg_sent_auth", _msgSentAuth),
("msg_sent_quota", _msgSentQuota),
("msg_sent_large", _msgSentLarge),
("msg_sent_block", _msgSentBlock),
("msg_recv", _msgRecv),
("msg_recv_get", _msgRecvGet),
("msg_get", _msgGet),
("msg_get_no_msg", _msgGetNoMsg),
("msg_get_auth", _msgGetAuth),
("msg_get_duplicate", _msgGetDuplicate),
("msg_get_prohibited", _msgGetProhibited),
("msg_expired", _msgExpired),
("msg_sent_ntf", _msgSentNtf),
("msg_recv_ntf", _msgRecvNtf),
("msg_ntfs", _msgNtfs),
("msg_ntfs_b", _msgNtfsB),
("msg_ntf_no_sub", _msgNtfNoSub),
("msg_ntf_lost", _msgNtfLost),
("msg_ntf_expired", _msgNtfExpired)
]
<> proxyFields "p_relays" _pRelays
<> proxyFields "p_relays_own" _pRelaysOwn
<> proxyFields "p_msg_fwds" _pMsgFwds
<> proxyFields "p_msg_fwds_own" _pMsgFwdsOwn
<> [("p_msg_fwds_recv", _pMsgFwdsRecv)]
<> serviceFields "rcv_services" _rcvServices
<> serviceFields "ntf_services" _ntfServices
<> map
(fmap (. _rslvStats))
[ ("rslv_reqs", _rslvReqs),
("rslv_succ", _rslvSucc),
("rslv_not_found", _rslvNotFound),
("rslv_resolver_errs", _rslvResolverErrs),
("rslv_disabled", _rslvDisabled)
]
)
where
proxyFields pfx sel =
map
(bimap ((pfx <> "_") <>) (. sel))
[ ("requests", _pRequests),
("successes", _pSuccesses),
("errors_connect", _pErrorsConnect),
("errors_compat", _pErrorsCompat),
("errors_other", _pErrorsOther)
]
serviceFields pfx sel =
map
(bimap ((pfx <> "_") <>) (. sel))
[ ("assoc_new", _srvAssocNew),
("assoc_duplicate", _srvAssocDuplicate),
("assoc_updated", _srvAssocUpdated),
("assoc_removed", _srvAssocRemoved),
("sub_count", _srvSubCount),
("sub_duplicate", _srvSubDuplicate),
("sub_queues", _srvSubQueues),
("sub_end", _srvSubEnd)
]
newtype StatsRow = StatsRow ServerStatsData
instance FromRow StatsRow where
fromRow = do
_fromTime <- posixSecondsToUTCTime . fromIntegral <$> field @Int64
_qCreated <- field
_qSecured <- field
_qDeletedAll <- field
_qDeletedNew <- field
_qDeletedSecured <- field
_qDeletedAllB <- field
_qBlocked <- field
_qSub <- field
_qSubAllB <- field
_qSubAuth <- field
_qSubDuplicate <- field
_qSubProhibited <- field
_qSubEnd <- field
_qSubEndB <- field
_ntfCreated <- field
_ntfNewCreated <- field
_ntfDeleted <- field
_ntfDeletedB <- field
_ntfSub <- field
_ntfSubB <- field
_ntfSubAuth <- field
_ntfSubDuplicate <- field
_msgSent <- field
_msgSentAuth <- field
_msgSentQuota <- field
_msgSentLarge <- field
_msgSentBlock <- field
_msgRecv <- field
_msgRecvGet <- field
_msgGet <- field
_msgGetNoMsg <- field
_msgGetAuth <- field
_msgGetDuplicate <- field
_msgGetProhibited <- field
_msgExpired <- field
_msgSentNtf <- field
_msgRecvNtf <- field
_msgNtfs <- field
_msgNtfsB <- field
_msgNtfNoSub <- field
_msgNtfLost <- field
_msgNtfExpired <- field
_pRelays <- proxyStatsP
_pRelaysOwn <- proxyStatsP
_pMsgFwds <- proxyStatsP
_pMsgFwdsOwn <- proxyStatsP
_pMsgFwdsRecv <- field
_rcvServices <- serviceStatsP
_ntfServices <- serviceStatsP
_rslvStats <- NameResolverStatsData <$> field <*> field <*> field <*> field <*> field
pure $
StatsRow
ServerStatsData
{ _fromTime,
_qCreated,
_qSecured,
_qDeletedAll,
_qDeletedAllB,
_qDeletedNew,
_qDeletedSecured,
_qBlocked,
_qSub,
_qSubAllB,
_qSubAuth,
_qSubDuplicate,
_qSubProhibited,
_qSubEnd,
_qSubEndB,
_ntfCreated,
_ntfNewCreated,
_ntfDeleted,
_ntfDeletedB,
_ntfSub,
_ntfSubB,
_ntfSubAuth,
_ntfSubDuplicate,
_msgSent,
_msgSentAuth,
_msgSentQuota,
_msgSentLarge,
_msgSentBlock,
_msgRecv,
_msgRecvAckTimes = emptyTimeBuckets,
_msgRecvGet,
_msgGet,
_msgGetNoMsg,
_msgGetAuth,
_msgGetDuplicate,
_msgGetProhibited,
_msgExpired,
_activeQueues = newPeriodStatsData,
_msgSentNtf,
_msgRecvNtf,
_activeQueuesNtf = newPeriodStatsData,
_msgNtfs,
_msgNtfsB,
_msgNtfNoSub,
_msgNtfLost,
_msgNtfExpired,
_pRelays,
_pRelaysOwn,
_pMsgFwds,
_pMsgFwdsOwn,
_pMsgFwdsRecv,
_ntfServices,
_rcvServices,
_rcvServicesSubMsg = 0,
_rcvServicesSubDuplicate = 0,
_authDelayExceeded = 0,
_cmdSlotWaits = 0,
_cmdSlotWaitMs = 0,
_cmdSlotWaitMaxMs = 0,
_cmdSlotWaitTimeouts = 0,
_qCount = 0,
_msgCount = 0,
_ntfCount = 0,
_rslvStats
}
where
proxyStatsP = ProxyStatsData <$> field <*> field <*> field <*> field <*> field
serviceStatsP = do
_srvAssocNew <- field
_srvAssocDuplicate <- field
_srvAssocUpdated <- field
_srvAssocRemoved <- field
_srvSubCount <- field
_srvSubDuplicate <- field
_srvSubQueues <- field
_srvSubEnd <- field
pure newServiceStatsData {_srvAssocNew, _srvAssocDuplicate, _srvAssocUpdated, _srvAssocRemoved, _srvSubCount, _srvSubDuplicate, _srvSubQueues, _srvSubEnd}
#endif