Files
simplexmq/src/Simplex/Messaging/Server/StoreLog.hs
Evgeny da37384335 smp server: batch commands (#1559)
* protocol: refactor types and encoding

* clean

* smp server: batch commands (#1560)

* smp server: batch commands verification into one DB transaction

* ghc 8.10.7

* flatten transmission tuples

* diff

* only use batch logic if there is more than one transmission

* func

* reset NTF service when adding notifier

* version

* Revert "smp server: use separate database pool for reading queues and creating service records (#1561)"

This reverts commit 3df2425162.

* version

* Revert "version"

This reverts commit d80a6b74c5.
2025-06-12 23:05:04 +01:00

367 lines
13 KiB
Haskell

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Simplex.Messaging.Server.StoreLog
( StoreLog, -- constructors are not exported
StoreLogRecord (..), -- used in tests
openWriteStoreLog,
openReadStoreLog,
storeLogFilePath,
closeStoreLog,
writeStoreLogRecord,
logCreateQueue,
logCreateLink,
logDeleteLink,
logSecureQueue,
logUpdateKeys,
logAddNotifier,
logSuspendQueue,
logBlockQueue,
logUnblockQueue,
logDeleteQueue,
logDeleteNotifier,
logUpdateQueueTime,
logNewService,
logQueueService,
readWriteStoreLog,
readLogLines,
foldLogLines,
)
where
import Control.Applicative (optional, (<|>))
import qualified Control.Exception as E
import Control.Logger.Simple
import Control.Monad
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.List (sort, stripPrefix)
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (mapMaybe)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime, nominalDay)
import Data.Time.Format.ISO8601 (iso8601Show, iso8601ParseM)
import GHC.IO (catchAny)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
-- import Simplex.Messaging.Server.MsgStore.Types
import Simplex.Messaging.Server.QueueStore
import Simplex.Messaging.Server.StoreLog.Types
import Simplex.Messaging.Util (ifM, tshow, unlessM, whenM)
import System.Directory (doesFileExist, listDirectory, removeFile, renameFile)
import System.IO
import System.FilePath (takeDirectory, takeFileName)
data StoreLogRecord
= CreateQueue RecipientId QueueRec
| CreateLink RecipientId LinkId QueueLinkData
| DeleteLink RecipientId
| SecureQueue QueueId SndPublicAuthKey
| UpdateKeys RecipientId (NonEmpty RcvPublicAuthKey)
| AddNotifier QueueId NtfCreds
| SuspendQueue QueueId
| BlockQueue QueueId BlockingInfo
| UnblockQueue QueueId
| DeleteQueue QueueId
| DeleteNotifier QueueId
| UpdateTime QueueId RoundedSystemTime
| NewService ServiceRec
| QueueService RecipientId ASubscriberParty (Maybe ServiceId)
deriving (Show)
data SLRTag
= CreateQueue_
| CreateLink_
| DeleteLink_
| SecureQueue_
| UpdateKeys_
| AddNotifier_
| SuspendQueue_
| BlockQueue_
| UnblockQueue_
| DeleteQueue_
| DeleteNotifier_
| UpdateTime_
| NewService_
| QueueService_
instance StrEncoding QueueRec where
strEncode QueueRec {recipientKeys, rcvDhSecret, rcvServiceId, senderId, senderKey, queueMode, queueData, notifier, status, updatedAt} =
B.concat
[ p "rk=" recipientKeys,
p " rdh=" rcvDhSecret,
p " sid=" senderId,
p " sk=" senderKey,
maybe "" ((" queue_mode=" <>) . smpEncode) queueMode,
opt " link_id=" (fst <$> queueData),
opt " queue_data=" (snd <$> queueData),
opt " notifier=" notifier,
opt " updated_at=" updatedAt,
statusStr,
opt " rsrv=" rcvServiceId
]
where
p :: StrEncoding a => ByteString -> a -> ByteString
p param = (param <>) . strEncode
opt :: StrEncoding a => ByteString -> Maybe a -> ByteString
opt = maybe "" . p
statusStr = case status of
EntityActive -> ""
_ -> " status=" <> strEncode status
strP = do
recipientKeys <- "rk=" *> strP_
rcvDhSecret <- "rdh=" *> strP_
senderId <- "sid=" *> strP_
senderKey <- "sk=" *> strP
queueMode <-
toQueueMode <$> (" sndSecure=" *> strP)
<|> Just <$> (" queue_mode=" *> smpP)
<|> pure Nothing -- unknown queue mode, we cannot imply that it is contact address
queueData <- optional $ (,) <$> (" link_id=" *> strP) <*> (" queue_data=" *> strP)
notifier <- optional $ " notifier=" *> strP
updatedAt <- optional $ " updated_at=" *> strP
status <- (" status=" *> strP) <|> pure EntityActive
rcvServiceId <- optional $ " rsrv=" *> strP
pure
QueueRec
{ recipientKeys,
rcvDhSecret,
senderId,
senderKey,
queueMode,
queueData,
notifier,
status,
updatedAt,
rcvServiceId
}
where
toQueueMode sndSecure = Just $ if sndSecure then QMMessaging else QMContact
instance StrEncoding SLRTag where
strEncode = \case
CreateQueue_ -> "CREATE"
CreateLink_ -> "LINK"
DeleteLink_ -> "LDELETE"
SecureQueue_ -> "SECURE"
UpdateKeys_ -> "KEYS"
AddNotifier_ -> "NOTIFIER"
SuspendQueue_ -> "SUSPEND"
BlockQueue_ -> "BLOCK"
UnblockQueue_ -> "UNBLOCK"
DeleteQueue_ -> "DELETE"
DeleteNotifier_ -> "NDELETE"
UpdateTime_ -> "TIME"
NewService_ -> "NEW_SERVICE"
QueueService_ -> "QUEUE_SERVICE"
strP =
A.choice
[ "CREATE" $> CreateQueue_,
"LINK" $> CreateLink_,
"LDELETE" $> DeleteLink_,
"SECURE" $> SecureQueue_,
"KEYS" $> UpdateKeys_,
"NOTIFIER" $> AddNotifier_,
"SUSPEND" $> SuspendQueue_,
"BLOCK" $> BlockQueue_,
"UNBLOCK" $> UnblockQueue_,
"DELETE" $> DeleteQueue_,
"NDELETE" $> DeleteNotifier_,
"TIME" $> UpdateTime_,
"NEW_SERVICE" $> NewService_,
"QUEUE_SERVICE" $> QueueService_
]
instance StrEncoding StoreLogRecord where
strEncode = \case
CreateQueue rId q -> B.unwords [strEncode CreateQueue_, "rid=" <> strEncode rId, strEncode q]
CreateLink rId lnkId d -> strEncode (CreateLink_, rId, lnkId, d)
DeleteLink rId -> strEncode (DeleteLink_, rId)
SecureQueue rId sKey -> strEncode (SecureQueue_, rId, sKey)
UpdateKeys rId rKeys -> strEncode (UpdateKeys_, rId, rKeys)
AddNotifier rId ntfCreds -> strEncode (AddNotifier_, rId, ntfCreds)
SuspendQueue rId -> strEncode (SuspendQueue_, rId)
BlockQueue rId info -> strEncode (BlockQueue_, rId, info)
UnblockQueue rId -> strEncode (UnblockQueue_, rId)
DeleteQueue rId -> strEncode (DeleteQueue_, rId)
DeleteNotifier rId -> strEncode (DeleteNotifier_, rId)
UpdateTime rId t -> strEncode (UpdateTime_, rId, t)
NewService sr -> strEncode (NewService_, sr)
QueueService rId party serviceId -> strEncode (QueueService_, rId, party, serviceId)
strP =
strP_ >>= \case
CreateQueue_ -> CreateQueue <$> ("rid=" *> strP_) <*> strP
CreateLink_ -> CreateLink <$> strP_ <*> strP_ <*> strP
DeleteLink_ -> DeleteLink <$> strP
SecureQueue_ -> SecureQueue <$> strP_ <*> strP
UpdateKeys_ -> UpdateKeys <$> strP_ <*> strP
AddNotifier_ -> AddNotifier <$> strP_ <*> strP
SuspendQueue_ -> SuspendQueue <$> strP
BlockQueue_ -> BlockQueue <$> strP_ <*> strP
UnblockQueue_ -> UnblockQueue <$> strP
DeleteQueue_ -> DeleteQueue <$> strP
DeleteNotifier_ -> DeleteNotifier <$> strP
UpdateTime_ -> UpdateTime <$> strP_ <*> strP
NewService_ -> NewService <$> strP
QueueService_ -> QueueService <$> strP_ <*> strP_ <*> strP
openWriteStoreLog :: Bool -> FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog append f = do
h <- openFile f $ if append then AppendMode else WriteMode
hSetBuffering h LineBuffering
pure $ WriteStoreLog f h
openReadStoreLog :: FilePath -> IO (StoreLog 'ReadMode)
openReadStoreLog f = do
unlessM (doesFileExist f) (writeFile f "")
ReadStoreLog f <$> openFile f ReadMode
storeLogFilePath :: StoreLog a -> FilePath
storeLogFilePath = \case
WriteStoreLog f _ -> f
ReadStoreLog f _ -> f
closeStoreLog :: StoreLog a -> IO ()
closeStoreLog = \case
WriteStoreLog _ h -> close_ h
ReadStoreLog _ h -> close_ h
where
close_ h = hClose h `catchAny` \e -> logError ("STORE: closeStoreLog, error closing, " <> tshow e)
writeStoreLogRecord :: StrEncoding r => StoreLog 'WriteMode -> r -> IO ()
writeStoreLogRecord (WriteStoreLog _ h) r = E.uninterruptibleMask_ $ do
B.hPut h $ strEncode r `B.snoc` '\n' -- hPutStrLn makes write non-atomic for length > 1024
hFlush h
logCreateQueue :: StoreLog 'WriteMode -> RecipientId -> QueueRec -> IO ()
logCreateQueue s rId q = writeStoreLogRecord s $ CreateQueue rId q
logCreateLink :: StoreLog 'WriteMode -> RecipientId -> LinkId -> QueueLinkData -> IO ()
logCreateLink s rId lnkId d = writeStoreLogRecord s $ CreateLink rId lnkId d
logDeleteLink :: StoreLog 'WriteMode -> RecipientId -> IO ()
logDeleteLink s = writeStoreLogRecord s . DeleteLink
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SndPublicAuthKey -> IO ()
logSecureQueue s qId sKey = writeStoreLogRecord s $ SecureQueue qId sKey
logUpdateKeys :: StoreLog 'WriteMode -> QueueId -> NonEmpty RcvPublicAuthKey -> IO ()
logUpdateKeys s rId rKeys = writeStoreLogRecord s $ UpdateKeys rId rKeys
logAddNotifier :: StoreLog 'WriteMode -> QueueId -> NtfCreds -> IO ()
logAddNotifier s qId ntfCreds = writeStoreLogRecord s $ AddNotifier qId ntfCreds
logSuspendQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
logSuspendQueue s = writeStoreLogRecord s . SuspendQueue
logBlockQueue :: StoreLog 'WriteMode -> QueueId -> BlockingInfo -> IO ()
logBlockQueue s qId info = writeStoreLogRecord s $ BlockQueue qId info
logUnblockQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
logUnblockQueue s = writeStoreLogRecord s . UnblockQueue
logDeleteQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
logDeleteQueue s = writeStoreLogRecord s . DeleteQueue
logDeleteNotifier :: StoreLog 'WriteMode -> QueueId -> IO ()
logDeleteNotifier s = writeStoreLogRecord s . DeleteNotifier
logUpdateQueueTime :: StoreLog 'WriteMode -> QueueId -> RoundedSystemTime -> IO ()
logUpdateQueueTime s qId t = writeStoreLogRecord s $ UpdateTime qId t
logNewService :: StoreLog 'WriteMode -> ServiceRec -> IO ()
logNewService s = writeStoreLogRecord s . NewService
logQueueService :: (PartyI p, ServiceParty p) => StoreLog 'WriteMode -> RecipientId -> SParty p -> Maybe ServiceId -> IO ()
logQueueService s rId party = writeStoreLogRecord s . QueueService rId (ASP party)
readWriteStoreLog :: (FilePath -> s -> IO ()) -> (StoreLog 'WriteMode -> s -> IO ()) -> FilePath -> s -> IO (StoreLog 'WriteMode)
readWriteStoreLog readStore writeStore f st =
ifM
(doesFileExist tempBackup)
(useTempBackup >> readWriteLog)
(ifM (doesFileExist f) readWriteLog (writeLog "creating store log..."))
where
f' = T.pack f
tempBackup = f <> ".start"
useTempBackup = do
-- preserve current file, use temp backup
logWarn $ "Server terminated abnormally on last start, restoring state from " <> T.pack tempBackup
whenM (doesFileExist f) $ do
renameFile f (f <> ".bak")
logNote $ "preserved incomplete state " <> f' <> " as " <> (f' <> ".bak")
renameFile tempBackup f
readWriteLog = do
-- log backup is made in two steps to mitigate the crash during the compacting.
-- Temporary backup file .start will be used when it is present.
readStore f st
renameFile f tempBackup -- 1) make temp backup
s <- writeLog "compacting store log (do not terminate)..." -- 2) save state
renameBackup -- 3) timed backup
removeStoreLogBackups f
pure s
writeLog msg = do
s <- openWriteStoreLog False f
logNote msg
writeStore s st
pure s
renameBackup = do
ts <- getCurrentTime
let timedBackup = f <> "." <> iso8601Show ts
renameFile tempBackup timedBackup
logNote $ "original state preserved as " <> T.pack timedBackup
removeStoreLogBackups :: FilePath -> IO ()
removeStoreLogBackups f = do
ts <- getCurrentTime
times <- sort . mapMaybe backupPathTime <$> listDirectory (takeDirectory f)
let new = addUTCTime (- nominalDay) ts
old = addUTCTime (- oldBackupTTL) ts
times1 = filter (< new) times -- exclude backups newer than 24 hours
times2 = take (length times1 - minOldBackups) times1 -- keep 3 backups older than 24 hours
toDelete = filter (< old) times2 -- remove all backups older than 21 day
mapM_ (removeFile . backupPath) toDelete
when (length toDelete > 0) $ do
putStrLn $ "Removed " <> show (length toDelete) <> " backups:"
mapM_ (putStrLn . backupPath) toDelete
where
backupPathTime :: FilePath -> Maybe UTCTime
backupPathTime = iso8601ParseM <=< stripPrefix backupPathPfx
backupPath :: UTCTime -> FilePath
backupPath ts = f <> "." <> iso8601Show ts
backupPathPfx = takeFileName f <> "."
minOldBackups = 3
oldBackupTTL = 21 * nominalDay
readLogLines :: Bool -> FilePath -> (Bool -> B.ByteString -> IO ()) -> IO ()
readLogLines tty f action = foldLogLines tty f (const action) ()
foldLogLines :: Bool -> FilePath -> (a -> Bool -> B.ByteString -> IO a) -> a -> IO a
foldLogLines tty f action initValue = do
(count :: Int, acc) <- withFile f ReadMode $ \h -> ifM (hIsEOF h) (pure (0, initValue)) (loop h 0 initValue)
putStrLn $ progress count
pure acc
where
loop h !i !acc = do
s <- B.hGetLine h
eof <- hIsEOF h
acc' <- action acc eof s
let i' = i + 1
when (tty && i' `mod` 100000 == 0) $ putStr (progress i' <> "\r") >> hFlush stdout
if eof then pure (i', acc') else loop h i' acc'
progress i = "Processed: " <> show i <> " log lines"