Skip to content

Commit 9d55314

Browse files
committed
refactor: alternate implementation for exception context
This implementation works differently than the previous one. It actually implements all the standard function based on the GHC 9.12 api (e.g. `tryWithContext`, `catchNoPropagate`, `rethrowIO`) and provides a simple and localise compatibility layer which reimplements these function for older `base`. The implementation should be easier to read with less CPP and subtle logic scattered everywhere. It also implements `xxxWithContext` version for most of the functions used to return `SomeException`: they are now returning `ExceptionWithContext SomeException`, based on the `tryWithContext` semantic.
1 parent 91a23d5 commit 9d55314

2 files changed

Lines changed: 93 additions & 54 deletions

File tree

Control/Concurrent/Async.hs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE CPP #-}
12
-----------------------------------------------------------------------------
23
-- |
34
-- Module : Control.Concurrent.Async
@@ -146,7 +147,11 @@ module Control.Concurrent.Async (
146147
withAsyncOnWithUnmask,
147148

148149
-- ** Querying 'Async's
149-
wait, poll, waitCatch, asyncThreadId,
150+
wait, poll,
151+
#if MIN_VERSION_base(4,21,0)
152+
pollWithContext,
153+
#endif
154+
waitCatch, asyncThreadId,
150155
cancel, cancelMany, uninterruptibleCancel, cancelWith, AsyncCancelled(..),
151156

152157
-- ** #high-level-utilities# High-level utilities
@@ -164,6 +169,9 @@ module Control.Concurrent.Async (
164169

165170
-- *** STM operations
166171
waitSTM, pollSTM, waitCatchSTM,
172+
#if MIN_VERSION_base(4,21,0)
173+
waitCatchSTMWithContext,
174+
#endif
167175

168176
-- *** Waiting for multiple 'Async's
169177
waitAny, waitAnyCatch, waitAnyCancel, waitAnyCatchCancel,

Control/Concurrent/Async/Internal.hs

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ data Async a = Async
9191
{ asyncThreadId :: {-# UNPACK #-} !ThreadId
9292
-- ^ Returns the 'ThreadId' of the thread running
9393
-- the given 'Async'.
94-
, _asyncWait :: STM (Either SomeException a)
94+
, _asyncWait :: STM (Either (ExceptionWithContext SomeException) a)
9595
}
9696

9797
instance Eq (Async a) where
@@ -159,7 +159,7 @@ asyncUsing doFork action = do
159159
-- t <- forkFinally action (\r -> atomically $ putTMVar var r)
160160
-- slightly faster:
161161
t <- mask $ \restore ->
162-
doFork $ try (restore action_plus) >>= atomically . putTMVar var
162+
doFork $ tryWithContext (restore action_plus) >>= atomically . putTMVar var
163163
return (Async t (readTMVar var))
164164

165165

@@ -213,7 +213,6 @@ withAsyncOnWithUnmask ::
213213
withAsyncOnWithUnmask cpu actionWith =
214214
withAsyncUsing (rawForkOn cpu) (actionWith unsafeUnmask)
215215

216-
#if MIN_VERSION_base(4,21,0)
217216
withAsyncUsing ::
218217
CALLSTACK
219218
(IO () -> IO ThreadId) -> IO a -> (Async a -> IO b) -> IO b
@@ -223,7 +222,7 @@ withAsyncUsing doFork action inner = do
223222
var <- newEmptyTMVarIO
224223
mask $ \restore -> do
225224
let action_plus = debugLabelMe >> action
226-
t <- doFork $ try (restore action_plus) >>= atomically . putTMVar var
225+
t <- doFork $ tryWithContext (restore action_plus) >>= atomically . putTMVar var
227226
let a = Async t (readTMVar var)
228227
-- Using catch/no/propagate and rethrowIO, we do not wrap the exception
229228
-- with a `WhileWaiting`
@@ -232,37 +231,47 @@ withAsyncUsing doFork action inner = do
232231
rethrowIO (e :: ExceptionWithContext SomeException)
233232
uninterruptibleCancel a
234233
return r
234+
235+
-- * Compatibilty logic with base 4.21 for exception context. The rational here is that this module is implemented with 'ExceptionWithContext' as the basic building block with the following special cases:
236+
--
237+
-- - With base >= 4.21 (GHC 9.12), exception context is propagated correctly using the 'rethrowIO', 'catchNoPropagate', ... functions.
238+
-- - With base >= 4.20 (GHC 9.10), exception context logic exists, but not the 'rethrow' logic. We reimplemented these function which are basically discarding the context
239+
-- - With base < 4.20 (GHC 9.8 and older), we just use the old functions which does not know anything about exception context. We implement an alias 'ExceptionWithContext' which is actually bare exception.
240+
--
241+
-- For all version we implement 'dropContext' which is able to drop the
242+
-- context, for all the function such as 'poll' which returns an exception without context.
243+
244+
245+
-- | Drop the exception context
246+
dropContext :: ExceptionWithContext t -> t
247+
248+
-- | Rethrow an exception inside 'STM' context, while preserving the 'ExceptionContext'. See 'rethrowIO' for details.
249+
rethrowSTM :: Exception e => ExceptionWithContext e -> STM a
250+
251+
#if MIN_VERSION_base(4,21,0)
235252
#else
236-
withAsyncUsing ::
237-
CALLSTACK
238-
(IO () -> IO ThreadId) -> IO a -> (Async a -> IO b) -> IO b
239-
-- The bracket version works, but is slow. We can do better by
240-
-- hand-coding it:
241-
withAsyncUsing doFork action inner = do
242-
var <- newEmptyTMVarIO
243-
mask $ \restore -> do
244-
let action_plus = debugLabelMe >> action
245-
t <- doFork $ try (restore action_plus) >>= atomically . putTMVar var
246-
let a = Async t (readTMVar var)
247-
r <- restore (inner a) `catchAll` \e -> do
248-
uninterruptibleCancel a
249-
throwIO e
250-
uninterruptibleCancel a
251-
return r
253+
rethrowIO :: ExceptionWithContext SomeException -> IO a
254+
catchNoPropagate :: forall e a. Exception e => IO a -> (ExceptionWithContext e -> IO a) -> IO a
252255
#endif
253256

254-
255-
-- | This function attempts at rethrowing while keeping the context
256-
-- This is internal and only working with GHC >=9.12, otherwise it fallsback to
257-
-- standard 'throwIO'
258-
rethrowIO' :: SomeException -> IO a
259257
#if MIN_VERSION_base(4,21,0)
260-
rethrowIO' e =
261-
case fromException e of
262-
Just (e' :: ExceptionWithContext SomeException) -> rethrowIO e'
263-
Nothing -> throwIO e
258+
dropContext (ExceptionWithContext _context e) = e
259+
rethrowSTM e = throwSTM (NoBacktrace e)
260+
#elif MIN_VERSION_base(4,20,0)
261+
dropContext (ExceptionWithContext ctx e) = e
262+
rethrowSTM e = throwSTM e
263+
264+
rethrowIO e = throwIO (dropContext e)
265+
catchNoPropagate = catch
266+
tryWithContext = try
264267
#else
265-
rethrowIO' = throwIO
268+
dropContext e = e
269+
rethrowSTM e = throwSTM e
270+
271+
type ExceptionWithContext e = e
272+
rethrowIO e = throwIO e
273+
catchNoPropagate = catch
274+
tryWithContext = try
266275
#endif
267276

268277
-- | An exception annotation which stores the callstack of a 'wait',
@@ -326,37 +335,59 @@ waitCatch = tryAgain . atomically . waitCatchSTM
326335
poll :: Async a -> IO (Maybe (Either SomeException a))
327336
poll = atomically . pollSTM
328337

338+
339+
#if MIN_VERSION_base(4,21,0)
340+
-- | Check whether an 'Async' has completed yet. If it has not
341+
-- completed yet, then the result is @Nothing@, otherwise the result
342+
-- is @Just e@ where @e@ is @Left x@ if the @Async@ raised an
343+
-- exception @x@, or @Right a@ if it returned a value @a@.
344+
--
345+
-- > pollWithContext = atomically . pollSTMWithContext
346+
--
347+
-- This version behaves as 'poll' but does not discard the 'ExceptionContext'.
348+
-- See 'tryWithContext' for details.
349+
--
350+
{-# INLINE pollWithContext #-}
351+
pollWithContext :: Async a -> IO (Maybe (Either (ExceptionWithContext SomeException) a))
352+
pollWithContext = atomically . pollSTMWithContext
353+
#endif
354+
329355
-- | A version of 'wait' that can be used inside an STM transaction.
330356
--
331357
waitSTM :: Async a -> STM a
332358
waitSTM a = do
333-
r <- waitCatchSTM a
359+
r <- waitCatchSTMWithContext a
334360
either (rethrowSTM) return r
335361

336-
-- | This function attempts at rethrowing while keeping the context
337-
-- This is internal and only working with GHC >=9.12, otherwise it fallsback to
338-
-- standard 'throwSTM'
339-
rethrowSTM :: SomeException -> STM a
340-
#if MIN_VERSION_base(4,21,0)
341-
rethrowSTM e =
342-
case fromException e of
343-
Just (e' :: ExceptionWithContext SomeException) -> throwSTM (NoBacktrace e')
344-
Nothing -> throwSTM e
345-
#else
346-
rethrowSTM = throwSTM
347-
#endif
348-
349362
-- | A version of 'waitCatch' that can be used inside an STM transaction.
350363
--
351364
{-# INLINE waitCatchSTM #-}
352365
waitCatchSTM :: Async a -> STM (Either SomeException a)
353-
waitCatchSTM (Async _ w) = w
366+
waitCatchSTM (Async _ w) = either (Left . dropContext) Right <$> w
367+
368+
369+
-- | A version of 'waitCatch' that can be used inside an STM transaction.
370+
--
371+
-- The returned exception keep the 'ExceptionContext'. See 'tryWithContext' for details.
372+
{-# INLINE waitCatchSTMWithContext #-}
373+
waitCatchSTMWithContext :: Async a -> STM (Either (ExceptionWithContext SomeException) a)
374+
waitCatchSTMWithContext (Async _ w) = w
354375

355376
-- | A version of 'poll' that can be used inside an STM transaction.
356377
--
357378
{-# INLINE pollSTM #-}
358379
pollSTM :: Async a -> STM (Maybe (Either SomeException a))
359-
pollSTM (Async _ w) = (Just <$> w) `orElse` return Nothing
380+
pollSTM (Async _ w) = (Just . either (Left . dropContext) Right <$> w) `orElse` return Nothing
381+
382+
#if MIN_VERSION_base(4,21,0)
383+
-- | A version of 'poll' that can be used inside an STM transaction.
384+
--
385+
-- It keep the exception context associated with the exception. See 'tryWithContext' for details.
386+
--
387+
{-# INLINE pollSTMWithContext #-}
388+
pollSTMWithContext :: Async a -> STM (Maybe (Either (ExceptionWithContext SomeException) a))
389+
pollSTMWithContext (Async _ w) = (Just <$> w) `orElse` return Nothing
390+
#endif
360391

361392
-- | Cancel an asynchronous action by throwing the @AsyncCancelled@
362393
-- exception to it, and waiting for the `Async` thread to quit.
@@ -743,7 +774,7 @@ race left right = concurrently' left right collect
743774
collect m = do
744775
e <- m
745776
case e of
746-
Left ex -> rethrowIO' ex
777+
Left ex -> rethrowIO ex
747778
Right r -> return r
748779

749780
-- race_ :: IO a -> IO b -> IO ()
@@ -757,7 +788,7 @@ concurrently left right = concurrently' left right (collect [])
757788
collect xs m = do
758789
e <- m
759790
case e of
760-
Left ex -> rethrowIO' ex
791+
Left ex -> rethrowIO ex
761792
Right r -> collect (r:xs) m
762793

763794
-- concurrentlyE :: IO (Either e a) -> IO (Either e b) -> IO (Either e (a, b))
@@ -770,13 +801,13 @@ concurrentlyE left right = concurrently' left right (collect [])
770801
collect xs m = do
771802
e <- m
772803
case e of
773-
Left ex -> rethrowIO' ex
804+
Left ex -> rethrowIO ex
774805
Right r -> collect (r:xs) m
775806

776807
concurrently' ::
777808
CALLSTACK
778809
IO a -> IO b
779-
-> (IO (Either SomeException (Either a b)) -> IO r)
810+
-> (IO (Either (ExceptionWithContext SomeException) (Either a b)) -> IO r)
780811
-> IO r
781812
concurrently' left right collect = do
782813
done <- newEmptyMVar
@@ -787,10 +818,10 @@ concurrently' left right collect = do
787818
-- the thread to terminate.
788819
lid <- forkIO $ uninterruptibleMask_ $
789820
restore (left >>= putMVar done . Right . Left)
790-
`catchAll` (putMVar done . Left)
821+
`catchNoPropagate` (putMVar done . Left)
791822
rid <- forkIO $ uninterruptibleMask_ $
792823
restore (right >>= putMVar done . Right . Right)
793-
`catchAll` (putMVar done . Left)
824+
`catchNoPropagate` (putMVar done . Left)
794825

795826
count <- newIORef (2 :: Int)
796827
let takeDone = do
@@ -831,7 +862,7 @@ concurrently_ left right = concurrently' left right (collect 0)
831862
collect i m = do
832863
e <- m
833864
case e of
834-
Left ex -> rethrowIO' ex
865+
Left ex -> rethrowIO ex
835866
Right _ -> collect (i + 1 :: Int) m
836867

837868

0 commit comments

Comments
 (0)