packages feed

ron-rdt-0.11: lib/RON/Store/FS.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

module RON.Store.FS (
    module RON.Store,
    Handle,
    Store,
    debugDump,
    fetchUpdates,
    newHandle,
    newHandleWithReplica,
    runStore,
) where

import RON.Prelude

import Control.Concurrent (MVar, newMVar, withMVar)
import Control.Concurrent.STM (
    TChan,
    atomically,
    dupTChan,
    newBroadcastTChanIO,
    writeTChan,
 )
import Control.Exception (throwIO)
import Data.Bits (shiftL)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.ByteString.Lazy.Char8 qualified as BSLC
import Data.Foldable (find)
import Data.IORef (newIORef)
import Network.Info (MAC (MAC), getNetworkInterfaces, mac)
import System.Directory (
    createDirectoryIfMissing,
    doesDirectoryExist,
    listDirectory,
    makeAbsolute,
 )
import System.FileLock (SharedExclusive (Exclusive), tryLockFile)
import System.FilePath ((</>))
import System.Random.TF (newTFGen)
import System.Random.TF.Instances (random)

import RON.Data.VersionVector (VV, (·≼))
import RON.Epoch (EpochClock, getCurrentEpochTime, runEpochClock)
import RON.Error (
    Error (..),
    MonadE,
    errorContext,
    liftEitherString,
    throwErrorText,
    tryIO,
 )
import RON.Event (
    OriginVariety (ApplicationSpecific),
    Replica,
    ReplicaClock,
    getEventUuid,
    mkReplica,
 )
import RON.Store (MonadStore (..))
import RON.Text.Parse (parseOpenFrame)
import RON.Text.Serialize.Experimental (serializeOpenFrame)
import RON.Types (Op (..), UUID)
import RON.Types.Experimental (Patch (..))
import RON.UUID qualified as UUID
import RON.Util.Word (Word60, leastSignificant60)

-- | Store handle (uses the “Handle pattern”).
data Handle = Handle
    { clock :: IORef Word60
    , dataDir :: FilePath
    , onObjectChanged :: TChan UUID
    {- ^ A channel of changes in the database.
    This is a broadcast channel, so you MUST NOT read from it directly,
    call 'fetchUpdates' to read from derived channel instead.
    -}
    , opLock :: MVar ()
    , replica :: Replica
    }

newtype Store a = Store (ExceptT Error (ReaderT Handle EpochClock) a)
    deriving
        (Applicative, Functor, Monad, MonadError Error, MonadIO, ReplicaClock)

instance MonadStore Store where
    listObjects =
        errorContext "Store.listObjects" do
            Handle{dataDir} <- Store ask
            objectDirs <-
                tryIO do
                    exists <- doesDirectoryExist dataDir
                    if exists then listDirectoryDirs dataDir else pure []
            traverse uuidFromFileName objectDirs

    appendPatch = appendPatchFS

    loadWholeObjectLog = loadWholeObjectLogFS

askObjectLogsDir :: (MonadReader Handle m) => UUID -> m FilePath
askObjectLogsDir objectId = do
    Handle{dataDir} <- ask
    pure $ dataDir </> uuidToFileName objectId </> "log"

getObjectPatches :: UUID -> Store [FilePath]
getObjectPatches objectId = do
    objectLogsDir <- Store $ askObjectLogsDir objectId
    objectExists <- tryIO $ doesDirectoryExist objectLogsDir
    if objectExists then tryIO $ listDirectory objectLogsDir else pure []

loadWholeObjectLogFS :: UUID -> VV -> Store [Op]
loadWholeObjectLogFS objectId version = do
    objectLogsDir <- Store $ askObjectLogsDir objectId
    patchNames <- getObjectPatches objectId
    fold . catMaybes <$> for patchNames \patchName -> do
        patchTimestamp <- uuidFromFileName patchName
        if patchTimestamp ·≼ version then
            pure Nothing
        else do
            let patchFile = objectLogsDir </> patchName
            patchContent <- tryIO $ BS.readFile patchFile
            patch <-
                liftEitherString $ parseOpenFrame $ BSL.fromStrict patchContent
            pure $ Just patch

appendPatchFS :: Patch -> Store ()
appendPatchFS Patch{object, log} = do
    Handle{dataDir, onObjectChanged} <- Store ask
    let objectLogsDir = dataDir </> uuidToFileName object </> "log"
    tryIO $ createDirectoryIfMissing True objectLogsDir
    patchVersion <- getEventUuid
    let patchFile = objectLogsDir </> uuidToFileName patchVersion
    tryIO $ BSL.writeFile patchFile $ serializeOpenFrame $ toList log
    tryIO $ atomically $ writeTChan onObjectChanged object

-- | Run a 'Store' action
runStore :: Handle -> Store a -> IO a
runStore h@Handle{replica, clock, opLock} (Store action) = do
    res <-
        withMVar opLock \_ ->
            runEpochClock replica clock $ (`runReaderT` h) $ runExceptT action
    either throwIO pure res

{- | Create new storage handle.
Uses MAC address for replica id or generates a random one.
-}
newHandle :: FilePath -> IO (Maybe Handle)
newHandle dataDir = do
    macAddress <- getMacAddress
    replicaId <-
        case macAddress of
            Just macAddress' -> pure macAddress'
            Nothing -> fst . random <$> newTFGen
    newHandleWithReplica dataDir $ leastSignificant60 replicaId

newHandleWithReplica :: FilePath -> Word60 -> IO (Maybe Handle)
newHandleWithReplica dataDir' replicaId = do
    dataDir <- makeAbsolute dataDir'
    let appLockFile = dataDir </> "applock"
    mLock <- tryLockFile appLockFile Exclusive
    case mLock of
        Nothing -> pure Nothing
        Just _ -> do
            time <- getCurrentEpochTime
            clock <- newIORef time
            onObjectChanged <- newBroadcastTChanIO
            opLock <- newMVar ()
            let replica = mkReplica ApplicationSpecific replicaId
            pure $ Just Handle{..}

getMacAddress :: IO (Maybe Word64)
getMacAddress =
    do
        macAddress <- getMac
        pure $ decodeMac <$> macAddress
  where
    getMac = find (/= minBound) . map mac <$> getNetworkInterfaces
    decodeMac (MAC b5 b4 b3 b2 b1 b0) =
        (fromIntegral b5 `shiftL` 40)
            + (fromIntegral b4 `shiftL` 32)
            + (fromIntegral b3 `shiftL` 24)
            + (fromIntegral b2 `shiftL` 16)
            + (fromIntegral b1 `shiftL` 8)
            + fromIntegral b0

uuidFromFileName :: (MonadE m) => FilePath -> m UUID
uuidFromFileName name =
    maybe
        ( throwErrorText $
            "UUID.decodeBase32: file name "
                <> show name
                <> " is not a valid UUID"
        )
        pure
        (UUID.decodeBase32 name)

uuidToFileName :: UUID -> FilePath
uuidToFileName = UUID.encodeBase32

debugDump :: FilePath -> IO ()
debugDump dataDir = do
    objectDirs <- do
        exists <- doesDirectoryExist dataDir
        if exists then listDirectory dataDir else pure []
    for_ (sort objectDirs) \objectDir -> do
        isDir <- doesDirectoryExist $ dataDir </> objectDir
        when isDir do
            let logsDir = dataDir </> objectDir </> "log"
            logs <- listDirectory logsDir
            for_ (sort logs) \logName -> do
                let logPath = logsDir </> logName
                BSL.putStr =<< BSL.readFile logPath
            BSLC.putStrLn ""

fetchUpdates :: Handle -> IO (TChan UUID)
fetchUpdates Handle{onObjectChanged} = atomically $ dupTChan onObjectChanged

listDirectoryDirs :: FilePath -> IO [FilePath]
listDirectoryDirs dir =
    listDirectory dir >>= filterM (\name -> doesDirectoryExist (dir </> name))