packages feed

delta-store-1.0.0.0: src/Data/DBVar.hs

{-# OPTIONS_GHC -Wno-redundant-constraints#-}
-- We intentionally specify more constraints than necessary for some exports.
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

{-|
Copyright   : © 2022-2023 IOHK, 2023-2025 Cardano Foundation
License     : Apache-2.0
Description : Mutable variable that mirrors its value in a 'Data.Store.Store'.

'DBVar' represents a mutable variable that stores one Haskell value
in volatile memory (RAM),
but mirrors the value to persistent storage,
for example to a database on disk.

* 'Store' represents the storage facility to which the variable is mirrored.
* Read-access is from volatile memory and highly concurrent.
* Updates are incremental and use delta types, see "Data.Delta".
-}
module Data.DBVar (
      DBVar
    , readDBVar
    , updateDBVar
    , modifyDBVar
    , modifyDBMaybe
    , initDBVar
    , loadDBVar
    ) where

import Prelude

import Control.Concurrent.Class.MonadSTM
    ( MonadSTM
    , atomically
    , newTVarIO
    , readTVar
    , readTVarIO
    , retry
    , writeTVar
    )
import Control.Monad.Class.MonadThrow
    ( MonadEvaluate
    , MonadMask
    , MonadThrow
    , bracket
    , evaluate
    , mask
    , throwIO
    )
import Data.Delta
    ( Delta (..)
    )
import Data.Store
    ( Store (..)
    , UpdateStore
    )

{-------------------------------------------------------------------------------
    DBVar
-------------------------------------------------------------------------------}
-- | A 'DBVar'@ m delta@ is a mutable reference to a Haskell value of type @a@.
-- The type @delta@ is a delta type for this value type @a@,
-- that is we have @a ~ @'Base'@ delta@.
--
-- The Haskell value is cached in memory, in weak head normal form (WHNF).
-- However, whenever the value is updated, a copy of the value will be written
-- to persistent storage like a file or database on disk;
-- the specific storage facility is represented by a 'Store'.
-- For efficient updates, the delta type @delta@ is used in the update.
--
-- Concurrency: 'DBVar' fully supports concurrent reads and updates.
--
-- * Updates are atomic and will block other updates.
-- * Reads will /not/ be blocked during an update
--   (except for a small moment where the new value atomically
--    replaces the old one).
data DBVar m delta = DBVar
    { readDBVar_     :: m (Base delta)
    , modifyDBMaybe_ :: forall b. (Base delta -> (Maybe delta, b)) -> m b
    }

-- | Read the current value of the 'DBVar'.
readDBVar :: (Delta da, a ~ Base da) => DBVar m da -> m a
readDBVar = readDBVar_

-- | Update the value of the 'DBVar' using a delta type.
--
-- The new value will be evaluated to weak head normal form.
updateDBVar :: (Delta da, Monad m) => DBVar m da -> da -> m ()
updateDBVar var delta = modifyDBMaybe var $ \_ -> (Just delta,())

-- | Modify the value in a 'DBVar'.
--
-- The new value will be evaluated to weak head normal form (WHNF).
modifyDBVar
    :: (Delta da, Monad m, a ~ Base da)
    => DBVar m da -> (a -> (da, b)) -> m b
modifyDBVar var f = modifyDBMaybe var $ \a -> let (da,b) = f a in (Just da,b)

-- | Maybe modify the value in a 'DBVar'
--
-- If updated,
-- the new value will be evaluated to weak head normal form (WHNF).
modifyDBMaybe
    :: (Delta da, Monad m, a ~ Base da)
    => DBVar m da -> forall b. (a -> (Maybe da, b)) -> m b
modifyDBMaybe = modifyDBMaybe_

-- | Initialize a new 'DBVar' that mirrors to a given 'Store'.
initDBVar
    ::  ( MonadSTM m, MonadThrow m, MonadEvaluate m, MonadMask m
        , Delta da, a ~ Base da
        )
    => UpdateStore m da -- ^ 'Store' for mirroring.
    -> a -- ^ Initial value.
    -> m (DBVar m da)
initDBVar store v = do
    writeS store v
    newWithCache (updateS store . Just) v

-- | Create a 'DBVar' that mirrors to a given 'Store',
-- and also loads its initial value from there.
-- Throws an exception if the value cannot be loaded.
loadDBVar
    ::  ( MonadSTM m, MonadThrow m, MonadEvaluate m, MonadMask m
        , Delta da
        )
    => UpdateStore m da -- ^ 'Store' for mirroring and for reading the initial value.
    -> m (DBVar m da)
loadDBVar store =
    loadS store >>= \case
        Left  e -> throwIO e
        Right a -> newWithCache (updateS store . Just) a

-- | Create 'DBVar' from an initial value and an update function
-- using a 'TVar' as in-memory cache.
--
-- Space: The value in the 'TVar' will be evaluated to weak head normal form.
--
-- Concurrency: The update function needs to be atomic even in the presence
-- of asynchronous exceptions.
newWithCache
    ::  ( MonadSTM m, MonadThrow m, MonadMask m, MonadEvaluate m
        , Delta da, a ~ Base da
        )
    => (a -> da -> m ()) -> a -> m (DBVar m da)
newWithCache update a = do
    cache  <- newTVarIO a
    locked <- newTVarIO False   -- lock for updating the cache
    pure $ DBVar
        { readDBVar_     = readTVarIO cache
        , modifyDBMaybe_ = \f -> do
            let before = atomically $ do
                    readTVar locked >>= \case
                        True  -> retry
                        False -> do
                            writeTVar locked True
                            readTVar cache
                after _ = atomically $ writeTVar locked False
                action old = do
                    let (mdelta, b) = f old
                    case mdelta of
                        Nothing    -> pure ()
                        Just delta -> do
                            new <- evaluate $ apply delta old
                            mask $ \restore -> do
                                -- We mask asynchronous exceptions here
                                -- to ensure that the TVar will be updated
                                -- whenever @update@ succeeds without exception.
                                restore $ update old delta
                                atomically $ writeTVar cache new
                    pure b
            bracket before after action
        }