mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-04-02 22:26:00 +00:00
* agent: remove service/queue associations when service ID changes * agent: check that service ID in NEW response matches session ID in transport session * agent subscription WIP * test * comment * enable tests * update queries * agent: option to add SQLite aggregates to DB connection (#1673) * agent: add build_relations_vector function to sqlite * update aggregate * use static aggregate * remove relations --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * add test, treat BAD_SERVICE as temp error, only remove queue associations on service errors * add packZipWith for backward compatibility with GHC 8.10.7 --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
216 lines
8.5 KiB
Haskell
216 lines
8.5 KiB
Haskell
{-# LANGUAGE ConstraintKinds #-}
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE DerivingStrategies #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
{-# LANGUAGE InstanceSigs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE NumericUnderscores #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StandaloneDeriving #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
|
module Simplex.Messaging.Agent.Store.SQLite
|
|
( DBOpts (..),
|
|
Migrations.getCurrentMigrations,
|
|
migrateDBSchema,
|
|
createDBStore,
|
|
closeDBStore,
|
|
reopenDBStore,
|
|
execSQL,
|
|
-- used in Simplex.Chat.Archive
|
|
sqlString,
|
|
keyString,
|
|
storeKey,
|
|
-- used in tests
|
|
connectSQLiteStore,
|
|
openSQLiteStore,
|
|
)
|
|
where
|
|
|
|
import Control.Concurrent.MVar
|
|
import Control.Concurrent.STM
|
|
import Control.Exception (bracketOnError, onException, throwIO)
|
|
import Control.Monad
|
|
import Data.Bits (xor)
|
|
import Data.ByteArray (ScrubbedBytes)
|
|
import qualified Data.ByteArray as BA
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.ByteString as B
|
|
import Data.Functor (($>))
|
|
import Data.IORef
|
|
import Data.Maybe (fromMaybe)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Database.SQLite.Simple (Query (..))
|
|
import qualified Database.SQLite.Simple as SQL
|
|
import Database.SQLite.Simple.QQ (sql)
|
|
import qualified Database.SQLite3 as SQLite3
|
|
import Database.SQLite3.Bindings
|
|
import Foreign.C.Types
|
|
import Foreign.Ptr
|
|
import Simplex.Messaging.Agent.Store.Migrations (DBMigrate (..), sharedMigrateSchema)
|
|
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
|
import Simplex.Messaging.Agent.Store.SQLite.Common
|
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
|
import Simplex.Messaging.Agent.Store.SQLite.Util
|
|
import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfig (..), MigrationError (..))
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Util (ifM, packZipWith, safeDecodeUtf8)
|
|
import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist)
|
|
import System.FilePath (takeDirectory, takeFileName, (</>))
|
|
|
|
-- * SQLite Store implementation
|
|
|
|
createDBStore :: DBOpts -> [Migration] -> MigrationConfig -> IO (Either MigrationError DBStore)
|
|
createDBStore opts@DBOpts {dbFilePath} migrations migrationConfig = do
|
|
let dbDir = takeDirectory dbFilePath
|
|
createDirectoryIfMissing True dbDir
|
|
st <- connectSQLiteStore opts
|
|
r <- migrateDBSchema st opts Nothing migrations migrationConfig `onException` closeDBStore st
|
|
case r of
|
|
Right () -> pure $ Right st
|
|
Left e -> closeDBStore st $> Left e
|
|
where
|
|
|
|
migrateDBSchema :: DBStore -> DBOpts -> Maybe Query -> [Migration] -> MigrationConfig -> IO (Either MigrationError ())
|
|
migrateDBSchema st DBOpts {dbFilePath, vacuum} migrationsTable migrations MigrationConfig {confirm, backupPath} =
|
|
let initialize = Migrations.initialize st migrationsTable
|
|
getCurrent = withTransaction st $ Migrations.getCurrentMigrations migrationsTable
|
|
run = Migrations.run st migrationsTable vacuum
|
|
backup = mkBackup <$> backupPath
|
|
mkBackup bp =
|
|
let f = if null bp then dbFilePath else bp </> takeFileName dbFilePath
|
|
in copyFile dbFilePath $ f <> ".bak"
|
|
dbm = DBMigrate {initialize, getCurrent, run, backup}
|
|
in sharedMigrateSchema dbm (dbNew st) migrations confirm
|
|
|
|
connectSQLiteStore :: DBOpts -> IO DBStore
|
|
connectSQLiteStore DBOpts {dbFilePath, dbFunctions, dbKey = key, keepKey, track} = do
|
|
dbNew <- not <$> doesFileExist dbFilePath
|
|
dbConn <- dbBusyLoop $ connectDB dbFilePath dbFunctions key track
|
|
dbConnection <- newMVar dbConn
|
|
dbKey <- newTVarIO $! storeKey key keepKey
|
|
dbClosed <- newTVarIO False
|
|
dbSem <- newTVarIO 0
|
|
pure DBStore {dbFilePath, dbFunctions, dbKey, dbSem, dbConnection, dbNew, dbClosed}
|
|
|
|
connectDB :: FilePath -> [SQLiteFuncDef] -> ScrubbedBytes -> DB.TrackQueries -> IO DB.Connection
|
|
connectDB path functions key track = do
|
|
db <- DB.open path track
|
|
prepare db `onException` DB.close db
|
|
-- _printPragmas db path
|
|
pure db
|
|
where
|
|
prepare db = do
|
|
unless (BA.null key) . SQLite3.exec db' $ "PRAGMA key = " <> keyString key <> ";"
|
|
SQLite3.exec db' . fromQuery $
|
|
[sql|
|
|
PRAGMA busy_timeout = 100;
|
|
PRAGMA foreign_keys = ON;
|
|
-- PRAGMA trusted_schema = OFF;
|
|
PRAGMA secure_delete = ON;
|
|
PRAGMA auto_vacuum = FULL;
|
|
|]
|
|
mapM_ addFunction functions'
|
|
where
|
|
db' = SQL.connectionHandle $ DB.conn db
|
|
functions' = SQLiteFuncDef "simplex_xor_md5_combine" 2 (SQLiteFuncPtr True sqliteXorMd5CombinePtr) : functions
|
|
addFunction SQLiteFuncDef {funcName, argCount, funcPtrs} =
|
|
either (throwIO . userError . show) pure =<< case funcPtrs of
|
|
SQLiteFuncPtr isDet funcPtr -> createStaticFunction db' funcName argCount isDet funcPtr
|
|
SQLiteAggrPtrs stepPtr finalPtr -> createStaticAggregate db' funcName argCount stepPtr finalPtr
|
|
|
|
foreign export ccall "simplex_xor_md5_combine" sqliteXorMd5Combine :: SQLiteFunc
|
|
|
|
foreign import ccall "&simplex_xor_md5_combine" sqliteXorMd5CombinePtr :: FunPtr SQLiteFunc
|
|
|
|
sqliteXorMd5Combine :: SQLiteFunc
|
|
sqliteXorMd5Combine = mkSQLiteFunc $ \cxt args -> do
|
|
idsHash <- SQLite3.funcArgBlob args 0
|
|
rId <- SQLite3.funcArgBlob args 1
|
|
SQLite3.funcResultBlob cxt $ xorMd5Combine idsHash rId
|
|
|
|
xorMd5Combine :: ByteString -> ByteString -> ByteString
|
|
xorMd5Combine idsHash rId = packZipWith xor idsHash $ C.md5Hash rId
|
|
{-# INLINE xorMd5Combine #-}
|
|
|
|
closeDBStore :: DBStore -> IO ()
|
|
closeDBStore st@DBStore {dbClosed} =
|
|
ifM (readTVarIO dbClosed) (putStrLn "closeDBStore: already closed") $
|
|
withConnection st $ \conn -> do
|
|
DB.close conn
|
|
atomically $ writeTVar dbClosed True
|
|
|
|
openSQLiteStore :: DBStore -> ScrubbedBytes -> Bool -> IO ()
|
|
openSQLiteStore st@DBStore {dbClosed} key keepKey =
|
|
ifM (readTVarIO dbClosed) (openSQLiteStore_ st key keepKey) (putStrLn "openSQLiteStore: already opened")
|
|
|
|
openSQLiteStore_ :: DBStore -> ScrubbedBytes -> Bool -> IO ()
|
|
openSQLiteStore_ DBStore {dbConnection, dbFilePath, dbFunctions, dbKey, dbClosed} key keepKey =
|
|
bracketOnError
|
|
(takeMVar dbConnection)
|
|
(tryPutMVar dbConnection)
|
|
$ \DB.Connection {slow, track} -> do
|
|
DB.Connection {conn} <- connectDB dbFilePath dbFunctions key track
|
|
atomically $ do
|
|
writeTVar dbClosed False
|
|
writeTVar dbKey $! storeKey key keepKey
|
|
putMVar dbConnection DB.Connection {conn, slow, track}
|
|
|
|
reopenDBStore :: DBStore -> IO ()
|
|
reopenDBStore st@DBStore {dbKey, dbClosed} =
|
|
ifM (readTVarIO dbClosed) open (putStrLn "reopenDBStore: already opened")
|
|
where
|
|
open =
|
|
readTVarIO dbKey >>= \case
|
|
Just key -> openSQLiteStore_ st key True
|
|
Nothing -> fail "reopenDBStore: no key"
|
|
|
|
keyString :: ScrubbedBytes -> Text
|
|
keyString = sqlString . safeDecodeUtf8 . BA.convert
|
|
|
|
sqlString :: Text -> Text
|
|
sqlString s = quote <> T.replace quote "''" s <> quote
|
|
where
|
|
quote = "'"
|
|
|
|
-- _printPragmas :: DB.Connection -> FilePath -> IO ()
|
|
-- _printPragmas db path = do
|
|
-- foreign_keys <- DB.query_ db "PRAGMA foreign_keys;" :: IO [[Int]]
|
|
-- print $ path <> " foreign_keys: " <> show foreign_keys
|
|
-- -- when run via sqlite-simple query for trusted_schema seems to return empty list
|
|
-- trusted_schema <- DB.query_ db "PRAGMA trusted_schema;" :: IO [[Int]]
|
|
-- print $ path <> " trusted_schema: " <> show trusted_schema
|
|
-- secure_delete <- DB.query_ db "PRAGMA secure_delete;" :: IO [[Int]]
|
|
-- print $ path <> " secure_delete: " <> show secure_delete
|
|
-- auto_vacuum <- DB.query_ db "PRAGMA auto_vacuum;" :: IO [[Int]]
|
|
-- print $ path <> " auto_vacuum: " <> show auto_vacuum
|
|
|
|
execSQL :: DB.Connection -> Text -> IO [Text]
|
|
execSQL db query = do
|
|
rs <- newIORef []
|
|
SQLite3.execWithCallback (SQL.connectionHandle $ DB.conn db) query (addSQLResultRow rs)
|
|
reverse <$> readIORef rs
|
|
|
|
addSQLResultRow :: IORef [Text] -> SQLite3.ColumnIndex -> [Text] -> [Maybe Text] -> IO ()
|
|
addSQLResultRow rs _count names values = modifyIORef' rs $ \case
|
|
[] -> [showValues values, T.intercalate "|" names]
|
|
rs' -> showValues values : rs'
|
|
where
|
|
showValues = T.intercalate "|" . map (fromMaybe "")
|