module Async.Combinators
( -- * Running forever
WorkerExited (WorkerExited, WorkerFailed)
, withWorker
) where
import Universum
import Control.Concurrent (myThreadId)
import Control.Concurrent.Async (withAsync)
import Control.Exception (asyncExceptionFromException, asyncExceptionToException)
import Control.Exception.Safe (Exception (..), finally, throwTo, tryAsync)
import Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)
import qualified GHC.Show as Show (Show (show))
-----------------------
-- Running forever
-----------------------
-- | Asynchronous exception thrown to the main thread if the worker exits.
data WorkerExited = WorkerExited Text -- ^ Worker returned
| WorkerFailed Text SomeException -- ^ Worker crashed
instance Show WorkerExited where
show (WorkerExited n) = toString $ "Worker '" <> n <> "' returned"
show (WorkerFailed n e) = toString $ "Worker '" <> n <> "' failed: " <> show e
instance Exception WorkerExited where
toException = asyncExceptionToException
fromException = asyncExceptionFromException
-- | Like 'withAsync', but makes sure that the worker thread will
-- not exit, neither by returning, nor by throwing an exception.
-- If it exits, a 'WorkerExited' exception will be thrown in the current thread.
withWorker :: MonadUnliftIO m
=> Text -- ^ Human-readable name for the forked thread
-> m () -- ^ Action performed by the worker
-> m b -- ^ Action performed in current thread
-> m b
withWorker name worker go = withRunInIO $ \run -> do
tid <- myThreadId
mainDone <- newIORef False
let worker' = do
res <- tryAsync $ run worker
unlessM (readIORef mainDone) $ throwTo tid $
case res of
Right () -> WorkerExited name
Left e -> WorkerFailed name e
withAsync worker' $ \_ -> run go `finally` atomicWriteIORef mainDone True