-
Notifications
You must be signed in to change notification settings - Fork 12
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
Bluefin is susceptible to #24189 #30
Comments
Thanks for reporting this intriguing issue. Below is the version of your script that I could make run. To check my understanding, is the problem that the async exception thrown to EDIT: Oh, I guess the problem is that the async exception gets stored in the thunk, so it is thrown when then thunk is later evaluated during the #!/usr/bin/env cabal
{- cabal:
build-depends: base, bluefin
-}
{-# OPTIONS_GHC -fno-omit-yields #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE GHC2021 #-}
-- base
import Data.Functor (void)
import Control.Concurrent (forkIO, killThread)
import Control.Exception (evaluate)
-- bluefin
import Bluefin.Eff (runPureEff)
import Bluefin.Exception (handle)
main :: IO ()
main = do
let
n = 200_000_000
eff_thunk = runPureEff do
handle (\() -> pure False) \_ -> do
delay n do
pure True
eval_thread <- forkIO do
void (evaluate eff_thunk)
delay (n `div` 2) do
killThread eval_thread
print eff_thunk
where
delay :: Int -> a -> a
delay n x = sum [1..n] `seq` x
|
#!/usr/bin/env cabal
{- cabal:
build-depends: base, effectful-core
-}
{-# OPTIONS_GHC -fno-omit-yields #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE GHC2021 #-}
-- base
import Data.Functor (void)
import Control.Concurrent (forkIO, killThread)
import Control.Exception (evaluate)
-- bluefin
import Effectful (runPureEff)
import Effectful.Error.Static (runError)
main :: IO ()
main = do
let
n = 200_000_000
eff_thunk = runPureEff do
r <- runError do
delay n do
pure True
pure $ case r of
Left (_, l) -> l
Right r -> r
eval_thread <- forkIO do
void (evaluate eff_thunk)
delay (n `div` 2) do
killThread eval_thread
print eff_thunk
where
delay :: Int -> a -> a
delay n x = sum [1..n] `seq` x
|
Oh no, a subtle change shows that #!/usr/bin/env cabal
{- cabal:
build-depends: base, effectful-core
-}
{-# OPTIONS_GHC -fno-omit-yields #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE GHC2021 #-}
-- base
import Data.Functor (void)
import Control.Concurrent (forkIO, killThread)
import Control.Exception (evaluate)
-- bluefin
import Effectful (runPureEff)
import Effectful.Error.Static (runError)
main :: IO ()
main = do
let
n = 200_000_000
eff_thunk = runPureEff do
r <- runError do
delay n do
pure True
pure $ case r of
-- Difference here: we catch `()` and return `False`
Left (_, ()) -> False
Right r -> r
eval_thread <- forkIO do
void (evaluate eff_thunk)
delay (n `div` 2) do
killThread eval_thread
print eff_thunk
where
delay :: Int -> a -> a
delay n x = sum [1..n] `seq` x
|
Actually, the situation is weirder with #!/usr/bin/env cabal
{- cabal:
build-depends: base, effectful-core
-}
{-# OPTIONS_GHC -fno-omit-yields #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE GHC2021 #-}
-- base
import Data.Functor (void)
import Control.Concurrent (forkIO, killThread)
import Control.Exception (evaluate)
-- bluefin
import Effectful (runPureEff)
import Effectful.Error.Static (runError)
main :: IO ()
main = do
let
n = 200_000_000
eff_thunk = runPureEff do
r <- runError do
delay n do
pure True
pure $ case r of
Left (_, ()) -> False
Right r -> r
eval_thread <- forkIO do
void (evaluate eff_thunk)
delay (n `div` 2) do
killThread eval_thread
putStrLn "After killThread"
print eff_thunk
where
delay :: Int -> a -> a
delay n x = sum [1..n] `seq` x
|
The best I have been able to come up with is bluefin/bluefin-internal/src/Bluefin/Internal.hs Lines 1322 to 1368 in d6f8995
The equivalent of your point of Alexis's example is at: bluefin/bluefin-internal/test/Main.hs Lines 44 to 68 in d6f8995
Then it runs as desired:
I think what happens, based on Alexis's explanation on GitLab, is that when the async exception arrives the
I have no idea if this is actually correct or just seems to work so it would be good to try to get confirmation somehow. Footnotes
|
Oh, and it would be nice if I could have done that with an existing structured concurrency abstraction like |
Both the issue and the workaround are complicated and subtle. The IssueThe issue can be broken down into two key points:
In the original reproducer, the first point interferes disastrously with the second, resulting not only in lost work but even an exploding update. Working Around ItTo craft a workaround, you just need to address these points—lets rederive lexi-lambda's solution. Rethrowing AsynchronouslyFor the first point, we want an alternative rethrowAsync :: Exception e => e -> IO ()
rethrowAsync e = do
me <- myThreadId
throwTo me e That looks good. catchPreserveAsync :: Exception e => IO a -> (e -> IO a) -> IO a
catchPreserveAsync act h = catches act
[ Handler h
, Handler \(e :: SomeAsyncException) -> rethrowAsync e
] But catchPreserveAsync :: Exception e => IO a -> (e -> IO a) -> IO a
catchPreserveAsync act h = fix \reset -> catches act
[ Handler h
, Handler \(e :: SomeAsyncException) -> rethrowAsync e >> reset
] But as lexi notes, a recursive call in the handler is implicitly masked; we want one last tweak to unmask it: catchPreserveAsync :: Exception e => IO a -> (e -> IO a) -> IO a
catchPreserveAsync act h = mask \restore -> fix \reset -> restore $ catches act
[ Handler h
, Handler \(e :: SomeAsyncException) -> rethrowAsync e >> reset
] Though imperfect, this is already a functioning workaround! Conserving WorkThough conserve :: IO a -> IO a
conserve act = evaluate thunk
where
{-# NOINLINE thunk #-}
thunk = unsafePerformIO act Now when At this point the rederivation is already complete; if you put these pieces together and inline their definitions you'll arive at something equivalent to what lexi wrote. SolutionsUnlike their The simplest approach is to give up on conserving work and just implement them in terms of A more complete and adventurous option would be to use conserve :: NFData a => IO a -> IO a
conserve act = evaluate (force thunk)
where
{-# NOINLINE thunk #-}
thunk = unsafePerformIO act |
Very clear derivation, thanks! I wonder why we don't catch all exceptions in the second branch, i.e. -- Not SomeAsyncException
Handler \(e :: SomeException) -> rethrowAsync e >> reset It seems to me that is still correct (because it will do the correct thing on synchronous exceptions too) and safer (because In any case, I think that's moot for Bluefin, since I'm happy with |
Due to the implementation of GHC's exception mechanism,
catch
ing underunsafePerformIO
has a bad interaction with async exceptions. The issue and some workarounds are detailed in #24189.A port of lexi-lambda's reproducer to bluefin:
This program terminates with an exception, but will conclude successfully if you comment out the redundant
handle
.The text was updated successfully, but these errors were encountered: