diff --git a/quasar.cabal b/quasar.cabal index ddd224566a0ab1b3f24b1c0ac654a2d155952d55..e0c9da8fda0f6cfea2faa5c847b6f5c98fea961d 100644 --- a/quasar.cabal +++ b/quasar.cabal @@ -117,6 +117,7 @@ test-suite quasar-test Quasar.ObservableSpec Quasar.Observable.ObservableHashMapSpec Quasar.Observable.ObservablePrioritySpec + Quasar.ResourceManagerSpec Quasar.SubscribableSpec hs-source-dirs: test diff --git a/src/Quasar/Disposable.hs b/src/Quasar/Disposable.hs index 8a6e3d42d4aa4921aba4d8658266c948a446edbf..3e0329e2bd3a2397fe629daa186db7b6f3336fa4 100644 --- a/src/Quasar/Disposable.hs +++ b/src/Quasar/Disposable.hs @@ -47,6 +47,7 @@ class IsDisposable a where {-# MINIMAL toDisposable | (dispose, isDisposed) #-} +-- TODO remove disposeAndAwait :: (MonadAwait m, MonadIO m) => IsDisposable a => a -> m () disposeAndAwait disposable = await =<< liftIO (dispose disposable) diff --git a/src/Quasar/Observable.hs b/src/Quasar/Observable.hs index 16a44afd267203dd49cecec736e1aadf7e91e9be..cac15b30db5dcf1f9d94969832e3d17cbbf904d7 100644 --- a/src/Quasar/Observable.hs +++ b/src/Quasar/Observable.hs @@ -76,6 +76,7 @@ toObservableUpdate (ObservableNotAvailable ex) = throwM ex class IsRetrievable v a | a -> v where retrieve :: MonadResourceManager m => a -> m (Awaitable v) +-- TODO remove retrieveIO :: IsRetrievable v a => a -> IO v retrieveIO x = withResourceManagerM $ await =<< retrieve x diff --git a/src/Quasar/ResourceManager.hs b/src/Quasar/ResourceManager.hs index 554219fb5d7833dc723d41d7a7ba2b4ae9b4fc06..7ba907b3e5b314e2773da73d8fa1fb3ebe57bfae 100644 --- a/src/Quasar/ResourceManager.hs +++ b/src/Quasar/ResourceManager.hs @@ -3,6 +3,7 @@ module Quasar.ResourceManager ( MonadResourceManager(..), registerDisposable, registerDisposeAction, + registerSimpleDisposeAction, disposeEventually, withSubResourceManagerM, onResourceManager, @@ -21,6 +22,9 @@ module Quasar.ResourceManager ( withRootResourceManager, withRootResourceManagerM, + CancelLinkedThread(..), + LinkedThreadDisposed(..), + -- ** Resource manager implementations newUnmanagedRootResourceManager, --newUnmanagedDefaultResourceManager, @@ -122,10 +126,11 @@ registerDisposable disposable = do registerDisposeAction :: MonadResourceManager m => IO (Awaitable ()) -> m () registerDisposeAction disposeAction = mask_ $ registerDisposable =<< newDisposable disposeAction -registerDisposeAction' :: MonadResourceManager m => IO () -> m () -registerDisposeAction' disposeAction = registerDisposeAction (pure () <$ disposeAction) +registerSimpleDisposeAction :: MonadResourceManager m => IO () -> m () +registerSimpleDisposeAction disposeAction = registerDisposeAction (pure () <$ disposeAction) +-- TODO rename to withResourceScope? withSubResourceManagerM :: MonadResourceManager m => m a -> m a withSubResourceManagerM action = bracket newResourceManager (await <=< dispose) \scope -> localResourceManager scope action @@ -173,7 +178,16 @@ captureTask action = do type ExceptionHandler = SomeException -> IO () loggingExceptionHandler :: ExceptionHandler -loggingExceptionHandler ex = hPutStrLn stderr $ displayException ex +loggingExceptionHandler ex = traceIO $ displayException ex + + +data CancelLinkedThread = CancelLinkedThread + deriving stock Show + deriving anyclass Exception + +data LinkedThreadDisposed = LinkedThreadDisposed + deriving stock Show + deriving anyclass Exception data CancelHelper = CancelHelper @@ -194,7 +208,7 @@ withLinkedExceptionHandler parentExceptionHandler action = do do unmask do atomically $ check =<< readTVar shouldCancelVar - throwTo mainThreadId CancelTask + throwTo mainThreadId CancelLinkedThread `catch` \CancelHelper -> pure () @@ -206,7 +220,7 @@ withLinkedExceptionHandler parentExceptionHandler action = do do \cancelThreadId -> liftIO $ throwTo cancelThreadId CancelHelper do \_ -> unmask $ action exceptionHandler `catch` - \CancelTask -> throwM TaskDisposed + \CancelLinkedThread -> throwM LinkedThreadDisposed diff --git a/test/Quasar/DisposableSpec.hs b/test/Quasar/DisposableSpec.hs index 9651659c5668ff32f45f1bcf0687c2f2c5f2d606..7ccd88545f206d2fce525cdd50304cd41f0d62b1 100644 --- a/test/Quasar/DisposableSpec.hs +++ b/test/Quasar/DisposableSpec.hs @@ -8,11 +8,6 @@ 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 "Disposable" $ do @@ -40,71 +35,3 @@ spec = parallel $ do void $ forkIO $ disposeAndAwait disposable disposeAndAwait disposable await (isDisposed disposable) - - - describe "ResourceManager" $ do - it "can be created" $ io do - void newUnmanagedResourceManager - - it "can be created and disposed" $ io do - resourceManager <- newUnmanagedResourceManager - await =<< dispose resourceManager - - it "can be created and disposed" $ io do - withResourceManager \_ -> pure () - - it "can be created and disposed with a delay" $ do - withResourceManager \_ -> threadDelay 100000 - - it "can \"dispose\" a noDisposable" $ io do - withResourceManager \resourceManager -> do - attachDisposable resourceManager noDisposable - - it "can attach an disposable" $ do - withResourceManager \resourceManager -> do - avar <- newAsyncVar :: IO (AsyncVar ()) - attachDisposable resourceManager $ alreadyDisposing avar - putAsyncVar_ avar () - pure () :: IO () - - it "can dispose an awaitable that is completed asynchronously" $ do - avar <- newAsyncVar :: IO (AsyncVar ()) - void $ forkIO $ do - threadDelay 100000 - putAsyncVar_ avar () - - withResourceManager \resourceManager -> do - attachDisposable resourceManager (alreadyDisposing avar) - - it "can call a trivial dispose action" $ do - withResourceManager \resourceManager -> - attachDisposeAction_ resourceManager $ pure $ pure () - pure () :: IO () - - it "can call a dispose action" $ do - withResourceManager \resourceManager -> do - avar <- newAsyncVar :: IO (AsyncVar ()) - attachDisposeAction_ resourceManager $ toAwaitable avar <$ putAsyncVar_ avar () - pure () :: IO () - - it "re-throws an exception" $ do - shouldThrow - do - withResourceManager \_ -> - throwIO TestException - \TestException -> True - - it "cancels the main thread when a dispose action fails" $ do - shouldThrow - do - withRootResourceManagerM do - withSubResourceManagerM do - registerDisposeAction $ throwIO TestException - liftIO $ threadDelay 100000 - fail "Did not stop main thread on failing dispose action" - \TaskDisposed -> True - - it "can attach an disposable that is disposed asynchronously" $ do - withResourceManager \resourceManager -> do - disposable <- attachDisposeAction resourceManager $ pure () <$ threadDelay 100000 - void $ forkIO $ disposeAndAwait disposable diff --git a/test/Quasar/ResourceManagerSpec.hs b/test/Quasar/ResourceManagerSpec.hs new file mode 100644 index 0000000000000000000000000000000000000000..640fcee6ba09e5f72aa6fd4202d60206cf3eacb7 --- /dev/null +++ b/test/Quasar/ResourceManagerSpec.hs @@ -0,0 +1,97 @@ +module Quasar.ResourceManagerSpec (spec) where + +import Control.Concurrent +import Control.Concurrent.STM +import Control.Exception +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 + void newUnmanagedResourceManager + + it "can be created and disposed" $ io do + resourceManager <- newUnmanagedResourceManager + await =<< dispose resourceManager + + it "can be created and disposed" $ io do + withResourceManager \_ -> pure () + + it "can be created and disposed with a delay" $ do + withResourceManager \_ -> threadDelay 100000 + + it "can \"dispose\" a noDisposable" $ io do + withResourceManager \resourceManager -> do + attachDisposable resourceManager noDisposable + + it "can attach an disposable" $ do + withResourceManager \resourceManager -> do + avar <- newAsyncVar :: IO (AsyncVar ()) + attachDisposable resourceManager $ alreadyDisposing avar + putAsyncVar_ avar () + pure () :: IO () + + it "can dispose an awaitable that is completed asynchronously" $ do + avar <- newAsyncVar :: IO (AsyncVar ()) + void $ forkIO $ do + threadDelay 100000 + putAsyncVar_ avar () + + withResourceManager \resourceManager -> do + attachDisposable resourceManager (alreadyDisposing avar) + + it "can call a trivial dispose action" $ do + withResourceManager \resourceManager -> + attachDisposeAction_ resourceManager $ pure $ pure () + pure () :: IO () + + it "can call a dispose action" $ do + withResourceManager \resourceManager -> do + avar <- newAsyncVar :: IO (AsyncVar ()) + attachDisposeAction_ resourceManager $ toAwaitable avar <$ putAsyncVar_ avar () + pure () :: IO () + + it "re-throws an exception" $ do + shouldThrow + do + withResourceManager \_ -> + throwIO TestException + \TestException -> True + + it "cancels the main thread when a dispose action fails" $ do + shouldThrow + do + withRootResourceManagerM do + withSubResourceManagerM do + registerDisposeAction $ throwIO TestException + liftIO $ threadDelay 100000 + fail "Did not stop main thread on failing dispose action" + \LinkedThreadDisposed -> True + + it "can attach an disposable that is disposed asynchronously" $ do + withResourceManager \resourceManager -> do + disposable <- attachDisposeAction resourceManager $ pure () <$ threadDelay 100000 + void $ forkIO $ disposeAndAwait disposable + + it "does not abort when encountering an exception" $ do + var1 <- newTVarIO False + var2 <- newTVarIO False + shouldThrow + do + withRootResourceManagerM do + registerDisposeAction $ pure () <$ (atomically (writeTVar var1 True)) + registerDisposeAction $ pure () <$ throwIO TestException + registerDisposeAction $ pure () <$ (atomically (writeTVar var2 True)) + \LinkedThreadDisposed -> True + atomically (readTVar var1) `shouldReturn` True + atomically (readTVar var2) `shouldReturn` True