{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE RankNTypes #-} {-| Module : Std.IO.Resource Description : The Resource monad Copyright : (c) Dong Han, 2017 License : BSD Maintainer : winterland1989@gmail.com Stability : experimental Portability : non-portable This module also implements Gabriel Gonzalez'd idea on 'Resource' applicative: . The 'Applicative' and 'Monad' instance is especially useful when you want safely combine multiple resources. A high performance resource pool based on STM is also provided. -} module Std.IO.Resource ( -- * Resource management Resource(..) , initResource , initResource_ , withResource , withResource' -- * Resource pool , Pool , PoolState(..) , initPool , statPool , initInPool ) where import Control.Concurrent.STM import Control.Concurrent.STM.TVar import Control.Monad import qualified Control.Monad.Catch as MonadCatch import Control.Monad.IO.Class import Std.Data.PrimIORef import Std.IO.LowResTimer import Std.IO.Exception -------------------------------------------------------------------------------- -- | A 'Resource' is an 'IO' action which acquires some resource of type a and -- also returns a finalizer of type IO () that releases the resource. -- -- The only safe way to use a 'Resource' is 'withResource' \/ 'withResource\'', -- You should not use the `acquire` field directly, unless you want to implement your own -- resource management. In the later case, you should always use 'mask_' since -- some resource initializations may assume async exceptions are masked. -- -- 'MonadIO' instance is provided so that you can lift 'IO' computation inside -- 'Resource', this is convenient for propagating 'Resource' around since many -- 'IO' computations carry finalizers. -- -- A convention in stdio is that functions returning a 'Resource' should be -- named in @initXXX@ format, users are strongly recommended to follow this convention. -- -- There're two additional guarantees we made in stdio: -- -- * All resources in stdio can track its own liveness, throw 'ResourceVanished' -- exception using 'throwECLOSED' or 'throwECLOSEDSTM' when used after resource -- is closed. -- -- * All resources' clean up action in stdio is idempotent. -- -- Library authors providing 'initXXX' are also encouraged to provide these guarantees. -- newtype Resource a = Resource { acquire :: HasCallStack => IO (a, IO ()) } -- | Create 'Resource' from create and release action. -- -- Note, 'resource' doesn't open resource itself, resource is created when you use -- 'with' \/ 'with''. -- initResource :: IO a -> (a -> IO ()) -> Resource a {-# INLINE initResource #-} initResource create release = Resource $ do r <- create return $ (r, release r) -- | Create 'Resource' from create and release action. -- -- This function is useful when you want to add some initialization and clean up action -- inside 'Resource' monad. -- initResource_ :: IO () -> IO () -> Resource () {-# INLINE initResource_ #-} initResource_ create release = Resource $ do r <- create return $ (r, release) instance Functor Resource where {-# INLINE fmap #-} fmap f resource = Resource $ do (a, release) <- acquire resource return (f a, release) instance Applicative Resource where {-# INLINE pure #-} pure a = Resource (pure (a, pure ())) {-# INLINE (<*>) #-} resource1 <*> resource2 = Resource $ do (f, release1) <- acquire resource1 (x, release2) <- acquire resource2 `onException` release1 return (f x, release2 >> release1) instance Monad Resource where {-# INLINE return #-} return = pure {-# INLINE (>>=) #-} m >>= f = Resource $ do (m', release1) <- acquire m (x , release2) <- acquire (f m') `onException` release1 return (x, release2 >> release1) instance MonadIO Resource where {-# INLINE liftIO #-} liftIO f = Resource $ fmap (\ a -> (a, dummyRelease)) f where dummyRelease = return () -- | Create a new resource and run some computation, resource is guarantee to -- be closed. -- -- Be care don't leak the resource through computation return value, because -- after the computation finishes, the resource is closed already. -- withResource :: (MonadCatch.MonadMask m, MonadIO m, HasCallStack) => Resource a -> (a -> m b) -> m b {-# INLINABLE withResource #-} withResource resource k = MonadCatch.bracket (liftIO (acquire resource)) (\(_, release) -> liftIO release) (\(a, _) -> k a) -- | Create a new resource and run some computation, resource is guarantee to -- be closed. -- -- The difference from 'with' is that the computation will receive an extra -- close action, which can be used to close the resource early before the whole -- computation finished, the close action can be called multiple times, -- only the first call will clean up the resource. -- withResource' :: (MonadCatch.MonadMask m, MonadIO m, HasCallStack) => Resource a -> (a -> m () -> m b) -> m b {-# INLINABLE withResource' #-} withResource' resource k = do c <- liftIO (newCounter 0) MonadCatch.bracket (liftIO $ do (a, release) <- (acquire resource) let release' = do c' <- atomicOrCounter c 1 when (c' == 0) release return (a, release')) (\(_, release) -> liftIO release) (\(a, release) -> k a (liftIO release)) -------------------------------------------------------------------------------- -- | A single resource pool entry. data Entry a = Entry (a, IO ()) -- the resource and clean up action {-# UNPACK #-} !Int -- the life remaining data PoolState = PoolClosed | PoolScanning | PoolEmpty deriving (Eq, Show) -- | A high performance resource pool based on STM. -- -- We choose to not divide pool into strips due to the difficults in resource balancing. If there -- is a high contention on resource (see 'statPool'), just increase the maximum number of resources -- can be opened. -- data Pool a = Pool { poolResource :: Resource a , poolLimit :: Int , poolIdleTime :: Int , poolEntries :: TVar [Entry a] , poolInUse :: TVar Int , poolState :: TVar PoolState } -- | Initialize a resource pool with given 'Resource' -- -- Like other initXXX functions, this function won't open a resource pool until you use 'withResource'. -- And this resource pool follow the same resource management pattern like other resources. -- initPool :: Resource a -> Int -- ^ maximum number of resources can be opened -> Int -- ^ amount of time after which an unused resource can be released (in seconds). -> Resource (Pool a) initPool res limit itime = initResource createPool closePool where createPool = do entries <- newTVarIO [] inuse <- newTVarIO 0 state <- newTVarIO PoolEmpty return (Pool res limit itime entries inuse state) closePool (Pool _ _ _ entries _ state) = join . atomically $ do c <- readTVar state if c == PoolClosed then return (return ()) else do writeTVar state PoolClosed return (do es <- readTVarIO entries forM_ es $ \ (Entry (_, close) _) -> MonadCatch.handleAll (\ _ -> return ()) close) -- | Get a resource pool's 'PoolState' -- -- This function is useful when debug, under load lots of 'PoolEmpty' may indicate -- contention on resources, i.e. the limit on maximum number of resources can be opened -- should be adjusted to a higher number. On the otherhand, lots of 'PoolScanning' -- may indicate there're too much free resources. -- statPool :: Pool a -> IO PoolState statPool pool = readTVarIO (poolState pool) -- | Obtain the pooled resource inside a given resource pool. -- -- You shouldn't use 'withResource' with this resource after you closed the pool, -- an 'ResourceVanished' with @EPOOLCLOSED@ name will be thrown. -- initInPool :: Pool a -> Resource a initInPool (Pool res limit itime entries inuse state) = fst <$> initResource takeFromPool returnToPool where takeFromPool = join . atomically $ do c <- readTVar state if c == PoolClosed then throwECLOSEDSTM else do es <- readTVar entries case es of ((Entry a _):es') -> do writeTVar entries es' return (return a) _ -> do i <- readTVar inuse when (i == limit) retry modifyTVar' inuse (+1) return (acquire res `onException` atomically (modifyTVar' inuse (subtract 1))) returnToPool a = join . atomically $ do c <- readTVar state case c of PoolClosed -> return (snd a) PoolEmpty -> do modifyTVar' entries (Entry a itime:) writeTVar state PoolScanning return (void $ registerLowResTimer 10 (scanPool entries inuse state)) _ -> do modifyTVar' entries (Entry a itime:) return (return ()) scanPool entries inuse state = do join . atomically $ do c <- readTVar state if c == PoolClosed then return (return ()) else do es <- readTVar entries if (null es) then do writeTVar state PoolEmpty return (return ()) else do let (deadNum, dead, living) = age es 0 [] [] writeTVar entries living modifyTVar' inuse (subtract deadNum) return (do forM_ dead $ \ (_, close) -> MonadCatch.handleAll (\ _ -> return ()) close void $ registerLowResTimer 10 (scanPool entries inuse state)) age ((Entry a life):es) !deadNum dead living | life > 1 = age es deadNum dead (Entry a (life-1):living) | otherwise = age es (deadNum+1) (a:dead) living age _ !deadNum dead living = (deadNum, dead, living)