From f414b5532d0f3c1afa814bdf2bea200b1eaf952d Mon Sep 17 00:00:00 2001 From: Jens Nolte <git@queezle.net> Date: Fri, 25 Feb 2022 18:55:13 +0100 Subject: [PATCH] Remove old resource manager and async modules --- quasar.cabal | 7 +- src/Quasar/Disposable.hs | 272 -------- src/Quasar/Old/Async.hs | 95 --- src/Quasar/Old/UnmanagedAsync.hs | 133 ---- src/Quasar/ResourceManager.hs | 628 ------------------ test/Quasar/AsyncSpec.hs | 12 +- test/Quasar/DisposableSpec.hs | 36 - .../Observable/ObservableHashMapSpec.hs | 18 +- test/Quasar/ObservableSpec.hs | 8 +- test/Quasar/ResourceManagerSpec.hs | 147 ---- test/Quasar/ResourcesSpec.hs | 178 +++++ 11 files changed, 198 insertions(+), 1336 deletions(-) delete mode 100644 src/Quasar/Disposable.hs delete mode 100644 src/Quasar/Old/Async.hs delete mode 100644 src/Quasar/Old/UnmanagedAsync.hs delete mode 100644 src/Quasar/ResourceManager.hs delete mode 100644 test/Quasar/DisposableSpec.hs delete mode 100644 test/Quasar/ResourceManagerSpec.hs create mode 100644 test/Quasar/ResourcesSpec.hs diff --git a/quasar.cabal b/quasar.cabal index 589d624..5ee209b 100644 --- a/quasar.cabal +++ b/quasar.cabal @@ -90,7 +90,6 @@ library Quasar.Async.STMHelper Quasar.Async.V2 Quasar.Awaitable - Quasar.Disposable Quasar.Exceptions Quasar.Exceptions.ExceptionChannel Quasar.Monad @@ -98,11 +97,8 @@ library Quasar.Observable.Delta Quasar.Observable.ObservableHashMap Quasar.Observable.ObservablePriority - Quasar.Old.Async - Quasar.Old.UnmanagedAsync Quasar.Prelude Quasar.PreludeExtras - Quasar.ResourceManager Quasar.Resources Quasar.Timer Quasar.Timer.PosixTimer @@ -130,10 +126,9 @@ test-suite quasar-test other-modules: Quasar.AsyncSpec Quasar.AwaitableSpec - Quasar.DisposableSpec Quasar.ObservableSpec Quasar.Observable.ObservableHashMapSpec Quasar.Observable.ObservablePrioritySpec - Quasar.ResourceManagerSpec + Quasar.ResourcesSpec hs-source-dirs: test diff --git a/src/Quasar/Disposable.hs b/src/Quasar/Disposable.hs deleted file mode 100644 index d232f2d..0000000 --- a/src/Quasar/Disposable.hs +++ /dev/null @@ -1,272 +0,0 @@ -module Quasar.Disposable ( - -- * Disposable - IsDisposable(..), - Disposable, - dispose, - disposeEventually, - disposeEventually_, - - newDisposable, - noDisposable, - - -- ** Async Disposable - newAsyncDisposable, - - -- ** STM disposable - STMDisposable, - newSTMDisposable, - newSTMDisposable', - disposeSTMDisposable, - - -- * Implementation internals - DisposeResult(..), - ResourceManagerResult(..), - DisposableFinalizers, - newDisposableFinalizers, - newDisposableFinalizersSTM, - defaultRegisterFinalizer, - defaultRunFinalizers, - awaitResourceManagerResult, -) where - -import Control.Concurrent (forkIO) -import Control.Concurrent.STM -import Control.Monad.Catch -import Control.Monad.Reader -import Data.HashSet (HashSet) -import Data.HashSet qualified as HashSet -import GHC.Conc (unsafeIOToSTM) -import Quasar.Awaitable -import Quasar.Prelude - - --- * Disposable - -class IsDisposable a where - -- | Convert an `IsDisposable`-Object to a `Disposable`. - -- - -- When implementing the `IsDisposable`-class this can be used to defer the dispose behavior to a disposable created - -- by e.g. `newDisposable`. - toDisposable :: a -> Disposable - toDisposable = Disposable - - -- | Begin to dispose (/release) resource(s). - -- - -- The implementation has to be idempotent, i.e. calling `beginDispose` once or multiple times should have the same - -- effect. - -- - -- `beginDispose` must be called in masked state. - -- - -- `beginDispose` must not block for an unbounded time. - -- - -- TODO document finalizers (finalizers also have to run when an exception is thrown) - beginDispose :: a -> IO DisposeResult - beginDispose = beginDispose . toDisposable - - isDisposed :: a -> Awaitable () - isDisposed = isDisposed . toDisposable - - -- | Finalizers MUST NOT throw exceptions. - -- - -- The boolean returned by register finalizer indicates if the operation was successful. - registerFinalizer :: a -> STM () -> STM Bool - registerFinalizer = registerFinalizer . toDisposable - - {-# MINIMAL toDisposable | (beginDispose, isDisposed, registerFinalizer) #-} - -dispose :: MonadIO m => IsDisposable a => a -> m () -dispose disposable = liftIO do - uninterruptibleMask_ (beginDispose disposable) >>= \case - DisposeResultDisposed -> pure () - (DisposeResultAwait awaitable) -> await awaitable - (DisposeResultResourceManager result) -> awaitResourceManagerResult result - --- | Begin to dispose a resource. -disposeEventually :: (IsDisposable a, MonadIO m) => a -> m (Awaitable ()) -disposeEventually disposable = do - disposeEventually_ disposable - pure $ isDisposed disposable - --- | Begin to dispose a resource. -disposeEventually_ :: (IsDisposable a, MonadIO m) => a -> m () -disposeEventually_ disposable = liftIO do - uninterruptibleMask_ $ void $ beginDispose disposable - -awaitResourceManagerResult :: forall m. MonadAwait m => ResourceManagerResult -> m () -awaitResourceManagerResult = void . go mempty - where - go :: HashSet Unique -> ResourceManagerResult -> m (HashSet Unique) - go keys (ResourceManagerResult key awaitable) - | HashSet.member key keys = pure keys -- resource manager was encountered before - | otherwise = do - dependencies <- await awaitable - foldM go (HashSet.insert key keys) dependencies - - -data DisposeResult - = DisposeResultDisposed - | DisposeResultAwait (Awaitable ()) - | DisposeResultResourceManager ResourceManagerResult - -data ResourceManagerResult = ResourceManagerResult Unique (Awaitable [ResourceManagerResult]) - - -instance IsDisposable a => IsDisposable (Maybe a) where - toDisposable = maybe noDisposable toDisposable - - -data Disposable = forall a. IsDisposable a => Disposable a - -instance IsDisposable Disposable where - beginDispose (Disposable x) = beginDispose x - isDisposed (Disposable x) = isDisposed x - registerFinalizer (Disposable x) = registerFinalizer x - toDisposable = id - -instance IsAwaitable () Disposable where - toAwaitable = isDisposed - - - -data IODisposable = IODisposable Unique (TMVar (IO ())) DisposableFinalizers (AsyncVar ()) - -instance IsDisposable IODisposable where - beginDispose (IODisposable key actionVar finalizers resultVar) = do - -- This is only safe when run in masked state - atomically (tryTakeTMVar actionVar) >>= mapM_ \action -> do - result <- try action - atomically do - putAsyncVarEitherSTM_ resultVar result - defaultRunFinalizers finalizers - -- Await so concurrent `beginDispose` calls don't exit too early - await resultVar - pure DisposeResultDisposed - - isDisposed (IODisposable _ _ _ resultVar) = toAwaitable resultVar `catchAll` \_ -> pure () - - registerFinalizer (IODisposable _ _ finalizers _) = defaultRegisterFinalizer finalizers - - --- | Create a new disposable from an IO action. Is is guaranteed, that the IO action will only be called once (even when --- `dispose` is called multiple times). --- --- The action must not block for an unbound time. -newDisposable :: IO () -> STM Disposable -newDisposable disposeAction = do - key <- newUniqueSTM - fmap toDisposable $ IODisposable key <$> newTMVar disposeAction <*> newDisposableFinalizersSTM <*> newAsyncVarSTM - - -data AsyncDisposable = AsyncDisposable Unique (TMVar (IO ())) DisposableFinalizers (AsyncVar ()) - -instance IsDisposable AsyncDisposable where - beginDispose (AsyncDisposable key actionVar finalizers resultVar) = do - -- This is only safe when run in masked state - atomically (tryTakeTMVar actionVar) >>= mapM_ \action -> do - void $ forkIO do - result <- try action - atomically do - putAsyncVarEitherSTM_ resultVar result - defaultRunFinalizers finalizers - pure $ DisposeResultAwait $ await resultVar - - isDisposed (AsyncDisposable _ _ _ resultVar) = toAwaitable resultVar `catchAll` \_ -> pure () - - registerFinalizer (AsyncDisposable _ _ finalizers _) = defaultRegisterFinalizer finalizers - --- | Create a new disposable from an IO action. The action will be run asynchrously. Is is guaranteed, that the IO --- action will only be called once (even when `dispose` is called multiple times). --- --- The action must not block for an unbound time. -newAsyncDisposable :: IO () -> STM Disposable -newAsyncDisposable disposeAction = do - key <- newUniqueSTM - fmap toDisposable $ AsyncDisposable key <$> newTMVar disposeAction <*> newDisposableFinalizersSTM <*> newAsyncVarSTM - - - -data STMDisposable = STMDisposable Unique (TMVar (STM ())) DisposableFinalizers (AsyncVar ()) - -instance IsDisposable STMDisposable where - beginDispose (STMDisposable key actionVar finalizers resultVar) = do - -- This is only safe when run in masked state - atomically (tryTakeTMVar actionVar) >>= mapM_ \action -> do - atomically do - result <- try action - putAsyncVarEitherSTM_ resultVar result - defaultRunFinalizers finalizers - -- Await so concurrent `beginDispose` calls don't exit too early - await resultVar - pure DisposeResultDisposed - - isDisposed (STMDisposable _ _ _ resultVar) = toAwaitable resultVar `catchAll` \_ -> pure () - - registerFinalizer (STMDisposable _ _ finalizers _) = defaultRegisterFinalizer finalizers - --- | Create a new disposable from an STM action. Is is guaranteed, that the STM action will only be called once (even --- when `dispose` is called multiple times). --- --- The action must not block (retry) for an unbound time. -newSTMDisposable :: STM () -> STM Disposable -newSTMDisposable disposeAction = toDisposable <$> newSTMDisposable' disposeAction - --- | Create a new disposable from an STM action. Is is guaranteed, that the STM action will only be called once (even --- when `dispose` is called multiple times). --- --- The action must not block (retry) for an unbound time. --- --- This variant of `newSTMDisposable` returns an unboxed `STMDisposable` which can be disposed from `STM` by using --- `disposeSTMDisposable`. -newSTMDisposable' :: STM () -> STM STMDisposable -newSTMDisposable' disposeAction = do - key <- unsafeIOToSTM newUnique - STMDisposable key <$> newTMVar disposeAction <*> newDisposableFinalizersSTM <*> newAsyncVarSTM - -disposeSTMDisposable :: STMDisposable -> STM () -disposeSTMDisposable (STMDisposable key actionVar finalizers resultVar) = do - tryTakeTMVar actionVar >>= \case - Just action -> do - result <- try action - putAsyncVarEitherSTM_ resultVar result - defaultRunFinalizers finalizers - Nothing -> readAsyncVarSTM resultVar - - -data EmptyDisposable = EmptyDisposable - -instance IsDisposable EmptyDisposable where - beginDispose EmptyDisposable = pure DisposeResultDisposed - isDisposed _ = pure () - registerFinalizer _ _ = pure False - - - --- | A `Disposable` for which `dispose` is a no-op and which reports as already disposed. -noDisposable :: Disposable -noDisposable = toDisposable EmptyDisposable - - - --- * Implementation internals - -newtype DisposableFinalizers = DisposableFinalizers (TMVar [STM ()]) - -newDisposableFinalizers :: IO DisposableFinalizers -newDisposableFinalizers = DisposableFinalizers <$> newTMVarIO [] - -newDisposableFinalizersSTM :: STM DisposableFinalizers -newDisposableFinalizersSTM = DisposableFinalizers <$> newTMVar [] - -defaultRegisterFinalizer :: DisposableFinalizers -> STM () -> STM Bool -defaultRegisterFinalizer (DisposableFinalizers finalizerVar) finalizer = - tryTakeTMVar finalizerVar >>= \case - Just finalizers -> do - putTMVar finalizerVar (finalizer : finalizers) - pure True - Nothing -> pure False - -defaultRunFinalizers :: DisposableFinalizers -> STM () -defaultRunFinalizers (DisposableFinalizers finalizerVar) = do - tryTakeTMVar finalizerVar >>= \case - Just finalizers -> sequence_ finalizers - Nothing -> throwM $ userError "defaultRunFinalizers was called multiple times (it must only be run once)" diff --git a/src/Quasar/Old/Async.hs b/src/Quasar/Old/Async.hs deleted file mode 100644 index e93e95f..0000000 --- a/src/Quasar/Old/Async.hs +++ /dev/null @@ -1,95 +0,0 @@ -module Quasar.Old.Async ( - -- * Async - Async, - async, - async_, - asyncWithUnmask, - asyncWithUnmask_, - withAsync, - - -- ** Async with explicit error handling - asyncWithHandler, - asyncWithHandler_, - asyncWithHandlerAndUnmask, - asyncWithHandlerAndUnmask_, - - -- ** Async exceptions - CancelAsync(..), - AsyncDisposed(..), - AsyncException(..), - isCancelAsync, - isAsyncDisposed, -) where - -import Control.Monad.Catch -import Control.Monad.Reader -import Quasar.Disposable -import Quasar.Old.UnmanagedAsync -import Quasar.Prelude -import Quasar.ResourceManager - --- | TODO: Documentation --- --- The action will be run with asynchronous exceptions unmasked. -async :: (MonadResourceManager m, MonadIO m, MonadMask m) => ResourceManagerIO a -> m (Async a) -async action = asyncWithUnmask \unmask -> unmask action - --- | TODO: Documentation --- --- The action will be run with asynchronous exceptions masked and will be passed an action that can be used to unmask. -asyncWithUnmask - :: (MonadResourceManager m, MonadIO m, MonadMask m) - => ((ResourceManagerIO a -> ResourceManagerIO a) - -> ResourceManagerIO r) - -> m (Async r) -asyncWithUnmask action = do - resourceManager <- askResourceManager - asyncWithHandlerAndUnmask (onResourceManager resourceManager . throwToResourceManager . AsyncException) action - -async_ :: (MonadResourceManager m, MonadIO m, MonadMask m) => ResourceManagerIO () -> m () -async_ action = void $ async action - -asyncWithUnmask_ :: (MonadResourceManager m, MonadIO m, MonadMask m) => ((ResourceManagerIO a -> ResourceManagerIO a) -> ResourceManagerIO ()) -> m () -asyncWithUnmask_ action = void $ asyncWithUnmask action - --- | TODO: Documentation --- --- The action will be run with asynchronous exceptions unmasked. When an exception is thrown that is not caused from --- the disposable instance (i.e. the task being canceled), the handler is called with that exception. -asyncWithHandlerAndUnmask - :: (MonadResourceManager m, MonadIO m, MonadMask m) - => (SomeException -> IO ()) - -> ((ResourceManagerIO a -> ResourceManagerIO a) - -> ResourceManagerIO r) - -> m (Async r) -asyncWithHandlerAndUnmask handler action = do - resourceManager <- askResourceManager - registerNewResource do - unmanagedAsyncWithHandlerAndUnmask wrappedHandler \unmask -> - onResourceManager resourceManager (action (liftUnmask unmask)) - where - wrappedHandler :: SomeException -> IO () - wrappedHandler (fromException -> Just AsyncDisposed) = pure () - wrappedHandler ex = handler ex - liftUnmask :: (forall b. IO b -> IO b) -> ResourceManagerIO a -> ResourceManagerIO a - liftUnmask unmask innerAction = do - resourceManager <- askResourceManager - liftIO $ unmask $ onResourceManager resourceManager innerAction - -asyncWithHandlerAndUnmask_ - :: (MonadResourceManager m, MonadIO m, MonadMask m) - => (SomeException -> IO ()) - -> ((ResourceManagerIO a -> ResourceManagerIO a) - -> ResourceManagerIO r) - -> m () -asyncWithHandlerAndUnmask_ handler action = void $ asyncWithHandlerAndUnmask handler action - -asyncWithHandler :: (MonadResourceManager m, MonadIO m, MonadMask m) => (SomeException -> IO ()) -> ResourceManagerIO r -> m (Async r) -asyncWithHandler handler action = asyncWithHandlerAndUnmask handler \unmask -> unmask action - -asyncWithHandler_ :: (MonadResourceManager m, MonadIO m, MonadMask m) => (SomeException -> IO ()) -> ResourceManagerIO r -> m () -asyncWithHandler_ handler action = void $ asyncWithHandler handler action - - -withAsync :: (MonadResourceManager m, MonadIO m, MonadMask m) => ResourceManagerIO r -> (Async r -> m a) -> m a -withAsync action = bracket (async action) dispose diff --git a/src/Quasar/Old/UnmanagedAsync.hs b/src/Quasar/Old/UnmanagedAsync.hs deleted file mode 100644 index 1b1254c..0000000 --- a/src/Quasar/Old/UnmanagedAsync.hs +++ /dev/null @@ -1,133 +0,0 @@ -module Quasar.Old.UnmanagedAsync ( - -- ** Unmanaged variant - Async, - unmanagedAsync, - unmanagedAsyncWithHandler, - unmanagedAsyncWithUnmask, - unmanagedAsyncWithHandlerAndUnmask, - - -- ** Async exceptions - CancelAsync(..), - AsyncDisposed(..), - AsyncException(..), - isCancelAsync, - isAsyncDisposed, -) where - - -import Control.Concurrent (ThreadId, forkIO, forkIOWithUnmask, throwTo) -import Control.Concurrent.STM -import Control.Monad.Catch -import Quasar.Exceptions -import Quasar.Awaitable -import Quasar.Disposable -import Quasar.Prelude - - --- | A async is an asynchronously running computation that can be cancelled. --- --- The result (or exception) can be aquired by using the `IsAwaitable` class (e.g. by calling `await` or `awaitIO`). --- It is possible to cancel the async by using `dispose` if the operation has not been completed. -data Async r = Async Unique (TVar AsyncState) DisposableFinalizers (Awaitable r) - -data AsyncState = AsyncStateInitializing | AsyncStateRunning ThreadId | AsyncStateThrowing | AsyncStateCompleted - -instance IsAwaitable r (Async r) where - toAwaitable (Async _ _ _ resultAwaitable) = resultAwaitable - -instance IsDisposable (Async r) where - beginDispose self@(Async key stateVar _ _) = uninterruptibleMask_ do - join $ atomically do - readTVar stateVar >>= \case - AsyncStateInitializing -> unreachableCodePathM - AsyncStateRunning threadId -> do - writeTVar stateVar AsyncStateThrowing - -- Fork to prevent synchronous exceptions when disposing this thread, and to prevent blocking when disposing - -- a thread that is running in uninterruptible masked state. - pure $ void $ forkIO do - throwTo threadId $ CancelAsync key - atomically $ writeTVar stateVar AsyncStateCompleted - AsyncStateThrowing -> pure $ pure @IO () - AsyncStateCompleted -> pure $ pure @IO () - - -- Wait for async completion or failure. Asyncs must not ignore `CancelAsync` or this will hang. - pure $ DisposeResultAwait $ isDisposed self - - isDisposed (Async _ _ _ resultAwaitable) = awaitSuccessOrFailure resultAwaitable - - registerFinalizer (Async _ _ finalizers _) = defaultRegisterFinalizer finalizers - -instance Functor Async where - fmap fn (Async key actionVar finalizerVar resultAwaitable) = Async key actionVar finalizerVar (fn <$> resultAwaitable) - - - --- | Base implementation for the `unmanagedAsync`- and `Quasar.Async.async`-class of functions. -unmanagedAsyncWithHandlerAndUnmask :: MonadIO m => (SomeException -> IO ()) -> ((forall b. IO b -> IO b) -> IO a) -> m (Async a) -unmanagedAsyncWithHandlerAndUnmask handler action = do - liftIO $ mask_ do - key <- newUnique - resultVar <- newAsyncVar - stateVar <- newTVarIO AsyncStateInitializing - finalizers <- newDisposableFinalizers - - threadId <- forkIOWithUnmask \unmask -> - handleAll - do \ex -> fail $ "coreAsyncImplementation thread failed: " <> displayException ex - do - result <- try $ catchAll - do action unmask - \ex -> do - -- Rewrite exception if its the cancel exception for this async - when (fromException ex == Just (CancelAsync key)) $ throwIO AsyncDisposed - throwIO $ AsyncException ex - - -- The `action` has completed its work. - -- "disarm" dispose: - catchIf - do \(CancelAsync exKey) -> key == exKey - do - atomically $ readTVar stateVar >>= \case - AsyncStateInitializing -> retry - AsyncStateRunning _ -> writeTVar stateVar AsyncStateCompleted - AsyncStateThrowing -> retry -- Could not disarm so we have to wait for the exception to arrive - AsyncStateCompleted -> pure () - do mempty -- ignore exception if it matches; this can only happen once (see AsyncStateThrowing above) - - catchAll - case result of - Left (fromException -> Just AsyncDisposed) -> pure () - Left (fromException -> Just (AsyncException ex)) -> handler ex - -- Impossible code path reached - Left ex -> traceIO $ "Error in unmanagedAsyncWithHandlerAndUnmask: " <> displayException ex - _ -> pure () - \ex -> traceIO $ "An exception was thrown while handling an async exception: " <> displayException ex - - atomically do - putAsyncVarEitherSTM_ resultVar result - defaultRunFinalizers finalizers - - atomically $ writeTVar stateVar $ AsyncStateRunning threadId - - pure $ Async key stateVar finalizers (toAwaitable resultVar) - - -unmanagedAsync :: MonadIO m => IO a -> m (Async a) -unmanagedAsync action = unmanagedAsyncWithUnmask \unmask -> unmask action - -unmanagedAsyncWithHandler :: MonadIO m => (SomeException -> IO ()) -> IO a -> m (Async a) -unmanagedAsyncWithHandler handler action = unmanagedAsyncWithHandlerAndUnmask handler \unmask -> unmask action - -unmanagedAsyncWithUnmask :: MonadIO m => ((forall b. IO b -> IO b) -> IO a) -> m (Async a) -unmanagedAsyncWithUnmask = unmanagedAsyncWithHandlerAndUnmask (traceIO . ("Unhandled exception in unmanaged async: " <>) . displayException) - - --- | Run a computation concurrently to another computation. When the current thread leaves `withAsync`, the async --- computation is cancelled. --- --- While the async is disposed when `withUnmanagedAsync` exits, an exception would be ignored if the action fails. This --- behavior is similar to the @withAsync@ function from the @async@ package. --- --- For an exception-safe version, see `Quasar.Async.withAsync`. -withUnmanagedAsync :: (MonadIO m, MonadMask m) => IO r -> (Async r -> m a) -> m a -withUnmanagedAsync action = bracket (unmanagedAsync action) dispose diff --git a/src/Quasar/ResourceManager.hs b/src/Quasar/ResourceManager.hs deleted file mode 100644 index 7bf218d..0000000 --- a/src/Quasar/ResourceManager.hs +++ /dev/null @@ -1,628 +0,0 @@ -module Quasar.ResourceManager ( - -- * MonadResourceManager - MonadResourceManager(..), - ResourceManagerT, - ResourceManagerIO, - ResourceManagerSTM, - FailedToRegisterResource, - attachDisposable, - registerNewResource, - registerNewResource_, - registerDisposable, - registerDisposeAction, - registerAsyncDisposeAction, - throwToResourceManager, - withScopedResourceManager, - onResourceManager, - onResourceManagerSTM, - captureDisposable, - captureDisposable_, - disposeOnError, - liftResourceManagerIO, - runInResourceManagerSTM, - enterResourceManager, - enterResourceManagerSTM, - newUniqueRM, - - -- ** Top level initialization - withRootResourceManager, - - -- ** ResourceManager - IsResourceManager(..), - ResourceManager, - newResourceManager, - newResourceManagerSTM, - attachDisposeAction, - attachDisposeAction_, - - -- ** Linking computations to a resource manager - linkExecution, - CancelLinkedExecution, - - -- * Reexports - CombinedException, - combinedExceptions, -) where - - -import Control.Concurrent (ThreadId, forkIO, myThreadId, throwTo, threadDelay) -import Control.Concurrent.STM -import Control.Monad.Catch -import Control.Monad.Reader -import Data.Foldable (toList) -import Data.HashMap.Strict (HashMap) -import Data.HashMap.Strict qualified as HM -import Data.List.NonEmpty ((<|), nonEmpty) -import Data.Sequence (Seq(..), (|>)) -import Data.Sequence qualified as Seq -import Quasar.Awaitable -import Quasar.Disposable -import Quasar.Exceptions -import Quasar.Old.UnmanagedAsync -import Quasar.Prelude -import Quasar.Utils.Exceptions - - -data FailedToRegisterResource = FailedToRegisterResource - deriving stock (Eq, Show) - -instance Exception FailedToRegisterResource where - displayException FailedToRegisterResource = - "FailedToRegisterResource: Failed to register a resource to a resource manager. This might result in leaked resources if left unhandled." - -data FailedToLockResourceManager = FailedToLockResourceManager - deriving stock (Eq, Show) - -instance Exception FailedToLockResourceManager where - displayException FailedToLockResourceManager = - "FailedToLockResourceManager: Failed to lock a resource manager." - --- TODO HasResourceManager, getResourceManager -class IsDisposable a => IsResourceManager a where - toResourceManager :: a -> ResourceManager - - -class MonadFix m => MonadResourceManager m where - -- | Get the underlying resource manager. - askResourceManager :: m ResourceManager - - -- | Replace the resource manager for a computation. - localResourceManager :: IsResourceManager a => a -> m r -> m r - - -- | Locks the resource manager. As long as the resource manager is locked, it's possible to register new resources - -- on the resource manager. - -- - -- This prevents the resource manager from disposing, so the computation must not block for an unbound amount of time. - lockResourceManager :: MonadResourceManager m => m a -> m a - - -- | Run an `STM` computation. Depending on the monad this may be run in a dedicated STM transaction or may be - -- embedded in a larger transaction. - runInSTM :: MonadResourceManager m => STM a -> m a - - maskIfRequired :: MonadResourceManager m => m a -> m a - - --- | Forward an exception that happened asynchronously. -throwToResourceManager :: (Exception e, MonadResourceManager m) => e -> m () -throwToResourceManager ex = do - resourceManager <- askResourceManager - runInSTM $ throwToResourceManagerImpl resourceManager (toException ex) - - -runInResourceManagerSTM :: MonadResourceManager m => ResourceManagerSTM a -> m a -runInResourceManagerSTM action = do - resourceManager <- askResourceManager - runInSTM $ runReaderT action resourceManager - --- | Register a `Disposable` to the resource manager. --- --- May throw an `FailedToRegisterResource` if the resource manager is disposing/disposed. -registerDisposable :: (IsDisposable a, MonadResourceManager m) => a -> m () -registerDisposable disposable = do - resourceManager <- askResourceManager - runInSTM $ attachDisposable resourceManager disposable - - -registerDisposeAction :: MonadResourceManager m => IO () -> m () -registerDisposeAction disposeAction = runInResourceManagerSTM do - disposable <- lift (newDisposable disposeAction) - registerDisposable disposable - -registerAsyncDisposeAction :: MonadResourceManager m => IO () -> m () -registerAsyncDisposeAction disposeAction = runInResourceManagerSTM do - disposable <- lift (newAsyncDisposable disposeAction) - registerDisposable disposable - --- | Locks the resource manager (which may fail), runs the computation and registeres the resulting disposable. --- --- The computation will be run in masked state (if not running atomically in `STM`). --- --- The computation must not block for an unbound amount of time. -registerNewResource :: (IsDisposable a, MonadResourceManager m) => m a -> m a -registerNewResource action = maskIfRequired $ lockResourceManager do - resource <- action - registerDisposable resource - pure resource - -registerNewResource_ :: (IsDisposable a, MonadResourceManager m) => m a -> m () -registerNewResource_ action = void $ registerNewResource action - -withScopedResourceManager :: (MonadResourceManager m, MonadIO m, MonadMask m) => m a -> m a -withScopedResourceManager action = - bracket newResourceManager dispose \scope -> localResourceManager scope action - - -type ResourceManagerT = ReaderT ResourceManager -type ResourceManagerIO = ResourceManagerT IO -type ResourceManagerSTM = ResourceManagerT STM - -instance (MonadAwait m, MonadMask m, MonadIO m, MonadFix m) => MonadResourceManager (ResourceManagerT m) where - localResourceManager resourceManager = local (const (toResourceManager resourceManager)) - - askResourceManager = ask - - lockResourceManager action = do - resourceManager <- askResourceManager - lockResourceManagerImpl resourceManager action - - runInSTM action = liftIO $ atomically action - - maskIfRequired = mask_ - - --- Overlaps the ResourceManagerT/MonadIO-instance, because `MonadIO` _could_ be specified for `STM` (but that would be --- _very_ incorrect, so this is safe). -instance {-# OVERLAPS #-} MonadResourceManager (ResourceManagerT STM) where - localResourceManager resourceManager = local (const (toResourceManager resourceManager)) - - askResourceManager = ask - - -- | No-op, since STM is always executed atomically. - lockResourceManager = id - - runInSTM action = lift action - - maskIfRequired = id - - -instance {-# OVERLAPPABLE #-} MonadResourceManager m => MonadResourceManager (ReaderT r m) where - askResourceManager = lift askResourceManager - - localResourceManager resourceManager action = do - x <- ask - lift $ localResourceManager resourceManager $ runReaderT action x - - lockResourceManager action = do - x <- ask - lift $ lockResourceManager $ runReaderT action x - - runInSTM action = lift $ runInSTM action - - maskIfRequired action = do - x <- ask - lift $ maskIfRequired $ runReaderT action x - --- TODO MonadResourceManager instances for StateT, WriterT, RWST, MaybeT, ... - - -onResourceManager :: (IsResourceManager a, MonadIO m) => a -> ResourceManagerIO r -> m r -onResourceManager target action = liftIO $ runReaderT action (toResourceManager target) - -onResourceManagerSTM :: (IsResourceManager a) => a -> ResourceManagerSTM r -> STM r -onResourceManagerSTM target action = runReaderT action (toResourceManager target) - -liftResourceManagerIO :: (MonadResourceManager m, MonadIO m) => ResourceManagerIO r -> m r -liftResourceManagerIO action = do - resourceManager <- askResourceManager - onResourceManager resourceManager action - - -captureDisposable :: MonadResourceManager m => m a -> m (a, Disposable) -captureDisposable action = do - resourceManager <- newResourceManager - result <- localResourceManager resourceManager action - pure (result, toDisposable resourceManager) - -captureDisposable_ :: MonadResourceManager m => m () -> m Disposable -captureDisposable_ = snd <<$>> captureDisposable - --- | Disposes all resources created by the computation if the computation throws an exception. -disposeOnError :: (MonadResourceManager m, MonadIO m, MonadMask m) => m a -> m a -disposeOnError action = do - bracketOnError - newResourceManager - dispose - \resourceManager -> localResourceManager resourceManager action - --- | Run a computation on a resource manager and throw any exception that occurs to the resource manager. --- --- This can be used to run e.g. callbacks that belong to a different resource context. --- --- Locks the resource manager, so the computation must not block for an unbounded time. --- --- May throw an exception when the resource manager is disposing. -enterResourceManager :: MonadIO m => ResourceManager -> ResourceManagerIO () -> m () -enterResourceManager resourceManager action = liftIO do - onResourceManager resourceManager $ lockResourceManager do - action `catchAll` \ex -> throwToResourceManager ex - --- | Run a computation on a resource manager and throw any exception that occurs to the resource manager. --- --- This can be used to run e.g. callbacks that belong to a different resource context. -enterResourceManagerSTM :: ResourceManager -> ResourceManagerSTM () -> STM () -enterResourceManagerSTM resourceManager action = do - onResourceManagerSTM resourceManager do - action `catchAll` \ex -> throwToResourceManager ex - - --- | Create a new `Unique` in a `MonadResourceManager` monad. -newUniqueRM :: MonadResourceManager m => m Unique -newUniqueRM = runInSTM newUniqueSTM - - - --- * Resource manager implementations - --- ** Root resource manager - -data ResourceManager - = NormalResourceManager ResourceManagerCore ResourceManager - | RootResourceManager ResourceManagerCore (TVar Bool) (TMVar (Seq SomeException)) (AsyncVar [SomeException]) - - -instance IsResourceManager ResourceManager where - toResourceManager = id - -resourceManagerCore :: ResourceManager -> ResourceManagerCore -resourceManagerCore (RootResourceManager core _ _ _) = core -resourceManagerCore (NormalResourceManager core _) = core - -throwToResourceManagerImpl :: ResourceManager -> SomeException -> STM () -throwToResourceManagerImpl (NormalResourceManager _ exceptionManager) ex = throwToResourceManagerImpl exceptionManager ex -throwToResourceManagerImpl (RootResourceManager _ _ exceptionsVar _) ex = do - tryTakeTMVar exceptionsVar >>= \case - Just exceptions -> do - putTMVar exceptionsVar (exceptions |> toException ex) - Nothing -> do - throwM $ userError "Could not throw to resource manager: RootResourceManager is already disposed" - - - -instance IsDisposable ResourceManager where - beginDispose (NormalResourceManager core _) = beginDispose core - beginDispose (RootResourceManager internal disposingVar _ _) = do - defaultResourceManagerDisposeResult internal <$ atomically do - disposing <- readTVar disposingVar - unless disposing $ writeTVar disposingVar True - - isDisposed (resourceManagerCore -> core) = isDisposed core - - registerFinalizer (resourceManagerCore -> core) = registerFinalizer core - -newUnmanagedRootResourceManagerInternal :: MonadIO m => m ResourceManager -newUnmanagedRootResourceManagerInternal = liftIO do - disposingVar <- newTVarIO False - exceptionsVar <- newTMVarIO Empty - finalExceptionsVar <- newAsyncVar - mfix \root -> do - void $ forkIO (disposeWorker root) - internal <- atomically $ newUnmanagedDefaultResourceManagerInternal (throwToResourceManagerImpl root) - pure $ RootResourceManager internal disposingVar exceptionsVar finalExceptionsVar - where - disposeWorker :: ResourceManager -> IO () - disposeWorker (NormalResourceManager _ _) = unreachableCodePathM - disposeWorker (RootResourceManager internal disposingVar exceptionsVar finalExceptionsVar) = - handleAll - do \ex -> fail $ "RootResourceManager thread failed unexpectedly: " <> displayException ex - do - -- Wait until disposing - atomically do - disposing <- readTVar disposingVar - hasExceptions <- not . Seq.null <$> readTMVar exceptionsVar - check $ disposing || hasExceptions - - -- Start a thread to report exceptions (or a potential hang) after a timeout - reportThread <- unmanagedAsync reportTimeout - - -- Dispose resources - dispose internal - - atomically do - -- The var is set to `Nothing` to signal that no more exceptions can be received - exceptions <- takeTMVar exceptionsVar - - putAsyncVarSTM_ finalExceptionsVar $ toList exceptions - - -- Clean up timeout/report thread - dispose reportThread - - where - timeoutSeconds :: Int - timeoutSeconds = 5 - timeoutMicroseconds :: Int - timeoutMicroseconds = timeoutSeconds * 1_000_000 - - reportTimeout :: IO () - reportTimeout = do - threadDelay timeoutMicroseconds - atomically (tryReadTMVar exceptionsVar) >>= \case - Nothing -> pure () -- Terminate - Just Empty -> do - traceIO $ mconcat ["Root resource manager did not dispose within ", show timeoutSeconds, " seconds"] - reportExceptions 0 Empty - Just exs -> do - traceIO $ mconcat [ "Root resource manager did not dispose within ", - show timeoutSeconds, " seconds (", show (length exs), " exception(s) queued)" ] - reportExceptions 0 exs - - reportExceptions :: Int -> Seq SomeException -> IO () - reportExceptions alreadyReported Empty = join $ atomically do - Seq.drop alreadyReported <<$>> tryReadTMVar exceptionsVar >>= \case - Nothing -> pure $ pure () -- Terminate - Just Empty -> retry - Just exs -> pure $ reportExceptions alreadyReported exs - reportExceptions alreadyReported (ex :<| exs) = do - traceIO $ "Exception thrown to blocked root resource manager: " <> displayException ex - reportExceptions (alreadyReported + 1) exs - - -withRootResourceManager :: MonadIO m => ResourceManagerIO a -> m a -withRootResourceManager action = liftIO $ uninterruptibleMask \unmask -> do - resourceManager@(RootResourceManager _ _ _ finalExceptionsVar) <- newUnmanagedRootResourceManagerInternal - - result <- try $ unmask $ onResourceManager resourceManager action - - disposeEventually_ resourceManager - exceptions <- await finalExceptionsVar - - case result of - Left (ex :: SomeException) -> maybe (throwM ex) (throwM . CombinedException . (ex <|)) (nonEmpty exceptions) - Right result' -> maybe (pure result') (throwM . CombinedException) $ nonEmpty exceptions - - --- ** Default resource manager - -data ResourceManagerCore = ResourceManagerCore { - resourceManagerKey :: Unique, - throwToHandler :: SomeException -> STM (), - stateVar :: TVar ResourceManagerState, - disposablesVar :: TMVar (HashMap Unique Disposable), - lockVar :: TVar Word64, - resultVar :: AsyncVar (Awaitable [ResourceManagerResult]), - finalizers :: DisposableFinalizers -} - -data ResourceManagerState - = ResourceManagerNormal - | ResourceManagerDisposing - | ResourceManagerDisposed - - --- | Attaches an `Disposable` to a ResourceManager. It will automatically be disposed when the resource manager is disposed. --- --- May throw an `FailedToRegisterResource` if the resource manager is disposing/disposed. -attachDisposable :: (IsResourceManager a, IsDisposable b) => a -> b -> STM () -attachDisposable rm disposable = attachDisposableImpl (resourceManagerCore (toResourceManager rm)) (toDisposable disposable) - -attachDisposableImpl :: ResourceManagerCore -> Disposable -> STM () -attachDisposableImpl ResourceManagerCore{stateVar, disposablesVar} disposable = do - key <- newUniqueSTM - state <- readTVar stateVar - case state of - ResourceManagerNormal -> do - disposables <- takeTMVar disposablesVar - putTMVar disposablesVar (HM.insert key disposable disposables) - void $ registerFinalizer disposable (finalizer key) - _ -> throwM FailedToRegisterResource - where - finalizer :: Unique -> STM () - finalizer key = - tryTakeTMVar disposablesVar >>= \case - Just disposables -> - putTMVar disposablesVar $ HM.delete key disposables - Nothing -> pure () - -lockResourceManagerImpl :: (MonadIO m, MonadMask m) => ResourceManager -> m b -> m b -lockResourceManagerImpl (resourceManagerCore -> ResourceManagerCore{stateVar, lockVar}) = - bracket_ (liftIO aquire) (liftIO release) - where - aquire :: IO () - aquire = atomically do - readTVar stateVar >>= \case - ResourceManagerNormal -> pure () - _ -> throwM FailedToLockResourceManager - modifyTVar lockVar (+ 1) - release :: IO () - release = atomically (modifyTVar lockVar (\x -> x - 1)) - -instance IsDisposable ResourceManagerCore where - beginDispose self@ResourceManagerCore{resourceManagerKey, throwToHandler, stateVar, disposablesVar, lockVar, resultVar, finalizers} = liftIO do - uninterruptibleMask_ do - join $ atomically do - state <- readTVar stateVar - case state of - ResourceManagerNormal -> do - writeTVar stateVar ResourceManagerDisposing - readTVar lockVar >>= \case - 0 -> do - disposables <- takeDisposables - pure (primaryBeginDispose disposables) - _ -> pure primaryForkDisposeThread - ResourceManagerDisposing -> pure $ pure $ defaultResourceManagerDisposeResult self - ResourceManagerDisposed -> pure $ pure DisposeResultDisposed - where - primaryForkDisposeThread :: IO DisposeResult - primaryForkDisposeThread = forkDisposeThread do - disposables <- atomically do - check =<< (== 0) <$> readTVar lockVar - takeDisposables - void $ primaryBeginDispose disposables - - -- Only one thread enters this function (in uninterruptible masked state) - primaryBeginDispose :: [Disposable] -> IO DisposeResult - primaryBeginDispose disposables = do - (reportExceptionActions, resultAwaitables) <- unzip <$> mapM beginDisposeEntry disposables - -- TODO cache was removed, has to be re-optimized later - let cachedResultAwaitable = mconcat resultAwaitables - putAsyncVar_ resultVar cachedResultAwaitable - - let - isCompletedAwaitable :: Awaitable () - isCompletedAwaitable = awaitResourceManagerResult $ ResourceManagerResult resourceManagerKey cachedResultAwaitable - - alreadyCompleted <- isJust <$> peekAwaitable isCompletedAwaitable - if alreadyCompleted - then do - completeDisposing - pure DisposeResultDisposed - else do - -- Start thread to collect exceptions, await completion and run finalizers - forkDisposeThread do - -- Collect exceptions from directly attached disposables - sequence_ reportExceptionActions - -- Await completion attached resource managers - await isCompletedAwaitable - - completeDisposing - - forkDisposeThread :: IO () -> IO DisposeResult - forkDisposeThread action = do - defaultResourceManagerDisposeResult self <$ forkIO do - catchAll - action - \ex -> - atomically $ throwToHandler $ toException $ - userError ("Dispose thread failed for ResourceManager: " <> displayException ex) - - takeDisposables :: STM [Disposable] - takeDisposables = toList <$> takeTMVar disposablesVar - - beginDisposeEntry :: Disposable -> IO (IO (), (Awaitable [ResourceManagerResult])) - beginDisposeEntry disposable = - catchAll - do - result <- beginDispose disposable - pure case result of - DisposeResultDisposed -> (pure (), pure []) - -- Moves error reporting from the awaitable to the finalizer thread - DisposeResultAwait awaitable -> (processDisposeException awaitable, [] <$ awaitSuccessOrFailure awaitable) - DisposeResultResourceManager resourceManagerResult -> (pure (), pure [resourceManagerResult]) - \ex -> do - atomically $ throwToHandler $ toException $ DisposeException ex - pure (pure (), pure []) - - processDisposeException :: Awaitable () -> IO () - processDisposeException awaitable = - await awaitable - `catchAll` - \ex -> atomically $ throwToHandler $ toException $ DisposeException ex - - completeDisposing :: IO () - completeDisposing = - atomically do - writeTVar stateVar $ ResourceManagerDisposed - defaultRunFinalizers finalizers - - isDisposed ResourceManagerCore{stateVar} = - unsafeAwaitSTM do - disposed <- stateIsDisposed <$> readTVar stateVar - check disposed - where - stateIsDisposed :: ResourceManagerState -> Bool - stateIsDisposed ResourceManagerDisposed = True - stateIsDisposed _ = False - - registerFinalizer ResourceManagerCore{finalizers} = defaultRegisterFinalizer finalizers - -defaultResourceManagerDisposeResult :: ResourceManagerCore -> DisposeResult -defaultResourceManagerDisposeResult ResourceManagerCore{resourceManagerKey, resultVar} = - DisposeResultResourceManager $ ResourceManagerResult resourceManagerKey $ join $ toAwaitable resultVar - --- | Internal constructor. The resulting resource manager core is indirectly attached to it's parent by it's exception --- handler. -newUnmanagedDefaultResourceManagerInternal :: (SomeException -> STM ()) -> STM ResourceManagerCore -newUnmanagedDefaultResourceManagerInternal throwToHandler = do - resourceManagerKey <- newUniqueSTM - stateVar <- newTVar ResourceManagerNormal - disposablesVar <- newTMVar HM.empty - lockVar <- newTVar 0 - finalizers <- newDisposableFinalizersSTM - resultVar <- newAsyncVarSTM - - pure ResourceManagerCore { - resourceManagerKey, - throwToHandler, - stateVar, - disposablesVar, - lockVar, - finalizers, - resultVar - } - -newResourceManager :: MonadResourceManager m => m ResourceManager -newResourceManager = do - parent <- askResourceManager - runInSTM $ newResourceManagerSTM parent - -newResourceManagerSTM :: ResourceManager -> STM ResourceManager -newResourceManagerSTM parent = do - -- Bind core exception handler to parent to tie exception handling to the parent - resourceManager <- newUnmanagedDefaultResourceManagerInternal (throwToResourceManagerImpl parent) - -- Attach disposable to parent to tie resource management to the parent - attachDisposable parent resourceManager - pure $ NormalResourceManager resourceManager parent - - --- * Utilities - --- | Creates an `Disposable` that is bound to a ResourceManager. It will automatically be disposed when the resource manager is disposed. -attachDisposeAction :: ResourceManager -> IO () -> STM Disposable -attachDisposeAction resourceManager action = do - disposable <- newDisposable action - attachDisposable resourceManager disposable - pure disposable - --- | Attaches a dispose action to a ResourceManager. It will automatically be run when the resource manager is disposed. -attachDisposeAction_ :: ResourceManager -> IO () -> STM () -attachDisposeAction_ resourceManager action = void $ attachDisposeAction resourceManager action - - --- ** Link execution to resource manager - --- | A computation bound to a resource manager with 'linkThread' should be canceled. -data CancelLinkedExecution = CancelLinkedExecution Unique - deriving anyclass Exception - -instance Show CancelLinkedExecution where - show _ = "CancelLinkedExecution" - - -data LinkState = LinkStateLinked ThreadId | LinkStateThrowing | LinkStateCompleted - deriving stock Eq - - --- | Links the execution of a computation to a resource manager. --- --- The computation is executed on the current thread. When the resource manager is disposed before the computation --- is completed, a `CancelLinkedExecution`-exception is thrown to the current thread. -linkExecution :: (MonadResourceManager m, MonadIO m, MonadMask m) => m a -> m (Maybe a) -linkExecution action = do - key <- liftIO $ newUnique - var <- liftIO $ newTVarIO =<< LinkStateLinked <$> myThreadId - registerDisposeAction $ do - atomically (swapTVar var LinkStateThrowing) >>= \case - LinkStateLinked threadId -> throwTo threadId $ CancelLinkedExecution key - LinkStateThrowing -> pure () -- Dispose called twice - LinkStateCompleted -> pure () -- Thread has already left link - - catch - do - result <- action - state <- liftIO $ atomically $ swapTVar var LinkStateCompleted - when (state == LinkStateThrowing) $ sleepForever -- Wait for exception to arrive - pure $ Just result - - \ex@(CancelLinkedExecution exceptionKey) -> - if key == exceptionKey - then return Nothing - else throwM ex diff --git a/test/Quasar/AsyncSpec.hs b/test/Quasar/AsyncSpec.hs index 625e2c4..eb4374c 100644 --- a/test/Quasar/AsyncSpec.hs +++ b/test/Quasar/AsyncSpec.hs @@ -1,14 +1,14 @@ module Quasar.AsyncSpec (spec) where -import Control.Concurrent -import Control.Monad (void) -import Control.Monad.IO.Class +--import Control.Concurrent +--import Control.Monad (void) +--import Control.Monad.IO.Class import Prelude import Test.Hspec --import Quasar.Async -import Quasar.Awaitable -import Quasar.ResourceManager -import System.Timeout +--import Quasar.Awaitable +--import Quasar.ResourceManager +--import System.Timeout spec :: Spec spec = describe "async" $ it "async" $ pendingWith "moving to new implementation..." diff --git a/test/Quasar/DisposableSpec.hs b/test/Quasar/DisposableSpec.hs deleted file mode 100644 index 087e61e..0000000 --- a/test/Quasar/DisposableSpec.hs +++ /dev/null @@ -1,36 +0,0 @@ -module Quasar.DisposableSpec (spec) where - -import Control.Concurrent -import Control.Concurrent.STM -import Quasar.Prelude -import Test.Hspec -import Quasar.Awaitable -import Quasar.Disposable - -spec :: Spec -spec = parallel $ do - describe "Disposable" $ do - describe "noDisposable" $ do - it "can be disposed" $ io do - dispose noDisposable - - it "can be awaited" $ io do - await (isDisposed noDisposable) - - describe "newDisposable" $ do - it "signals it's disposed state" $ io do - disposable <- atomically $ newDisposable $ pure () - void $ forkIO $ threadDelay 100000 >> dispose disposable - await (isDisposed disposable) - - it "can be disposed multiple times" $ io do - disposable <- atomically $ newDisposable $ pure () - dispose disposable - dispose disposable - await (isDisposed disposable) - - it "can be disposed in parallel" $ do - disposable <- atomically $ newDisposable $ threadDelay 100000 - void $ forkIO $ dispose disposable - dispose disposable - await (isDisposed disposable) diff --git a/test/Quasar/Observable/ObservableHashMapSpec.hs b/test/Quasar/Observable/ObservableHashMapSpec.hs index f3a5f35..041ac64 100644 --- a/test/Quasar/Observable/ObservableHashMapSpec.hs +++ b/test/Quasar/Observable/ObservableHashMapSpec.hs @@ -1,16 +1,16 @@ module Quasar.Observable.ObservableHashMapSpec (spec) where -import Control.Monad (void) -import Data.HashMap.Strict qualified as HM -import Data.IORef -import Quasar.Awaitable -import Quasar.Disposable -import Quasar.Observable -import Quasar.Observable.Delta -import Quasar.Observable.ObservableHashMap qualified as OM +--import Control.Monad (void) +--import Data.HashMap.Strict qualified as HM +--import Data.IORef +--import Quasar.Awaitable +--import Quasar.Disposable +--import Quasar.Observable +--import Quasar.Observable.Delta +--import Quasar.Observable.ObservableHashMap qualified as OM import Quasar.Prelude -import Quasar.ResourceManager +--import Quasar.ResourceManager import Test.Hspec shouldReturnM :: (Eq a, Show a, MonadIO m) => m a -> a -> m () diff --git a/test/Quasar/ObservableSpec.hs b/test/Quasar/ObservableSpec.hs index 97c0a95..38d2a61 100644 --- a/test/Quasar/ObservableSpec.hs +++ b/test/Quasar/ObservableSpec.hs @@ -1,10 +1,10 @@ module Quasar.ObservableSpec (spec) where -import Data.IORef +--import Data.IORef import Quasar.Prelude -import Quasar.Awaitable -import Quasar.Observable -import Quasar.ResourceManager +--import Quasar.Awaitable +--import Quasar.Observable +--import Quasar.ResourceManager import Test.Hspec diff --git a/test/Quasar/ResourceManagerSpec.hs b/test/Quasar/ResourceManagerSpec.hs deleted file mode 100644 index 1fe2530..0000000 --- a/test/Quasar/ResourceManagerSpec.hs +++ /dev/null @@ -1,147 +0,0 @@ -module Quasar.ResourceManagerSpec (spec) where - -import Control.Concurrent -import Control.Concurrent.STM -import Control.Exception -import Control.Monad.Catch -import Quasar.Prelude -import Test.Hspec -import Quasar.Awaitable -import Quasar.Disposable -import Quasar.ResourceManager - -data TestException = TestException - deriving stock (Eq, Show) - -instance Exception TestException - -spec :: Spec -spec = parallel $ do - describe "ResourceManager" $ do - it "can be created" $ io do - withRootResourceManager $ pure () - - it "can be created and disposed" $ io do - withRootResourceManager do - resourceManager <- askResourceManager - disposeEventually_ resourceManager - - it "is disposed when exiting withRootResourceManager" $ io do - resourceManager <- withRootResourceManager askResourceManager - - peekAwaitable (isDisposed resourceManager) `shouldReturn` Just () - - it "can be created and disposed with a delay" $ io do - withRootResourceManager $ liftIO $ threadDelay 100000 - - it "can \"dispose\" a noDisposable" $ io do - withRootResourceManager do - registerDisposable noDisposable - - it "can attach a dispose action" $ io do - var <- newTVarIO False - withRootResourceManager do - registerDisposeAction $ atomically $ writeTVar var True - - atomically (readTVar var) `shouldReturn` True - - it "can attach a slow dispose action" $ io do - withRootResourceManager do - registerDisposeAction $ threadDelay 100000 - - it "re-throws an exception" $ do - shouldThrow - do - withRootResourceManager do - liftIO $ throwIO TestException - \TestException -> True - - it "handles an exception while disposing" $ io do - (`shouldThrow` \(_ :: CombinedException) -> True) do - withRootResourceManager do - registerDisposeAction $ throwIO TestException - liftIO $ threadDelay 100000 - - it "passes an exception to the root resource manager" $ io do - (`shouldThrow` \(_ :: CombinedException) -> True) do - withRootResourceManager do - withScopedResourceManager do - registerDisposeAction $ throwIO TestException - liftIO $ threadDelay 100000 - - it "passes an exception to the root resource manager when closing the inner resource manager first" $ io do - (`shouldThrow` \(_ :: CombinedException) -> True) do - withRootResourceManager do - withScopedResourceManager do - registerDisposeAction $ throwIO TestException - liftIO $ threadDelay 100000 - - it "can attach an disposable that is disposed asynchronously" $ io do - withRootResourceManager do - disposable <- captureDisposable_ $ registerDisposeAction $ threadDelay 100000 - liftIO $ void $ forkIO $ dispose disposable - - it "does not abort disposing when encountering an exception" $ do - var1 <- newTVarIO False - var2 <- newTVarIO False - (`shouldThrow` \(_ :: CombinedException) -> True) do - withRootResourceManager do - registerDisposeAction $ atomically (writeTVar var1 True) - registerDisposeAction $ throwIO TestException - registerDisposeAction $ atomically (writeTVar var2 True) - atomically (readTVar var1) `shouldReturn` True - atomically (readTVar var2) `shouldReturn` True - - it "withRootResourceManager will start disposing when receiving an exception" $ io do - (`shouldThrow` \(_ :: CombinedException) -> True) do - withRootResourceManager do - linkExecution do - throwToResourceManager TestException - sleepForever - - it "combines exceptions from resources with exceptions on the thread" $ io do - (`shouldThrow` \(combinedExceptions -> exceptions) -> length exceptions == 2) do - withRootResourceManager do - throwToResourceManager TestException - throwM TestException - - it "can dispose a resource manager loop" $ io do - withRootResourceManager do - rm1 <- newResourceManager - rm2 <- newResourceManager - liftIO $ atomically do - attachDisposable rm1 rm2 - attachDisposable rm2 rm1 - - it "can dispose a resource manager loop" $ io do - withRootResourceManager do - rm1 <- newResourceManager - rm2 <- newResourceManager - liftIO $ atomically do - attachDisposable rm1 rm2 - attachDisposable rm2 rm1 - dispose rm1 - - it "can dispose a resource manager loop with a shared disposable" $ io do - var <- newTVarIO (0 :: Int) - d <- atomically $ newDisposable $ atomically $ modifyTVar var (+ 1) - withRootResourceManager do - rm1 <- newResourceManager - rm2 <- newResourceManager - liftIO $ atomically do - attachDisposable rm1 rm2 - attachDisposable rm2 rm1 - attachDisposable rm1 d - attachDisposable rm2 d - - atomically (readTVar var) `shouldReturn` 1 - - - describe "linkExecution" do - it "does not generate an exception after it is completed" $ io do - (`shouldThrow` \(_ :: CombinedException) -> True) do - withRootResourceManager do - linkExecution do - pure () - throwToResourceManager TestException - liftIO $ threadDelay 100000 diff --git a/test/Quasar/ResourcesSpec.hs b/test/Quasar/ResourcesSpec.hs new file mode 100644 index 0000000..e25884d --- /dev/null +++ b/test/Quasar/ResourcesSpec.hs @@ -0,0 +1,178 @@ +module Quasar.ResourcesSpec (spec) where + +import Control.Concurrent +import Control.Concurrent.STM +import Control.Exception +import Control.Monad.Catch +import Quasar.Prelude +import Test.Hspec +import Quasar.Awaitable +import Quasar.Resources +import Quasar.Monad + +data TestException = TestException + deriving stock (Eq, Show) + +instance Exception TestException + +spec :: Spec +spec = pure () +--spec = parallel $ do +-- describe "ResourceManager" $ do +-- it "can be created" $ io do +-- withRootResourceManager $ pure () +-- +-- it "can be created and disposed" $ io do +-- withRootResourceManager do +-- resourceManager <- askResourceManager +-- disposeEventually_ resourceManager +-- +-- it "is disposed when exiting withRootResourceManager" $ io do +-- resourceManager <- withRootResourceManager askResourceManager +-- +-- peekAwaitable (isDisposed resourceManager) `shouldReturn` Just () +-- +-- it "can be created and disposed with a delay" $ io do +-- withRootResourceManager $ liftIO $ threadDelay 100000 +-- +-- it "can \"dispose\" a noDisposable" $ io do +-- withRootResourceManager do +-- registerDisposable noDisposable +-- +-- it "can attach a dispose action" $ io do +-- var <- newTVarIO False +-- withRootResourceManager do +-- registerDisposeAction $ atomically $ writeTVar var True +-- +-- atomically (readTVar var) `shouldReturn` True +-- +-- it "can attach a slow dispose action" $ io do +-- withRootResourceManager do +-- registerDisposeAction $ threadDelay 100000 +-- +-- it "re-throws an exception" $ do +-- shouldThrow +-- do +-- withRootResourceManager do +-- liftIO $ throwIO TestException +-- \TestException -> True +-- +-- it "handles an exception while disposing" $ io do +-- (`shouldThrow` \(_ :: CombinedException) -> True) do +-- withRootResourceManager do +-- registerDisposeAction $ throwIO TestException +-- liftIO $ threadDelay 100000 +-- +-- it "passes an exception to the root resource manager" $ io do +-- (`shouldThrow` \(_ :: CombinedException) -> True) do +-- withRootResourceManager do +-- withScopedResourceManager do +-- registerDisposeAction $ throwIO TestException +-- liftIO $ threadDelay 100000 +-- +-- it "passes an exception to the root resource manager when closing the inner resource manager first" $ io do +-- (`shouldThrow` \(_ :: CombinedException) -> True) do +-- withRootResourceManager do +-- withScopedResourceManager do +-- registerDisposeAction $ throwIO TestException +-- liftIO $ threadDelay 100000 +-- +-- it "can attach an disposable that is disposed asynchronously" $ io do +-- withRootResourceManager do +-- disposable <- captureDisposable_ $ registerDisposeAction $ threadDelay 100000 +-- liftIO $ void $ forkIO $ dispose disposable +-- +-- it "does not abort disposing when encountering an exception" $ do +-- var1 <- newTVarIO False +-- var2 <- newTVarIO False +-- (`shouldThrow` \(_ :: CombinedException) -> True) do +-- withRootResourceManager do +-- registerDisposeAction $ atomically (writeTVar var1 True) +-- registerDisposeAction $ throwIO TestException +-- registerDisposeAction $ atomically (writeTVar var2 True) +-- atomically (readTVar var1) `shouldReturn` True +-- atomically (readTVar var2) `shouldReturn` True +-- +-- it "withRootResourceManager will start disposing when receiving an exception" $ io do +-- (`shouldThrow` \(_ :: CombinedException) -> True) do +-- withRootResourceManager do +-- linkExecution do +-- throwToResourceManager TestException +-- sleepForever +-- +-- it "combines exceptions from resources with exceptions on the thread" $ io do +-- (`shouldThrow` \(combinedExceptions -> exceptions) -> length exceptions == 2) do +-- withRootResourceManager do +-- throwToResourceManager TestException +-- throwM TestException +-- +-- it "can dispose a resource manager loop" $ io do +-- withRootResourceManager do +-- rm1 <- newResourceManager +-- rm2 <- newResourceManager +-- liftIO $ atomically do +-- attachDisposable rm1 rm2 +-- attachDisposable rm2 rm1 +-- +-- it "can dispose a resource manager loop" $ io do +-- withRootResourceManager do +-- rm1 <- newResourceManager +-- rm2 <- newResourceManager +-- liftIO $ atomically do +-- attachDisposable rm1 rm2 +-- attachDisposable rm2 rm1 +-- dispose rm1 +-- +-- it "can dispose a resource manager loop with a shared disposable" $ io do +-- var <- newTVarIO (0 :: Int) +-- d <- atomically $ newDisposable $ atomically $ modifyTVar var (+ 1) +-- withRootResourceManager do +-- rm1 <- newResourceManager +-- rm2 <- newResourceManager +-- liftIO $ atomically do +-- attachDisposable rm1 rm2 +-- attachDisposable rm2 rm1 +-- attachDisposable rm1 d +-- attachDisposable rm2 d +-- +-- atomically (readTVar var) `shouldReturn` 1 +-- +-- +-- describe "linkExecution" do +-- it "does not generate an exception after it is completed" $ io do +-- (`shouldThrow` \(_ :: CombinedException) -> True) do +-- withRootResourceManager do +-- linkExecution do +-- pure () +-- throwToResourceManager TestException +-- liftIO $ threadDelay 100000 + + +-- From DisposableSpec.hs: +--spec :: Spec +--spec = parallel $ do +-- describe "Disposable" $ do +-- describe "noDisposable" $ do +-- it "can be disposed" $ io do +-- dispose noDisposable +-- +-- it "can be awaited" $ io do +-- await (isDisposed noDisposable) +-- +-- describe "newDisposable" $ do +-- it "signals it's disposed state" $ io do +-- disposable <- atomically $ newDisposable $ pure () +-- void $ forkIO $ threadDelay 100000 >> dispose disposable +-- await (isDisposed disposable) +-- +-- it "can be disposed multiple times" $ io do +-- disposable <- atomically $ newDisposable $ pure () +-- dispose disposable +-- dispose disposable +-- await (isDisposed disposable) +-- +-- it "can be disposed in parallel" $ do +-- disposable <- atomically $ newDisposable $ threadDelay 100000 +-- void $ forkIO $ dispose disposable +-- dispose disposable +-- await (isDisposed disposable) -- GitLab