{-# OPTIONS_GHC -Wno-error=unused-top-binds #-} module Utils.Pool ( Pool', hoistPool , PoolResourceIdent' , Pool, PoolResourceIdent , getPoolAvailableCount, getPoolInUseCount, getPoolUsesCount , getPoolIdleTime, getPoolMaxAvailable , createPool, createPool' , purgePool , withResource, withResource' , destroyResources , takeResource, releaseResource , takeResource' ) where import ClassyPrelude import qualified Data.IntMap.Strict as IntMap import UnliftIO.Async.Utils import UnliftIO.Resource (MonadResource, register, release) import UnliftIO.Concurrent (forkIO) import Data.Fixed import System.Clock import Data.Time.Clock (DiffTime) import Control.Concurrent.STM.Delay import Control.Concurrent.STM.TVar (stateTVar) import Control.Monad.Writer.Strict (runWriter) import Control.Monad.Writer.Class (MonadWriter(..)) import Data.Semigroup (First(..)) import Utils.NTop -- data PoolResourceIdent' c = PoolResourceIdent Int c deriving (Eq, Ord, Show, Typeable) data Pool' m c' c a = Pool { create :: m a , destroy :: a -> m () , onUse :: c' -> a -> m c , onRelease :: c -> a -> m () , idleTime :: !(Maybe Int) , maxAvailable :: !(Maybe Int) , resources :: !(TVar (PoolResources c a)) , aliveRef :: !(IORef ()) } data PoolResources c a = PoolResources { inUseCount, availableCount :: !Int , inUse :: !(IntMap a) , available :: !(IntMap [a]) , inUseTick :: !Int } deriving (Functor) type Pool m a = Pool' m () () a type PoolResourceIdent = PoolResourceIdent' () hoistPool :: (forall b. m b -> n b) -> Pool' m c' c a -> Pool' n c' c a hoistPool nat Pool{..} = Pool { create = nat create , destroy = nat . destroy , onUse = (nat .) . onUse , onRelease = (nat .) . onRelease , .. } getPoolAvailableCount, getPoolInUseCount, getPoolUsesCount :: Pool' m c' c a -> STM Int getPoolAvailableCount Pool{..} = availableCount <$> readTVar resources getPoolInUseCount Pool{..} = inUseCount <$> readTVar resources getPoolUsesCount Pool{..} = inUseTick <$> readTVar resources getPoolIdleTime :: Pool' m c' c a -> Maybe DiffTime getPoolIdleTime = fmap realToFrac . maxAvailable getPoolMaxAvailable :: Pool' m c' c a -> Maybe Int getPoolMaxAvailable = maxAvailable toSecond :: TimeSpec -> Int toSecond = fromIntegral . sec currentSecond :: MonadIO m => m Int currentSecond = liftIO $ toSecond <$> getTime Monotonic createPool :: (MonadResource m, MonadUnliftIO m, MonadUnliftIO m') => (forall b. m' b -> m b) -> m' a -- ^ Create -> (a -> m' ()) -- ^ Destroy -> Maybe Int -- ^ Timeout in seconds -> Maybe Int -- ^ Max available -> m (Pool m' a) createPool nat create destroy = createPool' nat create destroy (\() _ -> return ()) (\() _ -> return ()) createPool' :: (MonadResource m, MonadUnliftIO m, MonadUnliftIO m') => (forall b. m' b -> m b) -> m' a -- ^ Create -> (a -> m' ()) -- ^ Destroy -> (c' -> a -> m' c) -- ^ onUse -> (c -> a -> m' ()) -- ^ onRelease -> Maybe Int -- ^ Timeout in seconds -> Maybe Int -- ^ Max available -> m (Pool' m' c' c a) createPool' nat create destroy onUse onRelease (fmap $ max 0 -> idleTime) (fmap $ max 0 -> maxAvailable) = do let inUseCount = 0 availableCount = 0 inUseTick = 0 inUse = IntMap.empty available = IntMap.empty aliveRef <- newIORef () resources <- newTVarIO PoolResources{..} let pool = Pool{..} reaper' <- for idleTime $ allocateLinkedAsync . nat . reaper destroy resources relKey <- withRunInIO $ \runInIO -> runInIO . register . runInIO $ do traverse_ cancel reaper' nat $ purgePool pool void . mkWeakIORef aliveRef $ release relKey return pool purgePool :: MonadUnliftIO m => Pool' m c' c a -> m () purgePool = destroyResources $ const True reaper :: MonadUnliftIO m => (a -> m ()) -> TVar (PoolResources c a) -> Int -> m () reaper destroy' resources' t = forever $ do atomically . waitDelay =<< liftIO (newDelay i) cutoff <- subtract t <$> currentSecond toDestroy <- atomically $ do res@PoolResources{..} <- readTVar resources' let (toDestroy, pivot, available'') = IntMap.splitLookup cutoff available available' = maybe id (IntMap.insert cutoff) pivot available'' writeTVar resources' res { available = available' , availableCount = availableCount - IntMap.size toDestroy } return toDestroy forM_ toDestroy . mapM_ $ void . destroy' where MkFixed (fromIntegral -> i) = 1 :: Micro takeResource :: MonadIO m => Pool m a -> m (a, PoolResourceIdent' ()) takeResource p = takeResource' p () takeResource' :: MonadIO m => Pool' m c' c a -> c' -> m (a, PoolResourceIdent' c) takeResource' Pool{..} stateInit = do takenAvailable <- atomically $ do PoolResources{..} <- readTVar resources case IntMap.maxViewWithKey available of Just ((t, av : avs), available') -> do let available'' | null avs = available' | otherwise = IntMap.insert t avs available' availableCount' = pred availableCount inUse' = IntMap.insert inUseTick av inUse inUseCount' = succ inUseCount inUseTick' = succ inUseTick writeTVar resources PoolResources { inUseCount = inUseCount' , availableCount = availableCount' , available = available'' , inUseTick = inUseTick' , inUse = inUse' } return $ Just (av, inUseTick) _other -> return Nothing case takenAvailable of Just (av, resTick) -> do hookData <- onUse stateInit av return (av, PoolResourceIdent resTick hookData) Nothing -> do newResource <- create resTick <- atomically . stateTVar resources $ \res@PoolResources{..} -> let inUseTick' = succ inUseTick inUseCount' = succ inUseCount inUse' = IntMap.insert inUseTick newResource inUse in ( inUseTick , res{ inUseCount = inUseCount', inUse = inUse', inUseTick = inUseTick' } ) hookData <- onUse stateInit newResource return (newResource, PoolResourceIdent resTick hookData) releaseResource :: MonadUnliftIO m => Bool -- ^ Destroy resource and don't return to pool? -> Pool' m c' c a -> (a, PoolResourceIdent' c) -> m () releaseResource isLost p@Pool{..} (x, ident) | isLost = do markResourceLost p ident void . forkIO $ destroy x | otherwise = markResourceAvailable p ident markResourceAvailable, markResourceLost :: MonadUnliftIO m => Pool' m c' c a -> PoolResourceIdent' c -> m () markResourceAvailable = returnResource True markResourceLost = returnResource False returnResource :: MonadUnliftIO m => Bool -- ^ return to available -> Pool' m c' c a -> PoolResourceIdent' c -> m () returnResource toAvailable Pool{..} (PoolResourceIdent inUseKey hookData) = do now <- if | toAvailable -> Just <$> currentSecond | otherwise -> return Nothing (toDestroy, released) <- atomically . stateTVar resources $ \res@PoolResources{..} -> case deleteView inUseKey inUse of Nothing -> ((Nothing, Nothing), res) Just (u, us) | NTop (Just availableCount) >= NTop maxAvailable -> ((Just u, Just u),) res { inUse = us , inUseCount = pred inUseCount } Just (u, us) -> ((Nothing, Just u), ) PoolResources { inUse = us , inUseCount = pred inUseCount , availableCount = bool id succ toAvailable availableCount , available = maybe id (IntMap.alter $ Just . (u :) . fromMaybe []) now available , inUseTick } forM_ released $ \u -> onRelease hookData u forM_ toDestroy $ void . forkIO . destroy where deleteView :: Int -> IntMap a -> Maybe (a, IntMap a) deleteView k vs = (, vs') <$> fmap getFirst fv where (vs', fv) = runWriter $ IntMap.alterF (\old -> Nothing <$ tell (First <$> old)) k vs withResource :: forall b m a. MonadUnliftIO m => Pool m a -> (a -> m b) -> m b withResource p = withResource' p () withResource' :: forall b m c' c a. MonadUnliftIO m => Pool' m c' c a -> c' -> (a -> m b) -> m b withResource' p stateInit act = bracketOnError (takeResource' p stateInit) (releaseResource True p) (\x'@(x, _) -> act x <* releaseResource False p x') destroyResources :: MonadUnliftIO m => (a -> Bool) -> Pool' m c' c a -> m () destroyResources p Pool{..} = do toDestroy <- atomically . stateTVar resources $ \res@PoolResources{..} -> let partitioned = partition p <$> available toDel = foldMap fst partitioned toKeep = IntMap.mapMaybe (\(_, toKeep') -> toKeep' <$ guard (not $ null toKeep')) partitioned in (toDel, ) res { availableCount = availableCount - length toDel , available = toKeep } forM_ toDestroy $ void . forkIO . destroy