Skip to content

Commit 3012a47

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.
1 parent 91a23d5 commit 3012a47

3 files changed

Lines changed: 72 additions & 55 deletions

File tree

Control/Concurrent/Async.hs

Lines changed: 3 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,8 @@ module Control.Concurrent.Async (
146147
withAsyncOnWithUnmask,
147148

148149
-- ** Querying 'Async's
149-
wait, poll, waitCatch, asyncThreadId,
150+
wait, poll,
151+
waitCatch, asyncThreadId,
150152
cancel, cancelMany, uninterruptibleCancel, cancelWith, AsyncCancelled(..),
151153

152154
-- ** #high-level-utilities# High-level utilities

Control/Concurrent/Async/Internal.hs

Lines changed: 68 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,48 @@ 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
255+
tryWithContext :: IO a -> IO (Either (ExceptionWithContext SomeException) a)
252256
#endif
253257

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
259258
#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
259+
dropContext (ExceptionWithContext _context e) = e
260+
rethrowSTM e = throwSTM (NoBacktrace e)
261+
#elif MIN_VERSION_base(4,20,0)
262+
dropContext (ExceptionWithContext _context e) = e
263+
rethrowSTM e = throwSTM (dropContext e)
264+
265+
rethrowIO e = throwIO (dropContext e)
266+
catchNoPropagate = catch
267+
tryWithContext = try
264268
#else
265-
rethrowIO' = throwIO
269+
dropContext e = e
270+
rethrowSTM e = throwSTM e
271+
272+
type ExceptionWithContext e = e
273+
rethrowIO e = throwIO e
274+
catchNoPropagate = catch
275+
tryWithContext = try
266276
#endif
267277

268278
-- | An exception annotation which stores the callstack of a 'wait',
@@ -330,33 +340,38 @@ poll = atomically . pollSTM
330340
--
331341
waitSTM :: Async a -> STM a
332342
waitSTM a = do
333-
r <- waitCatchSTM a
343+
r <- waitCatchSTMWithContext a
334344
either (rethrowSTM) return r
335345

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-
349346
-- | A version of 'waitCatch' that can be used inside an STM transaction.
350347
--
351348
{-# INLINE waitCatchSTM #-}
352349
waitCatchSTM :: Async a -> STM (Either SomeException a)
353-
waitCatchSTM (Async _ w) = w
350+
waitCatchSTM (Async _ w) = either (Left . dropContext) Right <$> w
351+
352+
353+
-- | A version of 'waitCatch' that can be used inside an STM transaction.
354+
--
355+
-- The returned exception keep the 'ExceptionContext'. See 'tryWithContext' for details.
356+
{-# INLINE waitCatchSTMWithContext #-}
357+
waitCatchSTMWithContext :: Async a -> STM (Either (ExceptionWithContext SomeException) a)
358+
waitCatchSTMWithContext (Async _ w) = w
354359

355360
-- | A version of 'poll' that can be used inside an STM transaction.
356361
--
357362
{-# INLINE pollSTM #-}
358363
pollSTM :: Async a -> STM (Maybe (Either SomeException a))
359-
pollSTM (Async _ w) = (Just <$> w) `orElse` return Nothing
364+
pollSTM (Async _ w) = (Just . either (Left . dropContext) Right <$> w) `orElse` return Nothing
365+
366+
#if MIN_VERSION_base(4,21,0)
367+
-- | A version of 'poll' that can be used inside an STM transaction.
368+
--
369+
-- It keep the exception context associated with the exception. See 'tryWithContext' for details.
370+
--
371+
{-# INLINE pollSTMWithContext #-}
372+
pollSTMWithContext :: Async a -> STM (Maybe (Either (ExceptionWithContext SomeException) a))
373+
pollSTMWithContext (Async _ w) = (Just <$> w) `orElse` return Nothing
374+
#endif
360375

361376
-- | Cancel an asynchronous action by throwing the @AsyncCancelled@
362377
-- exception to it, and waiting for the `Async` thread to quit.
@@ -743,7 +758,7 @@ race left right = concurrently' left right collect
743758
collect m = do
744759
e <- m
745760
case e of
746-
Left ex -> rethrowIO' ex
761+
Left ex -> rethrowIO ex
747762
Right r -> return r
748763

749764
-- race_ :: IO a -> IO b -> IO ()
@@ -757,7 +772,7 @@ concurrently left right = concurrently' left right (collect [])
757772
collect xs m = do
758773
e <- m
759774
case e of
760-
Left ex -> rethrowIO' ex
775+
Left ex -> rethrowIO ex
761776
Right r -> collect (r:xs) m
762777

763778
-- concurrentlyE :: IO (Either e a) -> IO (Either e b) -> IO (Either e (a, b))
@@ -770,13 +785,13 @@ concurrentlyE left right = concurrently' left right (collect [])
770785
collect xs m = do
771786
e <- m
772787
case e of
773-
Left ex -> rethrowIO' ex
788+
Left ex -> rethrowIO ex
774789
Right r -> collect (r:xs) m
775790

776791
concurrently' ::
777792
CALLSTACK
778793
IO a -> IO b
779-
-> (IO (Either SomeException (Either a b)) -> IO r)
794+
-> (IO (Either (ExceptionWithContext SomeException) (Either a b)) -> IO r)
780795
-> IO r
781796
concurrently' left right collect = do
782797
done <- newEmptyMVar
@@ -787,10 +802,10 @@ concurrently' left right collect = do
787802
-- the thread to terminate.
788803
lid <- forkIO $ uninterruptibleMask_ $
789804
restore (left >>= putMVar done . Right . Left)
790-
`catchAll` (putMVar done . Left)
805+
`catchNoPropagate` (putMVar done . Left)
791806
rid <- forkIO $ uninterruptibleMask_ $
792807
restore (right >>= putMVar done . Right . Right)
793-
`catchAll` (putMVar done . Left)
808+
`catchNoPropagate` (putMVar done . Left)
794809

795810
count <- newIORef (2 :: Int)
796811
let takeDone = do
@@ -831,7 +846,7 @@ concurrently_ left right = concurrently' left right (collect 0)
831846
collect i m = do
832847
e <- m
833848
case e of
834-
Left ex -> rethrowIO' ex
849+
Left ex -> rethrowIO ex
835850
Right _ -> collect (i + 1 :: Int) m
836851

837852

test/test-async.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ withasync_wait_blocked = do
196196
Left e ->
197197
case fromException e of
198198
Just BlockedIndefinitelyOnMVar -> return ()
199-
Nothing -> assertFailure $ show e
199+
Nothing -> assertFailure $ show ("what", e)
200200
Right () -> assertFailure ""
201201

202202
concurrently_success :: Assertion

0 commit comments

Comments
 (0)