From cc6af12e1f77a769a2b1582f9cd476aa68f18049 Mon Sep 17 00:00:00 2001
From: Jens Nolte <git@queezle.net>
Date: Sun, 3 Oct 2021 22:48:18 +0200
Subject: [PATCH] Add dedicated exceptions for linked resource manager threads

Co-authored-by: Jan Beinke <git@janbeinke.com>
---
 quasar.cabal                       |  1 +
 src/Quasar/Disposable.hs           |  1 +
 src/Quasar/Observable.hs           |  1 +
 src/Quasar/ResourceManager.hs      | 24 ++++++--
 test/Quasar/DisposableSpec.hs      | 73 ----------------------
 test/Quasar/ResourceManagerSpec.hs | 97 ++++++++++++++++++++++++++++++
 6 files changed, 119 insertions(+), 78 deletions(-)
 create mode 100644 test/Quasar/ResourceManagerSpec.hs

diff --git a/quasar.cabal b/quasar.cabal
index ddd2245..e0c9da8 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 8a6e3d4..3e0329e 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 16a44af..cac15b3 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 554219f..7ba907b 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 9651659..7ccd885 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 0000000..640fcee
--- /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
-- 
GitLab