moto-0.0.3: lib/Moto/Registry.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
-- | This module exports tools for implementing a registry that @moto@ can use
-- in order to keep track of the migrations that have been run so far.
--
-- It's unlikely that you'll need to concern yourself with this module as an
-- end user of @moto@.
--
-- Please import as:
--
-- @
-- import qualified "Moto.Registry" as Moto
-- @
module Moto.Registry
( -- * Command-line support
IC.RegistryConf(..)
-- * Registry
, I.Registry(..)
, newAppendOnlyRegistry
-- * State
, I.State
, I.emptyState
, I.updateState
, I.Log(..)
-- * Errors
, Err_Tainted(..)
, I.Err_Prepare(..)
, I.Err_Abort(..)
, I.Err_Commit(..)
, I.Err_UpdateState(..)
) where
import Control.Concurrent (readMVar, putMVar, takeMVar, newMVar)
import qualified Control.Exception.Safe as Ex
import qualified Data.Time as Time
import qualified Moto.Internal as I
import qualified Moto.Internal.Cli as IC
--------------------------------------------------------------------------------
-- | Create a 'I.Registry' backed by an append-only 'I.Log' storage.
--
-- This registry maintains its internal 'I.State' in memory as long as it is
-- possible to successfuly store all the changes in the underlying append-only
-- storage. If at some point this fails unrecoverably, then 'Err_Tainted' will
-- be thrown by the functions acting on this 'I.Registry'.
--
-- It's important to acquire some kind of exclusive lock on the underlying
-- storage, so that other applications can't poke it while our 'I.Registry' is
-- running.
newAppendOnlyRegistry
:: I.State
-- ^ Initial registry state obtained by reading 'I.Log's from the backing
-- append-only storage and running 'I.updateState' on them.
-> (I.Log -> IO ())
-- ^ How to store a newly generated 'I.Log' in the backing append-only
-- storage.
--
-- If this function throws an exception, then the execption will propagated
-- as usual, but also, this registry will be marked as tained and each
-- subsequent operation on it will throw 'Err_Tainted'.
-> IO I.Registry
newAppendOnlyRegistry !state0 putLog = do
mvState <- newMVar (Just state0)
let supdate :: (I.State -> Either e I.Log) -> IO (Either e I.Log)
supdate f = Ex.bracketOnError
(takeMVar mvState)
(\_ -> putMVar mvState Nothing)
(\case Nothing -> Ex.throwM Err_Tainted
Just s0 -> case f s0 of
Left e -> pure (Left e)
Right log_ -> case I.updateState s0 log_ of
Left e -> Ex.throwM e -- This is unreachable code.
Right !s1 -> do
putLog log_
putMVar mvState (Just s1)
pure (Right log_))
pure $ I.Registry
{ I.registry_state = \_ -> do
maybe (Ex.throwM Err_Tainted) pure =<< readMVar mvState
, I.registry_prepare = \_ mId d -> do
t <- Time.getCurrentTime
supdate $ \s -> case I.state_status s of
I.Dirty mId' d' -> Left (I.Err_Prepare_Dirty mId' d')
I.Clean -> case (d, elem mId (map fst (I.state_committed s))) of
(I.Forwards, True) -> Left (I.Err_Prepare_Duplicate mId)
(I.Backwards, False) -> Left (I.Err_Prepare_NotFound mId)
_ -> Right (I.Log_Prepare t mId d)
, I.registry_abort = \_ mId d -> do
t <- Time.getCurrentTime
supdate $ \s -> case I.state_status s of
I.Clean -> Left I.Err_Abort_Clean
I.Dirty mId' d'
| mId /= mId' || d /= d' -> Left (I.Err_Abort_Dirty mId' d')
| otherwise -> Right (I.Log_Abort t)
, I.registry_commit = \_ mId d -> do
t <- Time.getCurrentTime
supdate $ \s -> case I.state_status s of
I.Clean -> Left I.Err_Commit_Clean
I.Dirty mId' d'
| mId /= mId' || d /= d' -> Left (I.Err_Commit_Dirty mId' d')
| otherwise -> Right (I.Log_Commit t)
}
-- | The 'I.Registry' is tainted, meaning our last attempt to interact with the
-- registry's backing storage failed. We can't be certain about the current
-- state of the 'I.Registry'.
data Err_Tainted = Err_Tainted deriving (Eq, Show)
instance Ex.Exception Err_Tainted