streamly-core-0.3.0: src/Streamly/Internal/Control/Exception.hs
{-# LANGUAGE CPP #-}
-- |
-- Module : Streamly.Internal.Control.Exception
-- Copyright : (c) 2019 Composewell Technologies
--
-- License : BSD3
-- Maintainer : streamly@composewell.com
-- Stability : experimental
-- Portability : GHC
--
-- Additional "Control.Exception" utilities.
module Streamly.Internal.Control.Exception
(
-- * Verify
verify
, verifyM
-- * Resource Management
-- | Exception safe, thread safe resource managment operations, similar to
-- but more powerful than the @bracket@ and @finally@ operations available
-- in the base package.
--
-- These operations support allocation and free only in the IO monad,
-- hence the IO suffix.
--
, AcquireIO(..)
, Priority(..)
, allocator
, releaser
, withAcquireIO
, acquireWith
, acquire
, acquire_
, registerWith
, register
, hook
)
where
-- import Control.Concurrent (myThreadId)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Exception (mask_)
import Control.Monad.Catch (MonadMask)
import Data.IntMap.Strict (IntMap)
import Data.IORef (IORef, newIORef, atomicModifyIORef')
import qualified Control.Monad.Catch as MC
import qualified Data.IntMap.Strict as Map
#include "DocTestControlException.hs"
-------------------------------------------------------------------------------
-- Asserts
-------------------------------------------------------------------------------
-- | Like 'assert' but is not removed by the compiler, it is always present in
-- production code.
--
-- /Pre-release/
--
{-# INLINE verify #-}
verify :: Bool -> a -> a
verify predicate val =
if predicate
-- XXX it would be nice if we can print the predicate expr.
then error "verify failed"
else val
-- Like 'verify' but returns @()@ in an 'Applicative' context so that it can be
-- used as an independent statement in a @do@ block.
--
-- /Pre-release/
--
{-# INLINE verifyM #-}
verifyM :: Applicative f => Bool -> f ()
verifyM predicate = verify predicate (pure ())
-------------------------------------------------------------------------------
-- Resource management
-------------------------------------------------------------------------------
-- XXX In a manual release mechanism of resources we always have the risk of
-- using the resource by some persisting thread even after it has been freed.
-- Ideally, we should use the GC to clean up resources because that way we do
-- not need to worry about references, we can pass around resources to other
-- threads and we get an automatic reference counting. Is it possible to use
-- compact regions to confine resource to smaller areas so that we can perform
-- a limited GC to free them? We can then just put gc sync barriers at points
-- where we want to ensure that resources are freed.
-- | Resources with 'Priority1' are freed before 'Priority2'. Priority is
-- especially introduced to take care of the case where we need to free
-- concurrency channels, so that all the workers of the channel are cleaned up
-- before we free the resources allocated by the workers of the channel.
-- Otherwise we might free the resources and workers may be trying to use them
-- and start misbehaving.
--
data Priority = Priority1 | Priority2 deriving Show
-- To keep the type signatures simple and to avoid inference problems we should
-- use this newtype. We cannot pass around a foralled type without wrapping
-- it in a newtype.
-- | @AcquireIO@ is used to acquire a resource safely such that it is
-- automatically released if not released manually.
--
-- See 'withAcquireIO'.
--
newtype AcquireIO = AcquireIO
(forall b c. Priority -> IO b -> (b -> IO c) -> IO (b, IO ()))
-- | /Internal/.
allocator :: MonadIO m =>
IORef (Int, IntMap (IO ()), IntMap (IO ()))
-> Priority
-> IO a
-> (a -> IO b)
-> m (a, m ())
allocator ref pri alloc free = do
let insertResource r (i, mp1, mp2) =
case pri of
Priority1 ->
((i + 1, Map.insert i (void $ free r) mp1, mp2), i)
Priority2 ->
((i + 1, mp1, Map.insert i (void $ free r) mp2), i)
(r, index) <-
liftIO $ mask_ $ do
-- tid <- myThreadId
r <- alloc
idx <- atomicModifyIORef' ref (insertResource r)
-- liftIO $ putStrLn $ "insert: " ++ show pri
-- ++ " " ++ show idx ++ " " ++ show tid
return (r, idx)
let deleteResource (i, mp1, mp2) =
case pri of
Priority1 ->
let res = Map.lookup index mp1
in ((i, Map.delete index mp1, mp2), res)
Priority2 ->
let res = Map.lookup index mp2
in ((i, mp1, Map.delete index mp2), res)
release =
-- IMPORTANT: do not use interruptible operations in this
-- critical section. Even putStrLn can make tests fail.
liftIO $ mask_ $ do
-- tid <- myThreadId
-- liftIO $ putStrLn $ "releasing index: " ++ show index
-- ++ " " ++ show tid
f <- atomicModifyIORef' ref deleteResource
-- restoring exceptions makes it non-atomic, tests fail.
-- Can use allowInterrupt in "free" if desired.
sequence_ f
return (r, release)
-- XXX can we ensure via GC that the resources that we are freeing are all
-- dead, there are no other references to them?
-- | We ensure that all async workers for concurrent streams are stopped
-- before we release the resources so that nobody could be using the
-- resource after they are freed.
--
-- The only other possibility, could be user issued forkIO not being
-- tracked by us, however, that would be a programming error and any such
-- threads could misbehave if we freed the resources from under them.
--
-- We use GC based hooks in 'Stream.bracketIO\'' so there could be async threads
-- spawned by GC, releasing resources concurrently with us. For that reason we
-- need to make sure that the "release" in the bracket end action is executed
-- only once in that case.
--
-- /Internal/.
releaser :: MonadIO m => IORef (a, IntMap (IO b), IntMap (IO b)) -> m ()
releaser ref =
liftIO $ mask_ $ do
-- Delete the map from the ref first so that anyone else (GC)
-- releasing concurrently cannot find the map.
-- liftIO $ putStrLn "cleaning up priority 1"
mp1 <- atomicModifyIORef' ref
(\(i, mp1,mp2) -> ((i, Map.empty, mp2), mp1))
-- Note that the channel cleanup function is interruptible because
-- it has blocking points.
sequence_ mp1
-- Now nobody would be changing mp2, we can read it safely
-- liftIO $ putStrLn "cleaning up priority 2"
mp2 <- atomicModifyIORef' ref
(\(i, mp,mp2) -> ((i, mp, Map.empty), mp2))
sequence_ mp2
-- XXX We can now assert that the IORef has both maps empty.
-- | @withAcquireIO action@ runs the given @action@, providing it with a
-- an 'AcquireIO' reference called @ref@ as argument. @ref@ is used for resource
-- acquisition or hook registeration within the scope of @action@. An @acquire
-- ref alloc free@ call can be used within @action@ any number of times to
-- acquire resources that are automatically freed when the scope of @action@
-- ends or if an exception occurs at any time. @alloc@ is a function supplied
-- by the user to allocate a resource and @free@ is supplied to free the
-- allocated resource. @acquire@ returns @(resource, release)@ -- the acquired
-- @resource@ and a @release@ action to release it.
--
-- @acquire@ allocates a resource in an exception safe manner and sets up its
-- automatic release on exception or when the scope of @action@ ends. The
-- @release@ function returned by @acquire@ can be used to free the resource
-- manually at any time. @release@ is guaranteed to free the resource once and
-- only once even if it is called concurrently or multiple times.
--
-- Here is an example to allocate resources that are guaranteed to be released
-- automatically, and can be released manually as well:
--
-- >>> :{
-- close x h = do
-- putStrLn $ "closing: " ++ x
-- hClose h
-- :}
--
-- >>> :{
-- action ref =
-- Stream.fromList ["file1", "file2"]
-- & Stream.mapM
-- (\x -> do
-- (h, release) <- Exception.acquire ref (openFile x ReadMode) (close x)
-- -- use h here
-- threadDelay 1000000
-- when (x == "file1") $ do
-- putStrLn $ "Manually releasing: " ++ x
-- release
-- return x
-- )
-- & Stream.trace print
-- & Stream.fold Fold.drain
-- :}
--
-- >>> run = Exception.withAcquireIO action
--
-- In the above code, you should see the \"closing:\" message for both the
-- files, and only once for each file. Even if you interrupt the program with
-- CTRL-C you should still see the \"closing:\" message for the files opened
-- before the interrupt. Make sure you create "file1" and "file2" before
-- running this code snippet.
--
-- Cleanup is guaranteed to happen as soon as the scope of 'action'
-- finishes or if an exception occurs.
--
-- Here is an example for just registering hooks to be called eventually:
--
-- >>> :{
-- action ref =
-- Stream.fromList ["file1", "file2"]
-- & Stream.mapM
-- (\x -> do
-- Exception.register ref $ putStrLn $ "saw: " ++ x
-- threadDelay 1000000
-- return x
-- )
-- & Stream.trace print
-- & Stream.fold Fold.drain
-- :}
--
-- >>> run = Exception.withAcquireIO action
--
-- In the above code, even if you interrupt the program with CTRL-C you should
-- still see the "saw:" message for the elements seen before the interrupt.
--
-- The registered hooks are guaranteed to be invoked as soon as the scope of
-- 'action' finishes or if an exception occurs.
--
-- This function provides functionality similar to the @bracket@ function
-- available in the base library. However, it is more powerful as any number of
-- resources can be allocated and released within the scope of 'action'.
--
-- Exception safe, thread safe.
{-# INLINE withAcquireIO #-}
withAcquireIO :: (MonadIO m, MonadMask m) => (AcquireIO -> m a) -> m a
withAcquireIO action = do
-- Assuming 64-bit int counter will never overflow
ref <- liftIO $ newIORef (0 :: Int, Map.empty, Map.empty)
action (AcquireIO (allocator ref)) `MC.finally` releaser ref
-- | Like 'acquire' but allows specifying a priority for releasing the
-- resource. 'Priority1' resources are released before 'Priority2'. This allows
-- us to specify a dependency between resource release.
{-# INLINE acquireWith #-}
acquireWith :: Priority -> AcquireIO -> IO b -> (b -> IO c) -> IO (b, IO ())
acquireWith pri (AcquireIO f) = f pri
-- | @acquire ref alloc free@ is used in bracket-style safe resource allocation
-- functions, where @alloc@ is a function supplied by the user to allocate a
-- resource and @free@ is supplied to free it. @acquire@ returns a tuple
-- @(resource, release)@ where @resource@ is the allocated resource and
-- @release@ is an action that can be called later to release the resource.
-- Both @alloc@ and @free@ are invoked with async signals masked. You can use
-- @allowInterrupt@ from base package for allowing interrupts if required.
--
-- The @release@ action can be called multiple times or even concurrently from
-- multiple threads, but it will release the resource only once. If @release@
-- is never called by the programmer it will be automatically called at the end
-- of the bracket scope.
--
acquire :: AcquireIO -> IO b -> (b -> IO c) -> IO (b, IO ())
acquire = acquireWith Priority2
-- | Like 'acquire' but does not return a release action. The resource is freed
-- automatically only.
acquire_ :: AcquireIO -> IO b -> (b -> IO c) -> IO b
acquire_ a b c = fmap fst $ acquire a b c
-- | Like 'register' but specifies a 'Priority' for calling the hook.
{-# INLINE registerWith #-}
registerWith :: Priority -> AcquireIO -> IO () -> IO ()
registerWith pri (AcquireIO f) g = void $ f pri (return ()) (\() -> g)
-- | Register a hook to be executed at the end of a bracket.
register :: AcquireIO -> IO () -> IO ()
register = registerWith Priority2
-- | Like 'register' but returns a hook release function as well. When the
-- returned hook release function is called, the hook is invoked and removed.
-- If the returned function is never called by the programmer then it is
-- automatically invoked at the end of the bracket. The hook is invoked once
-- and only once.
--
hook :: AcquireIO -> IO () -> IO (IO())
hook (AcquireIO f) g = fmap snd $ f Priority2 (return ()) (\() -> g)