It seems that a piece of code using runPy do not react to async exception: during the FFI call to python, haskell main thread is blocked and cannot propagate the exception to python.
The solutions I am aware are:
For the cooperative version, the idea is to run the runPy in one haskell thread and concurrently another haskell thread which upon interruption will write on the shared memory. Something like:
alloca $ \ptr -> do
poke ptr 0
race (the_run_py_thread ptr) $ do
some_action_to_wait `finally` poke ptr 1
Where the_run_py_thread does the runPy and pass the ptr to the python process which is then responsible to read it regularly and stop its computation if the value change to 1.
Note that "callback" to haskell cannot receive the async exception either, so one solution is to run the callback computation in the main thread, something like:
{-# LANGUAGE QuasiQuotes #-}
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.DeepSeq (NFData, force)
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class (MonadIO (..))
import Data.IORef
import Data.Primitive (Ptr)
import Python.Inline
import Python.Inline.QQ (pye, pymain)
main = do
initializePython
runPy
[pymain|
import math
def foo(cb):
i = 0
while True:
(shouldStop, res) = cb(i)
if shouldStop:
raise "End"
i += 1
|]
let f :: Int -> IO ()
f x = do
if x `mod` 100000 == 0
then print (x :: Int)
else pure ()
withHaskellCallback f $ \cont -> do
runPy [pye|foo(cont_hs)|]
-- | transform an haskell function into a callback for 'runPy' with an
-- additionnal property that an async exception ends the callback immediatly
-- and it returns (True, None) instead of (False, result)
withHaskellCallback :: (NFData res, ToPy res) => (a -> IO res) -> ((a -> Py PyObject) -> IO x) -> IO x
withHaskellCallback initial_function cont = do
input <- newEmptyTMVarIO
output <- newEmptyTMVarIO
stopVar <- newTVarIO False
let action_in_ffi = cont $ \x -> do
stop <- liftIO $ atomically $ do
stop <- readTVar stopVar
if stop
then pure True
else do
putTMVar input x
pure False
if stop
then
toPy (True, ())
else do
resM <- liftIO $ atomically $ do
stop <- readTVar stopVar
if stop
then pure Nothing
else do
res <- takeTMVar output
pure (Just res)
case resM of
Nothing -> toPy (True, ())
Just res -> toPy (False, res)
worker_in_main = do
( forever $ do
x <- atomically $ takeTMVar input
res <- initial_function x
-- \| Force evaluation to NF
res_evaluated <- evaluate (force res)
atomically $ putTMVar output res_evaluated
)
`finally` do
atomically $ do
writeTVar stopVar True
result <- race action_in_ffi worker_in_main
case result of
Right (e :: ExceptionWithContext SomeException) -> throwIO e
Left result -> pure result
I'm documenting that workaround because I'm facing this issue at work currently. Maybe we can discuss how a robust solution could be introduced in inline-python.
It seems that a piece of code using
runPydo not react to async exception: during the FFI call to python, haskell main thread is blocked and cannot propagate the exception to python.The solutions I am aware are:
SIGPIPE(on linux).For the cooperative version, the idea is to run the runPy in one haskell thread and concurrently another haskell thread which upon interruption will write on the shared memory. Something like:
Where
the_run_py_threaddoes therunPyand pass theptrto the python process which is then responsible to read it regularly and stop its computation if the value change to 1.Note that "callback" to haskell cannot receive the async exception either, so one solution is to run the callback computation in the main thread, something like:
{-# LANGUAGE QuasiQuotes #-} import Control.Concurrent import Control.Concurrent.Async import Control.Concurrent.STM import Control.DeepSeq (NFData, force) import Control.Exception import Control.Monad import Control.Monad.IO.Class (MonadIO (..)) import Data.IORef import Data.Primitive (Ptr) import Python.Inline import Python.Inline.QQ (pye, pymain) main = do initializePython runPy [pymain| import math def foo(cb): i = 0 while True: (shouldStop, res) = cb(i) if shouldStop: raise "End" i += 1 |] let f :: Int -> IO () f x = do if x `mod` 100000 == 0 then print (x :: Int) else pure () withHaskellCallback f $ \cont -> do runPy [pye|foo(cont_hs)|] -- | transform an haskell function into a callback for 'runPy' with an -- additionnal property that an async exception ends the callback immediatly -- and it returns (True, None) instead of (False, result) withHaskellCallback :: (NFData res, ToPy res) => (a -> IO res) -> ((a -> Py PyObject) -> IO x) -> IO x withHaskellCallback initial_function cont = do input <- newEmptyTMVarIO output <- newEmptyTMVarIO stopVar <- newTVarIO False let action_in_ffi = cont $ \x -> do stop <- liftIO $ atomically $ do stop <- readTVar stopVar if stop then pure True else do putTMVar input x pure False if stop then toPy (True, ()) else do resM <- liftIO $ atomically $ do stop <- readTVar stopVar if stop then pure Nothing else do res <- takeTMVar output pure (Just res) case resM of Nothing -> toPy (True, ()) Just res -> toPy (False, res) worker_in_main = do ( forever $ do x <- atomically $ takeTMVar input res <- initial_function x -- \| Force evaluation to NF res_evaluated <- evaluate (force res) atomically $ putTMVar output res_evaluated ) `finally` do atomically $ do writeTVar stopVar True result <- race action_in_ffi worker_in_main case result of Right (e :: ExceptionWithContext SomeException) -> throwIO e Left result -> pure resultI'm documenting that workaround because I'm facing this issue at work currently. Maybe we can discuss how a robust solution could be introduced in inline-python.