Skip to content

runPy, callback and async exception. #48

Description

@guibou

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 #-}
importControl.ConcurrentimportControl.Concurrent.AsyncimportControl.Concurrent.STMimportControl.DeepSeq (NFData, force)
importControl.ExceptionimportControl.MonadimportControl.Monad.IO.Class (MonadIO (..))
importData.IORefimportData.Primitive (Ptr)
importPython.InlineimportPython.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 =doif x `mod`100000==0thenprint (x ::Int)
elsepure()
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:: (NFDatares, ToPyres) => (a->IOres) -> ((a->PyPyObject) ->IOx) ->IOx
withHaskellCallback initial_function cont =do
input <- newEmptyTMVarIO
output <- newEmptyTMVarIO
stopVar <- newTVarIO Falselet action_in_ffi = cont $\x ->do
stop <- liftIO $ atomically $do
stop <- readTVar stopVar
if stop
thenpureTrueelsedo
putTMVar input x
pureFalseif stop
then
toPy (True, ())
elsedo
resM <- liftIO $ atomically $do
stop <- readTVar stopVar
if stop
thenpureNothingelsedo
res <- takeTMVar output
pure (Just res)
case resM ofNothing-> 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 ofRight (e ::ExceptionWithContextSomeException) -> 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions