lsm-tree-1.0.0.2: test/Database/LSMTree/Model/Session.hs
{-# LANGUAGE PatternSynonyms #-}
-- | A pure model of a single session containing multiple tables.
--
-- The session model builds on top of "Database.LSMTree.Model.Table", adding
-- table and snapshot administration.
module Database.LSMTree.Model.Session (
-- * Model
Model (..)
, initModel
, UpdateCounter (..)
-- ** SomeTable, for testing
, SomeTable (..)
, toSomeTable
, fromSomeTable
, withSomeTable
, TableID
, tableID
, isUnionDescendant
, Model.size
-- ** Constraints
, C
, C_
, Model.SerialiseKey (..)
, Model.SerialiseValue (..)
-- ** ModelT and ModelM
, ModelT (..)
, runModelT
, ModelM
, runModelM
, runModelMWithInjectedErrors
-- ** Errors
, Err (..)
, isDiskFault
, isSnapshotCorrupted
, isOther
-- * Tables
, Table
, TableConfig (..)
, new
, close
-- * Monoidal value resolution
, ResolveSerialisedValue (..)
, getResolve
, noResolve
-- * Table querying and updates
-- ** Queries
, Range (..)
, LookupResult (..)
, lookups
, Entry (..)
, rangeLookup
-- ** Cursor
, Cursor
, CursorID
, cursorID
, newCursor
, closeCursor
, readCursor
-- ** Updates
, Update (..)
, updates
, inserts
, deletes
, upserts
-- ** Blobs
, BlobRef
, retrieveBlobs
, invalidateBlobRefs
-- * Snapshots
, SnapshotName
, saveSnapshot
, openTableFromSnapshot
, corruptSnapshot
, deleteSnapshot
, listSnapshots
-- * Multiple writable tables
, duplicate
-- * Table union
, IsUnionDescendant (..)
, union
, unions
, UnionDebt (..)
, remainingUnionDebt
, UnionCredits (..)
, supplyUnionCredits
, supplyPortionOfDebt
) where
import Control.Monad (forM, when)
import Control.Monad.Except (ExceptT (..), MonadError (..),
runExceptT)
import Control.Monad.Identity (Identity (runIdentity))
import Control.Monad.State.Strict (MonadState (..), StateT (..), gets,
modify)
import Data.Data
import Data.Dynamic
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromJust)
import qualified Data.Vector as V
import Data.Word
import Database.LSMTree (SerialiseKey (..), SerialiseValue (..),
SnapshotLabel (..), SnapshotName, UnionCredits (..),
UnionDebt (..))
import Database.LSMTree.Model.Table (Entry (..), LookupResult (..),
Range (..), ResolveSerialisedValue (..), Update (..),
getResolve, noResolve)
import qualified Database.LSMTree.Model.Table as Model
{-------------------------------------------------------------------------------
Model
-------------------------------------------------------------------------------}
data Model = Model {
tables :: Map TableID (UpdateCounter, SomeTable)
, cursors :: Map CursorID SomeCursor
, nextID :: Int
, snapshots :: Map SnapshotName Snapshot
}
deriving stock Show
initModel :: Model
initModel = Model {
tables = Map.empty
, cursors = Map.empty
, nextID = 0
, snapshots = Map.empty
}
-- | We conservatively model blob reference invalidation: each update after
-- acquiring a blob reference will invalidate it. We use 'UpdateCounter' to
-- track updates.
--
-- Supplying union credits is also considered an update, though this can only
-- invalidate a blob reference that is associated with a (descendant of a) union
-- table.
newtype UpdateCounter = UpdateCounter Word64
deriving stock (Show, Eq, Ord)
deriving newtype (Num)
data SomeTable where
SomeTable :: (Typeable k, Typeable v, Typeable b)
=> Model.Table k v b -> SomeTable
instance Show SomeTable where
show (SomeTable table) = show table
toSomeTable ::
(Typeable k, Typeable v, Typeable b)
=> Model.Table k v b
-> SomeTable
toSomeTable = SomeTable
fromSomeTable ::
(Typeable k, Typeable v, Typeable b)
=> SomeTable
-> Maybe (Model.Table k v b)
fromSomeTable (SomeTable tbl) = cast tbl
withSomeTable ::
(forall k v b. (Typeable k, Typeable v, Typeable b)
=> Model.Table k v b -> a)
-> SomeTable
-> a
withSomeTable f (SomeTable tbl) = f tbl
newtype SomeCursor = SomeCursor Dynamic
instance Show SomeCursor where
show (SomeCursor c) = show c
toSomeCursor ::
(Typeable k, Typeable v, Typeable b)
=> Model.Cursor k v b
-> SomeCursor
toSomeCursor = SomeCursor . toDyn
fromSomeCursor ::
(Typeable k, Typeable v, Typeable b)
=> SomeCursor
-> Maybe (Model.Cursor k v b)
fromSomeCursor (SomeCursor c) = fromDynamic c
--
-- Constraints
--
-- | Common constraints for keys, values and blobs
type C_ a = (Show a, Eq a, Typeable a)
type C k v b = (C_ k, C_ v, C_ b)
--
-- ModelT and ModelM
--
newtype ModelT m a = ModelT { _runModelT :: ExceptT Err (StateT Model m) a }
deriving stock Functor
deriving newtype ( Applicative
, Monad
, MonadState Model
, MonadError Err
)
runModelT :: ModelT m a -> Model -> m (Either Err a, Model)
runModelT = runStateT . runExceptT . _runModelT
type ModelM = ModelT Identity
runModelM :: ModelM a -> Model -> (Either Err a, Model)
runModelM m = runIdentity . runModelT m
-- | @'runModelMWithInjectedErrors' merrs onNoErrors onErrors@ runs the model
-- with injected disk faults @merrs@.
--
-- The model may run different actions based on whether there are injections or
-- not. 'onNoErrors' runs in case @merrs == Nothing@, and 'onErrors@ runs in
-- case @merrs == Just _@.
--
-- Typically, @onNoErrors@ will be a model function like 'lookups', and
-- @onErrors@ should be the identity state transition. This models the approach
-- of the @lsm-tree@ library: the *logical* state of the database should remain
-- unchanged if there were any disk faults.
--
-- The model functions in the remainder of the module only describe the happy
-- path with respect to disk faults: they assume there aren't any such faults.
-- The intent of 'runModelMWithInjectedErrors' is to augment the model with
-- responses to disk fault injection.
--
-- The real system's is not guaranteed to fail with an error if there are
-- injected disk faults. However, the approach taken in
-- 'runModelMWithInjectedErrors' is to *always* throw a modelled disk error in
-- case there are injections, *even if* the real system happened to not fail
-- completely. This makes 'runModelMWithInjectedErrors' an approximation of the
-- real system's behaviour in case of injections. For the model to more
-- accurately model the system's behaviour, the model would have to know for
-- each API function precisely which errors are injected in which order, and
-- whether they are handled internally by the library or not. This is simply
-- infeasible: the model would become very complex.
runModelMWithInjectedErrors ::
Maybe e -- ^ The errors that are injected
-> ModelM a -- ^ Action to run on 'Nothing'
-> ModelM () -- ^ Action to run on 'Just'
-> Model -> (Either Err a, Model)
runModelMWithInjectedErrors Nothing onNoErrors _ st =
runModelM onNoErrors st
runModelMWithInjectedErrors (Just _) _ onErrors st =
runModelM (onErrors >> throwError ModelErrDiskFault) st
-- | The default 'ErrDiskFault' that model operations will throw.
pattern ModelErrDiskFault :: Err
pattern ModelErrDiskFault = ErrDiskFault "model does not produce an error message"
-----
-- Errors
--
data Err
= ErrSessionDirDoesNotExist
| ErrSessionDirLocked
| ErrSessionDirCorrupted
| ErrSessionClosed
| ErrTableClosed
| ErrTableCorrupted
| ErrTableUnionHandleTypeMismatch
| ErrTableUnionSessionMismatch
| ErrSnapshotExists !SnapshotName
| ErrSnapshotDoesNotExist !SnapshotName
| ErrSnapshotCorrupted !SnapshotName
| ErrSnapshotWrongLabel !SnapshotName
| ErrBlobRefInvalid
| ErrCursorClosed
| ErrDiskFault !String
| ErrFsError !String
| ErrCommitActionRegistry !(NonEmpty Err)
| ErrAbortActionRegistry !(Maybe Err) !(NonEmpty Err)
| ErrOther !String
deriving stock (Eq, Show)
isSnapshotCorrupted :: Err -> Bool
isSnapshotCorrupted (ErrSnapshotCorrupted _) = True
isSnapshotCorrupted (ErrFsError _) = True
isSnapshotCorrupted (ErrCommitActionRegistry es) = firstNotOtherSatisfies isSnapshotCorrupted es
isSnapshotCorrupted (ErrAbortActionRegistry e es) = firstNotOtherSatisfies isSnapshotCorrupted (maybe id NE.cons e es)
isSnapshotCorrupted _ = False
isDiskFault :: Err -> Bool
isDiskFault (ErrDiskFault _) = True
isDiskFault (ErrFsError _) = True
isDiskFault (ErrCommitActionRegistry es) = firstNotOtherSatisfies isDiskFault es
isDiskFault (ErrAbortActionRegistry e es) = firstNotOtherSatisfies isDiskFault (maybe id NE.cons e es)
isDiskFault _ = False
isOther :: Err -> Bool
isOther (ErrOther _) = True
isOther (ErrCommitActionRegistry es) = all isOther es
isOther (ErrAbortActionRegistry e es) = all isOther (maybe id NE.cons e es)
isOther _ = False
firstNotOtherSatisfies :: (Err -> Bool) -> NonEmpty Err -> Bool
firstNotOtherSatisfies p =
maybe False (p . fst) . L.uncons . NE.dropWhile isOther
{-------------------------------------------------------------------------------
Tables
-------------------------------------------------------------------------------}
type TableID = Int
--
-- API
--
data Table k v b = Table {
tableID :: TableID
, config :: TableConfig
, isUnionDescendant :: IsUnionDescendant
}
deriving stock Show
-- | The model does not distinguish multiple values of the config type. This is
-- the right thing because the table config is never observed directly, only
-- via the influence it might have on other operation results. And we /do not
-- want/ table configs to have observable value-level effects. The table config
-- should only have performance effects. Thus having only one config value is
-- the right abstraction for the model.
--
data TableConfig = TableConfig
deriving stock (Show, Eq)
new ::
forall k v b m. (MonadState Model m, C k v b)
=> TableConfig
-> m (Table k v b)
new config = newTableWith config IsNotUnionDescendant Model.empty
-- |
--
-- This is idempotent.
close :: MonadState Model m => Table k v b -> m ()
close Table{..} = state $ \Model{..} ->
let tables' = Map.delete tableID tables
model' = Model {
tables = tables'
, ..
}
in ((), model')
--
-- Utility
--
guardTableIsOpen ::
forall k v b m. (
MonadState Model m, MonadError Err m
, Typeable k, Typeable v, Typeable b
)
=> Table k v b
-> m (UpdateCounter, Model.Table k v b)
guardTableIsOpen Table{..} =
gets (Map.lookup tableID . tables) >>= \case
Nothing ->
throwError ErrTableClosed
Just (updc, tbl) ->
pure (updc, fromJust $ fromSomeTable tbl)
newTableWith ::
(MonadState Model m, C k v b)
=> TableConfig
-> IsUnionDescendant
-> Model.Table k v b
-> m (Table k v b)
newTableWith config isUnionDescendant tbl = state $ \Model{..} ->
let table = Table {
tableID = nextID
, config
, isUnionDescendant
}
someTable = toSomeTable tbl
tables' = Map.insert nextID (0, someTable) tables
nextID' = nextID + 1
model' = Model {
tables = tables'
, nextID = nextID'
, ..
}
in (table, model')
{-------------------------------------------------------------------------------
Lookups
-------------------------------------------------------------------------------}
lookups ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, C k v b
)
=> V.Vector k
-> Table k v b
-> m (V.Vector (Model.LookupResult v (BlobRef b)))
lookups ks t = do
(updc, table) <- guardTableIsOpen t
pure $ liftBlobRefs (SomeTableID updc (tableID t)) $ Model.lookups ks table
rangeLookup ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, C k v b
)
=> Range k
-> Table k v b
-> m (V.Vector (Model.Entry k v (BlobRef b)))
rangeLookup r t = do
(updc, table) <- guardTableIsOpen t
pure $ liftBlobRefs (SomeTableID updc (tableID t)) $ Model.rangeLookup r table
{-------------------------------------------------------------------------------
Updates
-------------------------------------------------------------------------------}
updates ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, SerialiseValue b
, C k v b
)
=> ResolveSerialisedValue v
-> V.Vector (k, Model.Update v b)
-> Table k v b
-> m ()
updates r ups t@Table{..} = do
(updc, table) <- guardTableIsOpen t
let table' = Model.updates r ups table
modify (\m -> m {
tables = Map.insert tableID (updc + 1, toSomeTable table') (tables m)
})
inserts ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, SerialiseValue b
, C k v b
)
=> ResolveSerialisedValue v
-> V.Vector (k, v, Maybe b)
-> Table k v b
-> m ()
inserts r = updates r . fmap (\(k, v, blob) -> (k, Model.Insert v blob))
deletes ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, SerialiseValue b
, C k v b
)
=> ResolveSerialisedValue v
-> V.Vector k
-> Table k v b
-> m ()
deletes r = updates r . fmap (,Model.Delete)
upserts ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, SerialiseValue b
, C k v b
)
=> ResolveSerialisedValue v
-> V.Vector (k, v)
-> Table k v b
-> m ()
upserts r = updates r . fmap (fmap Model.Upsert)
{-------------------------------------------------------------------------------
Blobs
-------------------------------------------------------------------------------}
-- | For more details: 'Database.LSMTree.Internal.BlobRef' describes the
-- intended semantics of blob references.
data BlobRef b = BlobRef {
handleRef :: !(SomeHandleID b)
, innerBlob :: !(Model.BlobRef b)
}
deriving stock instance Show b => Show (BlobRef b)
retrieveBlobs ::
forall m b. ( MonadState Model m
, MonadError Err m
, SerialiseValue b
)
=> V.Vector (BlobRef b)
-> m (V.Vector b)
retrieveBlobs refs = Model.retrieveBlobs <$> V.mapM guard refs
where
guard BlobRef{..} = do
m <- get
-- In the real implementation, a blob reference /could/ be invalidated
-- every time you modify a table or cursor. This model takes the most
-- conservative approach: a blob reference is immediately invalidated
-- every time a table/cursor is modified.
case handleRef of
SomeTableID createdAt tableID ->
case Map.lookup tableID (tables m) of
-- If the table is now closed, it means the table has been modified.
Nothing -> errInvalid
-- If the table is open, we check timestamps (i.e., UpdateCounter)
-- to see if any modifications were made.
Just (updc, _) -> do
when (updc /= createdAt) $ errInvalid
pure innerBlob
SomeCursorID cursorID ->
-- The only modification to a cursor is that it can be closed.
case Map.lookup cursorID (cursors m) of
Nothing -> errInvalid
Just _ -> pure innerBlob
errInvalid :: m a
errInvalid = throwError ErrBlobRefInvalid
data SomeHandleID b where
SomeTableID :: !UpdateCounter -> !TableID -> SomeHandleID b
SomeCursorID :: !CursorID -> SomeHandleID b
deriving stock Show
liftBlobRefs ::
(Functor f, Functor g)
=> SomeHandleID b
-> g (f (Model.BlobRef b))
-> g (f (BlobRef b))
liftBlobRefs hid = fmap (fmap (BlobRef hid))
-- | Invalidate blob references that were created from the given table. This
-- function assumes that it is called on an open table.
--
-- This is useful in tests where blob references should be invalidated for other
-- reasons than normal operation of the model.
invalidateBlobRefs ::
MonadState Model m
=> Table k v b
-> m ()
invalidateBlobRefs Table{..} = do
gets (Map.lookup tableID . tables) >>= \case
Nothing -> error "invalidateBlobRefs: table is closed!"
Just (updc, tbl) -> do
modify (\m -> m {
tables = Map.insert tableID (updc + 1, tbl) (tables m)
})
{-------------------------------------------------------------------------------
Snapshots
-------------------------------------------------------------------------------}
data Snapshot = Snapshot
{ snapshotConfig :: TableConfig
, snapshotLabel :: SnapshotLabel
, snapshotTable :: SomeTable
, snapshotIsUnionDescendant :: IsUnionDescendant
, snapshotCorrupted :: Bool
}
deriving stock Show
saveSnapshot ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> SnapshotName
-> SnapshotLabel
-> Table k v b
-> m ()
saveSnapshot name label t@Table{..} = do
(_updc, table) <- guardTableIsOpen t
snaps <- gets snapshots
when (Map.member name snaps) $
throwError $ ErrSnapshotExists name
let snap =
Snapshot
config label
(toSomeTable $ Model.snapshot table)
isUnionDescendant False
modify (\m -> m {
snapshots = Map.insert name snap (snapshots m)
})
openTableFromSnapshot ::
forall k v b m. (
MonadState Model m
, MonadError Err m
, C k v b
)
=> SnapshotName
-> SnapshotLabel
-> m (Table k v b)
openTableFromSnapshot name label = do
snaps <- gets snapshots
case Map.lookup name snaps of
Nothing ->
throwError $ ErrSnapshotDoesNotExist name
Just (Snapshot conf label' tbl snapshotIsUnion corrupted) -> do
when corrupted $
throwError $ ErrSnapshotCorrupted name
when (label /= label') $
throwError $ ErrSnapshotWrongLabel name
case fromSomeTable tbl of
Nothing ->
-- The label should contain enough information to type snapshots,
-- but it's up to the user to pick labels. If the labels are picked
-- badly, then the SUT would fail with any number of and type of
-- errors. The model simply does not allow this to occur: if we fail
-- to cast modelled tables, then we consider it to be a bug in the
-- test setup, and so we use @error@ instead of @throwError@.
error "openTableFromSnapshot: snapshot opened at wrong type"
Just table' ->
newTableWith conf snapshotIsUnion table'
-- To match the implementation of the real table, this should not corrupt the
-- snapshot if there are _no non-empty files_; however, since there are no such
-- snapshots, this is probably fine.
corruptSnapshot ::
(MonadState Model m, MonadError Err m)
=> SnapshotName
-> m ()
corruptSnapshot name = do
snapshots <- gets snapshots
if Map.notMember name snapshots
then throwError $ ErrSnapshotDoesNotExist name
else modify $ \m -> m {snapshots = Map.adjust corruptSnapshotEntry name snapshots}
where
corruptSnapshotEntry (Snapshot c l t u _) = Snapshot c l t u True
deleteSnapshot ::
(MonadState Model m, MonadError Err m)
=> SnapshotName
-> m ()
deleteSnapshot name = do
snaps <- gets snapshots
case Map.lookup name snaps of
Nothing ->
throwError $ ErrSnapshotDoesNotExist name
Just _ ->
modify (\m -> m {
snapshots = Map.delete name snaps
})
listSnapshots ::
MonadState Model m
=> m [SnapshotName]
listSnapshots = gets (Map.keys . snapshots)
{-------------------------------------------------------------------------------
Multiple writable tables
-------------------------------------------------------------------------------}
duplicate ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> Table k v b
-> m (Table k v b)
duplicate t@Table{..} = do
table <- snd <$> guardTableIsOpen t
newTableWith config isUnionDescendant $ Model.duplicate table
{-------------------------------------------------------------------------------
Cursor
-------------------------------------------------------------------------------}
type CursorID = Int
data Cursor k v b = Cursor {
cursorID :: !CursorID
}
deriving stock Show
newCursor ::
forall k v b m. (
MonadState Model m, MonadError Err m
, SerialiseKey k
, C k v b
)
=> Maybe k
-> Table k v b
-> m (Cursor k v b)
newCursor offset t = do
table <- snd <$> guardTableIsOpen t
state $ \Model{..} ->
let cursor = Cursor { cursorID = nextID }
someCursor = toSomeCursor $ Model.newCursor offset table
cursors' = Map.insert nextID someCursor cursors
nextID' = nextID + 1
model' = Model {
cursors = cursors'
, nextID = nextID'
, ..
}
in (cursor, model')
closeCursor :: MonadState Model m => Cursor k v b -> m ()
closeCursor Cursor {..} = state $ \Model{..} ->
let cursors' = Map.delete cursorID cursors
model' = Model {
cursors = cursors'
, ..
}
in ((), model')
readCursor ::
( MonadState Model m
, MonadError Err m
, SerialiseKey k
, SerialiseValue v
, C k v b
)
=> Int
-> Cursor k v b
-> m (V.Vector (Model.Entry k v (BlobRef b)))
readCursor n c = do
cursor <- guardCursorIsOpen c
let (qrs, cursor') = Model.readCursor n cursor
modify (\m -> m {
cursors = Map.insert (cursorID c) (toSomeCursor cursor') (cursors m)
})
pure $ liftBlobRefs (SomeCursorID (cursorID c)) $ qrs
guardCursorIsOpen ::
forall k v b m. (
MonadState Model m, MonadError Err m
, Typeable k, Typeable v, Typeable b
)
=> Cursor k v b
-> m (Model.Cursor k v b)
guardCursorIsOpen Cursor{..} =
gets (Map.lookup cursorID . cursors) >>= \case
Nothing ->
throwError ErrCursorClosed
Just c ->
pure (fromJust $ fromSomeCursor c)
{-------------------------------------------------------------------------------
Table union
-------------------------------------------------------------------------------}
-- Is this a (descendant of a) union table?
--
-- This is important for invalidating blob references: if a table is a
-- (descendant of a) union table, then 'supplyUnionCredits' can invalidate blob
-- references.
data IsUnionDescendant = IsUnionDescendant | IsNotUnionDescendant
deriving stock (Show, Eq)
union ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> ResolveSerialisedValue v
-> Table k v b
-> Table k v b
-> m (Table k v b)
union r th1 th2 = do
(_, t1) <- guardTableIsOpen th1
(_, t2) <- guardTableIsOpen th2
newTableWith TableConfig IsUnionDescendant $ Model.union r t1 t2
unions ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> ResolveSerialisedValue v
-> NonEmpty (Table k v b)
-> m (Table k v b)
unions r tables = do
tables' <- forM tables $ \table -> do
(_, table') <- guardTableIsOpen table
pure table'
newTableWith TableConfig IsUnionDescendant $ Model.unions r tables'
-- | The model can not accurately predict union debt without considerable
-- knowledge about the implementation of /real/ tables. Therefore the model
-- considers unions to be finished right away, and the resulting debt will
-- always be 0.
remainingUnionDebt ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> Table k v b
-> m UnionDebt
remainingUnionDebt t = do
(_updc, _table) <- guardTableIsOpen t
pure (UnionDebt 0)
-- | The union debt is always 0, so supplying union credits has no effect on the
-- tables, except for invalidating its blob references in some cases.
--
-- In the /real/ implementation, blob references can be associated with a run in
-- a regular level, or in a union level. In the former case, only updates can
-- invalidate the blob reference. In the latter case, only supplying union
-- credits can invalidate the blob reference.
--
-- Without considerable knowledge about the /real/ implementation, the model can
-- not /always/ accurately predict which of two cases a blob reference belongs
-- to. However, there is one case where a table is guaranteed not to contain a
-- union level: if the table is /not/ a (descendant of a) union table.
--
-- There is another caveat: without considerable knowledge about the real
-- implementation, the model can not accurately predict after how many supplied
-- union credits /real/ blob references are invalidated. Therefore, we model
-- invalidation conservatively in a similar way to 'updates': any supply of
-- @>=1@ union credits is enough to invalidate union blob references.
--
-- To summarise, 'supplyUnionCredits' will invalidate blob references associated
-- with the input table if:
--
-- * The table is a (descendant of a) union table
--
-- * The number of supplied union credits is at least 1.
supplyUnionCredits ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> Table k v b
-> UnionCredits
-> m UnionCredits
supplyUnionCredits t@Table{..} c@(UnionCredits credits)
| credits <= 0 = do
_ <- guardTableIsOpen t
pure (UnionCredits 0) -- always 0, not negative
| otherwise = do
(updc, table) <- guardTableIsOpen t
when (isUnionDescendant == IsUnionDescendant) $
modify (\m -> m {
tables = Map.insert tableID (updc + 1, toSomeTable table) (tables m)
})
pure c
-- | A version of 'supplyUnionCredits' that supplies a portion of the current debt.
--
-- The debt in the model is always 0, so any portion of that debt is also 0. The
-- real system might have non-zero debt, but the model does not know how much,
-- so it has to assume that *any* portion (assuming it's a positive portion)
-- leads to invalidated blob references.
supplyPortionOfDebt ::
( MonadState Model m
, MonadError Err m
, C k v b
)
=> Table k v b
-> portion
-> m UnionCredits
supplyPortionOfDebt t@Table{..} _ = do
(updc, table) <- guardTableIsOpen t
when (isUnionDescendant == IsUnionDescendant) $
modify (\m -> m {
tables = Map.insert tableID (updc + 1, toSomeTable table) (tables m)
})
pure (UnionCredits 0)