Skip to content

Commit 5404d4d

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 5404d4d

3 files changed

Lines changed: 71 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: 67 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',
@@ -330,33 +339,38 @@ poll = atomically . pollSTM
330339
--
331340
waitSTM :: Async a -> STM a
332341
waitSTM a = do
333-
r <- waitCatchSTM a
342+
r <- waitCatchSTMWithContext a
334343
either (rethrowSTM) return r
335344

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

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

361375
-- | Cancel an asynchronous action by throwing the @AsyncCancelled@
362376
-- exception to it, and waiting for the `Async` thread to quit.
@@ -743,7 +757,7 @@ race left right = concurrently' left right collect
743757
collect m = do
744758
e <- m
745759
case e of
746-
Left ex -> rethrowIO' ex
760+
Left ex -> rethrowIO ex
747761
Right r -> return r
748762

749763
-- race_ :: IO a -> IO b -> IO ()
@@ -757,7 +771,7 @@ concurrently left right = concurrently' left right (collect [])
757771
collect xs m = do
758772
e <- m
759773
case e of
760-
Left ex -> rethrowIO' ex
774+
Left ex -> rethrowIO ex
761775
Right r -> collect (r:xs) m
762776

763777
-- concurrentlyE :: IO (Either e a) -> IO (Either e b) -> IO (Either e (a, b))
@@ -770,13 +784,13 @@ concurrentlyE left right = concurrently' left right (collect [])
770784
collect xs m = do
771785
e <- m
772786
case e of
773-
Left ex -> rethrowIO' ex
787+
Left ex -> rethrowIO ex
774788
Right r -> collect (r:xs) m
775789

776790
concurrently' ::
777791
CALLSTACK
778792
IO a -> IO b
779-
-> (IO (Either SomeException (Either a b)) -> IO r)
793+
-> (IO (Either (ExceptionWithContext SomeException) (Either a b)) -> IO r)
780794
-> IO r
781795
concurrently' left right collect = do
782796
done <- newEmptyMVar
@@ -787,10 +801,10 @@ concurrently' left right collect = do
787801
-- the thread to terminate.
788802
lid <- forkIO $ uninterruptibleMask_ $
789803
restore (left >>= putMVar done . Right . Left)
790-
`catchAll` (putMVar done . Left)
804+
`catchNoPropagate` (putMVar done . Left)
791805
rid <- forkIO $ uninterruptibleMask_ $
792806
restore (right >>= putMVar done . Right . Right)
793-
`catchAll` (putMVar done . Left)
807+
`catchNoPropagate` (putMVar done . Left)
794808

795809
count <- newIORef (2 :: Int)
796810
let takeDone = do
@@ -831,7 +845,7 @@ concurrently_ left right = concurrently' left right (collect 0)
831845
collect i m = do
832846
e <- m
833847
case e of
834-
Left ex -> rethrowIO' ex
848+
Left ex -> rethrowIO ex
835849
Right _ -> collect (i + 1 :: Int) m
836850

837851

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)