-
Notifications
You must be signed in to change notification settings - Fork 71
feat: support for exception context propagation #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
aa03c27 to
91c00c5
Compare
|
Is there anything else that needs to be done while this is WIP? It would be excellent for exception annotations to propagate through. |
|
Sorry, the only reason is that I had not took time to work on that. I'm using this MR since month at work and did not had any surprising behavior. Could be turned to "ready", if it builds with older GHC (I think so), I don't see any reason why not processing any further. Even if I missed some proper |
|
No worries, and thanks for the work! I tried backporting this to But sadly GHC 9.10 seems noisier - your example code gives If you spot any obvious problem, let me know. |
My understanding of As always with exception, I need to experiment to really understand what is going on (I have an infinite respect for people who can reason about exception in their head without experimenting ;)
I need to get back into this MR actually. It seems that your branch mrkline@bb9d703 is based on one commit that is not what's inside this MR. Your commit is bb9d703, which is really alike the commit on my local repo, 5cbf92365b4501d9c102b810e8f6159b778ae615, they all are on branch I'm lost in the history there, sorry, it may be my fault (not worked on this MR since a long time and I'm used to try a lot of thing locally, change branch, force push, ...). Do you remember how you ended with commit bb9d703 and branch |
|
Using GHC 9.12: Indeed, With no (So I'm wrong, throwing with However, throwing an exception caught with So as a first approximation, it seems that your implementation should be correct. I'll continue the experimentation, maybe that's different with ghc 9.10. |
|
With GHC 9.10.2: There is no difference between This is surprising. |
|
Haa, I think I got it. The problem is that in newest base, From
So the reason for the duplicate call stack is that it is part of the context AND part of To be honest, I think we can keep that. This is painful, indeed, but that's part of a work in progress in the exception handling logic. Adding more complexity would, IMHO, just lead to problem in the future. |
|
Also bear in mind that |
|
Seems reasonable. Could we have a test please? |
I'm doing that right now. |
|
I do have a question for you, regarding the test. The idea of test is to throw an exception in an async context (e.g. Exceptions and backtrace are an opaque feature, we can get the backtrace, display it (see https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception-Backtrace.html#t:Backtraces), but nothing more without using a GHC.Internal module. And unfortunately, the displayed value contains volatile items (such as the line numbers). My idea was to compare the exception caught from Is it OK if I just pick one line of the backtrace and compare (I've tested, it works), and we'll fix later if it break with a future GHC version, or should I go a bit deeper inside the GHC.Internal relevant module in order to extract more precise informations, at the risk of more breakage with next GHC version? I'll continue with the first approach (e.g. working on the |
d374da2 to
d0f21f8
Compare
|
Please take a look at the newly added tests. Before merging, give me a bit of time. I would like to see if we can add context in the "non-local" operations so the rethrow location is not wasted. e.g. when running |
|
Thanks @guibou, looks good to me. In theory you should be able to use |
d0f21f8 to
c16d82d
Compare
|
I'm a bit annoyed. Take this code sample: data Ann = Ann String
deriving (Show, ExceptionAnnotation)
data Exc' = Exc' String
deriving (Show, Exception)
asyncTask :: HasCallStack => IO ()
asyncTask = annotateIO (Ann "bonjour") $ do
throwIO (Exc' "yoto")
asyncTask' :: HasCallStack => IO ()
asyncTask' = annotateIO (Ann "bonjour2") $ do
throwIO (Exc' "yutu")
t = do
withAsync asyncTask $ \t ->
wait t
-- concurrently asyncTask asyncTask'The current output is: Here I'm annoyed by:
I think the correct solution would be to add some sort of |
c16d82d to
3064e24
Compare
|
In the meantime, I've rebased the branch AND cleaned build with ghc 9.8 -> 9.14. |
We specialize the `throwIO` call using a newly implemented `rethrowIO'`
which behaves as `rethrowIO` from base 4.21 when available or like the
previous `throw` implementation.
In short:
- Before `base-4.21`, the code is exactly as before
- After `base-4.21`, the code does not override the backtrace
annotations and instead uses `rethrowIO`.
Example of usage / changes:
The following code:
```haskell
{-# LANGUAGE DeriveAnyClass #-}
import Control.Concurrent.Async
import Control.Exception
import Control.Exception.Context
import Control.Exception.Annotation
import Data.Typeable
import Data.Traversable
import GHC.Stack
data Ann = Ann String
deriving (Show, ExceptionAnnotation)
asyncTask :: HasCallStack => IO ()
asyncTask = annotateIO (Ann "bonjour") $ do
error "yoto"
asyncTask' :: HasCallStack => IO ()
asyncTask' = annotateIO (Ann "bonjour2") $ do
error "yutu"
main = do
-- withAsync asyncTask wait
concurrently asyncTask asyncTask'
-- race asyncTask asyncTask'
```
When run without this commit leads to:
```
ASyncException.hs: Uncaught exception ghc-internal:GHC.Internal.Exception.ErrorCall:
yoto
HasCallStack backtrace:
throwIO, called at ./Control/Concurrent/Async/Internal.hs:630:24 in async-2.2.5-50rpfAJ7BEc1o5OswtTMUN:Control.Concurrent.Async.Internal
```
When run with this commit:
```
*** Exception: yoto
Ann "bonjour"
HasCallStack backtrace:
error, called at /home/guillaume//ASyncException.hs:15:3 in async-2.2.5-inplace:Main
asyncTask, called at /home/guillaume//ASyncException.hs:23:16 in async-2.2.5-inplace:Main
```
3cd0e27 to
fb09087
Compare
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.
fb09087 to
3d5cc30
Compare
|
@simonmar I've fixed the Regarding the fact that the "non-local" primitive do not appear in the callstack (e.g. For example, using the previous example, an exception will now be displayed with a callstack locating the exception original I've tested that it builds and run tests with GHC 9.8 -> 9.14. There are no tests for the new annotation for |
This change is in order to have exception context propagation.
Everywhere we "re"-
throwexception withthrow, we instead userethrowwhich does not remove the exception context. I've introducedrethrowIO'which is just a backward compatibility wrapper overrethrowIOandthrowIO.I will use this commit at work for a bit of time in order to gather some feedback and maybe comeback with a more robust solution.
Example of usage / changes:
The following code:
{-# LANGUAGE DeriveAnyClass #-} import Control.Concurrent.Async import Control.Exception import Control.Exception.Context import Control.Exception.Annotation import Data.Typeable import Data.Traversable import GHC.Stack data Ann = Ann String deriving (Show, ExceptionAnnotation) asyncTask :: HasCallStack => IO () asyncTask = annotateIO (Ann "bonjour") $ do error "yoto" asyncTask' :: HasCallStack => IO () asyncTask' = annotateIO (Ann "bonjour2") $ do error "yutu" main = do -- withAsync asyncTask wait concurrently asyncTask asyncTask' -- race asyncTask asyncTask'When run without this commit leads to:
When run with this commit: