Files
simplexmq/src/Simplex/Messaging/Server/StoreLog.hs
Evgeny 1329fc726f smp: support client notices (#1659)
* agent: support client notices

* improve

* fix, test

* rename

* cleanup

* send and process notices in more cases

* dont delete

* dont remove notice on other permanent errors

* dont remove notice if there is no notice ID in queue

* add server to error

* allow deleting

* only use notice if key hash matches
2025-10-17 18:34:59 +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.QueueStore
import Simplex.Messaging.Server.StoreLog.Types
import Simplex.Messaging.SystemTime
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 SystemDate
| 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 -> SystemDate -> 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"