| Safe Haskell | Safe-Inferred | 
|---|---|
| Language | GHC2021 | 
Control.ActionRegistry
Description
Registry of monadic actions supporting rollback actions and delayed actions in the presence of (a-)synchronous exceptions.
This module is heavily inspired by:
Synopsis
- modifyWithActionRegistry :: (PrimMonad m, MonadCatch m) => m st -> (st -> m ()) -> (ActionRegistry m -> st -> m (st, a)) -> m a
 - modifyWithActionRegistry_ :: (PrimMonad m, MonadCatch m) => m st -> (st -> m ()) -> (ActionRegistry m -> st -> m st) -> m ()
 - data ActionRegistry m
 - data ActionError
 - getActionError :: ActionError -> SomeException
 - mapActionError :: (SomeException -> SomeException) -> ActionError -> ActionError
 - withActionRegistry :: (PrimMonad m, MonadCatch m) => (ActionRegistry m -> m a) -> m a
 - unsafeNewActionRegistry :: PrimMonad m => m (ActionRegistry m)
 - unsafeFinaliseActionRegistry :: (PrimMonad m, MonadCatch m) => ActionRegistry m -> ExitCase a -> m ()
 - data CommitActionRegistryError = CommitActionRegistryError (NonEmpty ActionError)
 - data AbortActionRegistryError = AbortActionRegistryError AbortActionRegistryReason (NonEmpty ActionError)
 - data AbortActionRegistryReason
 - getReasonExitCaseException :: AbortActionRegistryReason -> Maybe SomeException
 - mapExceptionWithActionRegistry :: (Exception e1, Exception e2, MonadCatch m) => (e1 -> e2) -> m a -> m a
 - withRollback :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m a -> (a -> m ()) -> m a
 - withRollback_ :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m a -> m () -> m a
 - withRollbackMaybe :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m (Maybe a) -> (a -> m ()) -> m (Maybe a)
 - withRollbackEither :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m (Either e a) -> (a -> m ()) -> m (Either e a)
 - withRollbackFun :: (PrimMonad m, MonadMask m) => ActionRegistry m -> (a -> Maybe b) -> m a -> (b -> m ()) -> m a
 - delayedCommit :: PrimMonad m => ActionRegistry m -> m () -> m ()
 
Modify mutable state
When a piece of mutable state holding system resources is updated, then it is important to guarantee in the presence of (a-)synchronous exceptions that:
- Allocated resources end up in the state
 - Freed resources are removed from the state
 
Consider the example program below. We have some mutable State that holds a
  file handle/descriptor. We want to mutate this state by closing the current
  handle, and replacing it by a newly opened handle. Using the tools at our
  disposal in Control.ActionRegistry, we guarantee (1) and (2).
   type State = MVar Handle
   example :: State -> IO ()
   example st =
     modifyWithActionRegistry_
       (takeMVar st)
       (putMVar st)
       $ \reg h -> do
         h' <- withRollback reg
                 (openFile  "file.txt" ReadWriteMode)
                 hClose
         delayedCommit reg (hClose h)
         pure h'
 What is also nice about this examples is that it is atomic: other threads will
  not be able to see the updated State until modifyWithActionRegistry_ has
  exited and the necessary side effects have been performed. Of course, another
  thread *could* observe that the file.txt was created before
  modifyWithActionRegistry_ has exited, but the assumption is that the threads
  in our program are cooperative. It is up to the user to ensure that actions
  that are performed as part of the state update do not conflict with other
  actions.
modifyWithActionRegistry Source #
Arguments
| :: (PrimMonad m, MonadCatch m) | |
| => m st | Get the state  | 
| -> (st -> m ()) | Store a state  | 
| -> (ActionRegistry m -> st -> m (st, a)) | Modify the state  | 
| -> m a | 
Modify a piece piece of state given a fresh action registry.
modifyWithActionRegistry_ Source #
Arguments
| :: (PrimMonad m, MonadCatch m) | |
| => m st | Get the state  | 
| -> (st -> m ()) | Store a state  | 
| -> (ActionRegistry m -> st -> m st) | |
| -> m () | 
Like modifyWithActionRegistry, but without a return value.
Action registry
An ActionRegistry is a registry of monadic actions to support working with
  resources and mutable state in the presence of (a)synchronous exceptions. It
  works analogously to database transactions: within the "transaction" scope
  we can perform actions (such as resource allocations and state changes) and we
  can register delayed (commit) and rollback actions. The delayed actions are
  all executed at the end if the transaction scope is exited successfully, but
  if an exception is thrown (sync or async) then the rollback actions are
  executed instead, and the exception is propagated.
- Rollback actions are executed in the reverse order in which they were registered, which is the natural nesting order when considered as bracketing.
 - Delayed actions are executed in the same order in which they are registered.
 
data ActionRegistry m Source #
Registry of monadic actions supporting rollback actions and delayed actions in the presence of (a-)synchronous exceptions.
See Action registry for more information.
An action registry should be short-lived, and it is not thread-safe.
data ActionError Source #
Instances
| Exception ActionError Source # | |
Defined in Control.ActionRegistry Methods toException :: ActionError -> SomeException # fromException :: SomeException -> Maybe ActionError # displayException :: ActionError -> String #  | |
| Show ActionError Source # | |
Defined in Control.ActionRegistry Methods showsPrec :: Int -> ActionError -> ShowS # show :: ActionError -> String # showList :: [ActionError] -> ShowS #  | |
mapActionError :: (SomeException -> SomeException) -> ActionError -> ActionError Source #
Runners
withActionRegistry :: (PrimMonad m, MonadCatch m) => (ActionRegistry m -> m a) -> m a Source #
Run code with a new ActionRegistry.
(A-)synchronous exception safety is only guaranteed within the scope of
 withActionRegistry (and only for properly registered actions). As soon as
 we leave this scope, all bets are off. If, for example, a newly allocated
 file handle escapes the scope, then that file handle can be leaked. If such
 is the case, then it is highly likely that you should be using
 modifyWithActionRegistry instead.
If the code was interrupted due to an exception for example, then the registry is aborted, which performs registered rollback actions. If the code successfully terminated, then the registry is committed, in which case registered, delayed actions will be performed.
Registered actions are run in LIFO order, whether they be rollback actions or delayed actions.
unsafeNewActionRegistry :: PrimMonad m => m (ActionRegistry m) Source #
This function is considered unsafe. Preferably, use withActionRegistry
 instead.
If this function is used directly, use generalBracket to pair
 unsafeNewActionRegistry with an unsafeFinaliseActionRegistry.
unsafeFinaliseActionRegistry :: (PrimMonad m, MonadCatch m) => ActionRegistry m -> ExitCase a -> m () Source #
This function is considered unsafe. See unsafeNewActionRegistry.
This commits the action registry on ExitCaseSuccess, and otherwise aborts
 the action registry.
data CommitActionRegistryError Source #
Constructors
| CommitActionRegistryError (NonEmpty ActionError) | 
Instances
| Exception CommitActionRegistryError Source # | |
Defined in Control.ActionRegistry  | |
| Show CommitActionRegistryError Source # | |
Defined in Control.ActionRegistry Methods showsPrec :: Int -> CommitActionRegistryError -> ShowS # show :: CommitActionRegistryError -> String # showList :: [CommitActionRegistryError] -> ShowS #  | |
data AbortActionRegistryError Source #
Constructors
| AbortActionRegistryError AbortActionRegistryReason (NonEmpty ActionError) | 
Instances
| Exception AbortActionRegistryError Source # | |
Defined in Control.ActionRegistry  | |
| Show AbortActionRegistryError Source # | |
Defined in Control.ActionRegistry Methods showsPrec :: Int -> AbortActionRegistryError -> ShowS # show :: AbortActionRegistryError -> String # showList :: [AbortActionRegistryError] -> ShowS #  | |
data AbortActionRegistryReason Source #
Reasons why an action registry was aborted.
Constructors
| ReasonExitCaseException SomeException | The action registry was aborted because the code that it scoped over
 threw an exception (see   | 
| ReasonExitCaseAbort | The action registry was aborted because the code that it scoped over
 aborted (see   | 
Instances
| Show AbortActionRegistryReason Source # | |
Defined in Control.ActionRegistry Methods showsPrec :: Int -> AbortActionRegistryReason -> ShowS # show :: AbortActionRegistryReason -> String # showList :: [AbortActionRegistryReason] -> ShowS #  | |
mapExceptionWithActionRegistry :: (Exception e1, Exception e2, MonadCatch m) => (e1 -> e2) -> m a -> m a Source #
As mapException, but aware of the structure of
   AbortActionRegistryError and CommitActionRegistryError.
Registering actions
Actions are monadic computations that (may) produce side effects. Such side effects can include opening or closing a file handle, but also modifying a mutable variable.
We make a distinction between three types of actions:
- An immediate action is performed immediately, as the name suggests.
 - A rollback action is an action that is registered in an action registry,
    and it is performed precisely when the corresponding action registry is
    aborted. See 
withRollbackfor examples. - A delayed action is an action that is registered in an action registry,
    and it is performed precisely when the corresponding action registry is
    committed. See 
delayedCommitfor examples. 
Immediate actions are run with asynchronous exceptions masked to guarantee that the rollback action is registered after the immediate action has returned successfully. This means that all the usual masking caveats apply for the immediate acion.
Rollback actions and delayed actions are performed precisely when aborting or committing an action registry respectively (see Action registry). To achieve this, finalisation of the action registry happens in the same masked state as running the registered actions. This means all the usual masking caveats apply for the registered actions.
withRollback :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m a -> (a -> m ()) -> m a Source #
Perform an immediate action and register a rollback action.
See Registering actions for more information about the different types of actions.
A typical use case for withRollback is to allocate a resource as the
 immediate action, and to release said resource as the rollback action. In
 that sense, withRollback is similar to bracketOnError, but withRollback
 offers stronger guarantees.
Note that the following two expressions are not equivalent. The former is correct in the presence of asynchronous exceptions, while the latter is not!
withRollback reg acquire free =/= acquire >>= x -> withRollback reg free (pure x)
withRollback_ :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m a -> m () -> m a Source #
Like withRollback, but the rollback action does not get access to the
 result of the immediate action.
withRollbackMaybe :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m (Maybe a) -> (a -> m ()) -> m (Maybe a) Source #
Like withRollback, but the immediate action may fail with a Nothing.
 The rollback action will only be registered if Just.
withRollbackEither :: (PrimMonad m, MonadMask m) => ActionRegistry m -> m (Either e a) -> (a -> m ()) -> m (Either e a) Source #
Like withRollback, but the immediate action may fail with a Left. The
 rollback action will only be registered if Right.
withRollbackFun :: (PrimMonad m, MonadMask m) => ActionRegistry m -> (a -> Maybe b) -> m a -> (b -> m ()) -> m a Source #
Like withRollback, but the immediate action may fail in some general
 way. The rollback function will only be registered if the (a -> Maybe b)
 function returned Just.
withRollbackFun is the most general form in the 'withRollback*' family of
 functions. All 'withRollback*' functions can be defined in terms of
 withRollBackFun.
delayedCommit :: PrimMonad m => ActionRegistry m -> m () -> m () Source #
Register a delayed action.
See Registering actions for more information about the different types of actions.
A typical use case for delayedCommit is to delay destructive actions until
 they are safe to be performed. For example, a destructive action such as
 removing a file can often not be rolled back without jumping through
 additional hoops.
If you can think of a sensible rollback action for the action you want to
 delay then withRollback might be a more suitable fit than delayedCommit.
 For example, incrementing a thread-safe mutable variable can easily be rolled
 back by decrementing the same variable again.