Skip to content

Commit fb09087

Browse files
committed
feat: annotate 'waitXXX' function with the callsite for 'wait'
Previous commits extended the exception annotations so the default callstack represents the exception location of the original exception in the async process. This callstack also includes where the async process was started (e.g. in `withAsync`). This commits extends the exception context by adding a new `AsyncWaitLocation` exception annotation which contains the location of the `wait` call. Note the usage of `withFrozenCallStack` in order to not expose the internal of the async library in the callstack.
1 parent 16750e9 commit fb09087

1 file changed

Lines changed: 33 additions & 6 deletions

File tree

Control/Concurrent/Async/Internal.hs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ import Data.IORef
5757
import GHC.Exts
5858
import GHC.IO hiding (finally, onException)
5959
import GHC.Conc (ThreadId(..))
60+
import GHC.Stack (CallStack, callStack, prettyCallStack, withFrozenCallStack)
61+
import Control.Exception.Annotation (ExceptionAnnotation (..))
62+
import GHC.Stack.Types (HasCallStack)
6063

6164
#if defined(__MHS__)
6265
import Data.Traversable
@@ -259,15 +262,39 @@ rethrowIO' e =
259262
rethrowIO' = throwIO
260263
#endif
261264

265+
#if MIN_VERSION_base(4,21,0)
266+
-- | An exception annotation which stores the callstack of a 'wait',
267+
-- 'waitBoth', 'waitEither' call.
268+
data AsyncWaitLocation = AsyncWaitLocation CallStack
269+
deriving (Show)
270+
271+
instance ExceptionAnnotation AsyncWaitLocation where
272+
displayExceptionAnnotation (AsyncWaitLocation callstack) = "AsyncWaitLocation " <> prettyCallStack callstack
273+
274+
-- | Annotate an exception with the current callstack with GHC >= 9.12
275+
annotateWithCallSite :: HasCallStack => IO b -> IO b
276+
annotateWithCallSite action = do
277+
resM <- tryWithContext action
278+
case resM of
279+
Right res -> pure res
280+
Left (exc :: ExceptionWithContext SomeException) -> do
281+
annotateIO (AsyncWaitLocation callStack) $ rethrowIO exc
282+
#else
283+
-- | Do nothing with GHC < 9.12
284+
annotateWithCallSite :: HasCallStack => IO b -> IO b
285+
annotateWithCallSite action = action
286+
#endif
287+
288+
262289
-- | Wait for an asynchronous action to complete, and return its
263290
-- value. If the asynchronous action threw an exception, then the
264291
-- exception is re-thrown by 'wait'.
265292
--
266293
-- > wait = atomically . waitSTM
267294
--
268295
{-# INLINE wait #-}
269-
wait :: Async a -> IO a
270-
wait = tryAgain . atomically . waitSTM
296+
wait :: HasCallStack => Async a -> IO a
297+
wait = withFrozenCallStack $ annotateWithCallSite . tryAgain . atomically . waitSTM
271298
where
272299
-- See: https://github.com/simonmar/async/issues/14
273300
tryAgain f = f `catch` \BlockedIndefinitelyOnSTM -> f
@@ -485,8 +512,8 @@ waitEitherCatchCancel left right =
485512
-- re-thrown by 'waitEither'.
486513
--
487514
{-# INLINE waitEither #-}
488-
waitEither :: Async a -> Async b -> IO (Either a b)
489-
waitEither left right = atomically (waitEitherSTM left right)
515+
waitEither :: HasCallStack => Async a -> Async b -> IO (Either a b)
516+
waitEither left right = withFrozenCallStack $ annotateWithCallSite $ atomically (waitEitherSTM left right)
490517

491518
-- | A version of 'waitEither' that can be used inside an STM transaction.
492519
--
@@ -524,8 +551,8 @@ waitEitherCancel left right =
524551
-- re-thrown by 'waitBoth'.
525552
--
526553
{-# INLINE waitBoth #-}
527-
waitBoth :: Async a -> Async b -> IO (a,b)
528-
waitBoth left right = tryAgain $ atomically (waitBothSTM left right)
554+
waitBoth :: HasCallStack => Async a -> Async b -> IO (a,b)
555+
waitBoth left right = withFrozenCallStack $ annotateWithCallSite $ tryAgain $ atomically (waitBothSTM left right)
529556
where
530557
-- See: https://github.com/simonmar/async/issues/14
531558
tryAgain f = f `catch` \BlockedIndefinitelyOnSTM -> f

0 commit comments

Comments
 (0)