From 6b8d8263c91650d1fbf07527df31560804cd5bc5 Mon Sep 17 00:00:00 2001 From: Jens Nolte <git@queezle.net> Date: Thu, 10 Feb 2022 20:22:25 +0100 Subject: [PATCH] Implement new resource manager --- src/Quasar/Resources.hs | 157 +++++++++++++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 17 deletions(-) diff --git a/src/Quasar/Resources.hs b/src/Quasar/Resources.hs index bf37097..8683371 100644 --- a/src/Quasar/Resources.hs +++ b/src/Quasar/Resources.hs @@ -1,18 +1,27 @@ module Quasar.Resources ( Resource(..), Disposer, - ResourceManager, dispose, disposeEventuallySTM, disposeEventuallySTM_, isDisposed, newPrimitiveDisposer, + + -- * Resource manager + ResourceManager, + newResourceManagerSTM, + attachResource, ) where import Control.Concurrent (forkIO) import Control.Concurrent.STM +import Control.Monad (foldM) import Control.Monad.Catch +import Data.HashMap.Strict (HashMap) +import Data.HashMap.Strict qualified as HM +import Data.HashSet (HashSet) +import Data.HashSet qualified as HashSet import Quasar.Async.STMHelper import Quasar.Awaitable import Quasar.Exceptions @@ -32,11 +41,6 @@ data Disposer type DisposeFn = IO (Awaitable ()) -newShortDisposer :: TIOWorker -> ExceptionChannel -> IO () -> STM Disposer -newShortDisposer worker exChan disposeFn = newPrimitiveDisposer worker exChan (pure <$> disposeFn) - -newShortSTMDisposer :: TIOWorker -> ExceptionChannel -> STM () -> STM Disposer -newShortSTMDisposer worker exChan disposeFn = newShortDisposer worker exChan (atomically disposeFn) -- TODO document: IO has to be "short" newPrimitiveDisposer :: TIOWorker -> ExceptionChannel -> IO (Awaitable ()) -> STM Disposer @@ -53,7 +57,8 @@ disposeEventuallySTM resource = case getDisposer resource of FnDisposer _ worker exChan state finalizers -> do beginDisposeFnDisposer worker exChan state finalizers - ResourceManagerDisposer resourceManager -> undefined + ResourceManagerDisposer resourceManager -> + beginDisposeResourceManager resourceManager disposeEventuallySTM_ :: Resource r => r -> STM () disposeEventuallySTM_ resource = void $ disposeEventuallySTM resource @@ -62,8 +67,8 @@ disposeEventuallySTM_ resource = void $ disposeEventuallySTM resource isDisposed :: Resource a => a -> Awaitable () isDisposed resource = case getDisposer resource of - FnDisposer _ state _ -> join (toAwaitable state) - ResourceManagerDisposer _resourceManager -> undefined -- resource manager + FnDisposer _ _ _ state _ -> join (toAwaitable state) + ResourceManagerDisposer resourceManager -> resourceManagerIsDisposed resourceManager beginDisposeFnDisposer :: TIOWorker -> ExceptionChannel -> DisposerState -> Finalizers -> STM (Awaitable ()) @@ -89,21 +94,139 @@ beginDisposeFnDisposer worker exChan disposeState finalizers = atomically $ runFinalizers finalizers throwIO $ DisposeException ex +disposerKey :: Disposer -> Unique +disposerKey (FnDisposer key _ _ _ _) = key +disposerKey (ResourceManagerDisposer resourceManager) = resourceManagerKey resourceManager -data ResourceManager = ResourceManager -beginDisposeResourceManager :: ResourceManager -> STM (Awaitable ()) -beginDisposeResourceManager = undefined -- resource manager +disposerFinalizers :: Disposer -> Finalizers +disposerFinalizers (FnDisposer _ _ _ _ finalizers) = finalizers +disposerFinalizers (ResourceManagerDisposer rm) = resourceManagerFinalizers rm -data DisposeResult - = DisposeResultDisposed - | DisposeResultAwait (Awaitable ()) - | DisposeResultResourceManager ResourceManagerResult -data ResourceManagerResult = ResourceManagerResult Unique (Awaitable [ResourceManagerResult]) +data DisposeResult + = DisposeResultAwait (Awaitable ()) + | DisposeResultDependencies DisposeDependencies + +data DisposeDependencies = DisposeDependencies Unique (Awaitable [DisposeDependencies]) + + +-- * Resource manager + +data ResourceManager = ResourceManager { + resourceManagerKey :: Unique, + resourceManagerState :: TVar ResourceManagerState, + resourceManagerFinalizers :: Finalizers +} + +data ResourceManagerState + = ResourceManagerNormal (TVar (HashMap Unique Disposer)) TIOWorker + | ResourceManagerDisposing (Awaitable [DisposeDependencies]) + | ResourceManagerDisposed + + +newResourceManagerSTM :: TIOWorker -> STM ResourceManager +newResourceManagerSTM worker = do + resourceManagerKey <- newUniqueSTM + attachedResources <- newTVar mempty + resourceManagerState <- newTVar (ResourceManagerNormal attachedResources worker) + resourceManagerFinalizers <- newFinalizers + pure ResourceManager { + resourceManagerKey, + resourceManagerState, + resourceManagerFinalizers + } + + +attachResource :: Resource a => ResourceManager -> a -> STM () +attachResource resourceManager resource = + attachDisposer resourceManager (getDisposer resource) + +attachDisposer :: ResourceManager -> Disposer -> STM () +attachDisposer resourceManager disposer = do + readTVar (resourceManagerState resourceManager) >>= \case + ResourceManagerNormal attachedResources _ -> do + alreadyAttached <- isJust . HM.lookup key <$> readTVar attachedResources + unless alreadyAttached do + -- Returns false if the disposer is already finalized + attachedFinalizer <- registerFinalizer (disposerFinalizers disposer) finalizer + when attachedFinalizer $ modifyTVar attachedResources (HM.insert key disposer) + _ -> undefined -- failed to attach resource + where + key :: Unique + key = disposerKey disposer + finalizer :: STM () + finalizer = readTVar (resourceManagerState resourceManager) >>= \case + ResourceManagerNormal attachedResources _ -> modifyTVar attachedResources (HM.delete key) + -- No finalization required in other states, since all resources are disposed soon + -- (and awaiting each resource is cheaper than modifying a HashMap until it is empty). + _ -> pure () + +beginDisposeResourceManager :: ResourceManager -> STM (Awaitable ()) +beginDisposeResourceManager rm = do + void $ beginDisposeResourceManagerInternal rm + pure $ resourceManagerIsDisposed rm + +beginDisposeResourceManagerInternal :: ResourceManager -> STM DisposeDependencies +beginDisposeResourceManagerInternal rm = do + readTVar (resourceManagerState rm) >>= \case + ResourceManagerNormal attachedResources worker -> do + dependenciesVar <- newAsyncVarSTM + writeTVar (resourceManagerState rm) (ResourceManagerDisposing (toAwaitable dependenciesVar)) + attachedDisposers <- HM.elems <$> readTVar attachedResources + startTrivialIO_ worker undefined (void $ forkIO (disposeThread dependenciesVar attachedDisposers)) + pure $ DisposeDependencies rmKey (toAwaitable dependenciesVar) + ResourceManagerDisposing deps -> pure $ DisposeDependencies rmKey deps + ResourceManagerDisposed -> pure $ DisposeDependencies rmKey mempty + where + disposeThread :: AsyncVar [DisposeDependencies] -> [Disposer] -> IO () + disposeThread dependenciesVar attachedDisposers = do + -- Begin to dispose all attached resources + results <- mapM (atomically . resourceManagerBeginDispose) attachedDisposers + -- Await direct resource awaitables and collect indirect dependencies + dependencies <- await (collectDependencies results) + -- Publish "direct dependencies complete"-status + putAsyncVar_ dependenciesVar dependencies + -- Await indirect dependencies + awaitDisposeDependencies $ DisposeDependencies rmKey (pure dependencies) + -- Set state to disposed and run finalizers + atomically do + writeTVar (resourceManagerState rm) ResourceManagerDisposed + runFinalizers (resourceManagerFinalizers rm) + + rmKey :: Unique + rmKey = resourceManagerKey rm + + resourceManagerBeginDispose :: Disposer -> STM DisposeResult + resourceManagerBeginDispose (FnDisposer _ worker exChan state finalizers) = + DisposeResultAwait <$> beginDisposeFnDisposer worker exChan state finalizers + resourceManagerBeginDispose (ResourceManagerDisposer resourceManager) = + DisposeResultDependencies <$> beginDisposeResourceManagerInternal resourceManager + + collectDependencies :: [DisposeResult] -> Awaitable [DisposeDependencies] + collectDependencies (DisposeResultAwait awaitable : xs) = awaitable >> collectDependencies xs + collectDependencies (DisposeResultDependencies deps : xs) = (deps : ) <$> collectDependencies xs + collectDependencies [] = pure [] + + awaitDisposeDependencies :: DisposeDependencies -> IO () + awaitDisposeDependencies = void . go mempty + where + go :: HashSet Unique -> DisposeDependencies -> IO (HashSet Unique) + go keys (DisposeDependencies key deps) + | HashSet.member key keys = pure keys -- loop detection: dependencies were already handled + | otherwise = do + dependencies <- await deps + foldM go (HashSet.insert key keys) dependencies + + +resourceManagerIsDisposed :: ResourceManager -> Awaitable () +resourceManagerIsDisposed rm = unsafeAwaitSTM $ + readTVar (resourceManagerState rm) >>= \case + ResourceManagerDisposed -> pure () + _ -> retry -- * Implementation internals -- GitLab