tbodt/v8py

cant 't call pyfunc in async mode

RonaldinhoL opened this issue · 5 comments

if i call py function in async, then will cause exception

import asyncio
import time

import v8py

class Test():

    async def aysnc_func(self):
        for i in range(0, 3):
            print("sleep " + str(i))
            await asyncio.sleep(1)

    def sync_func(self):
        print("iam sync ")
        time.sleep(1)

async def main():
    ctx = v8py.Contenxt()
    ctx.expose(Test)
    ctx.eval("""test = new Test()""")
    ctx.eval("""
    test.sync_func()
    """)
    print("main end...")

asyncio.run(main())

StopIteration

The above exception was the direct cause of the following exception:

SystemError: _PyEval_EvalFrameDefault returned a result with an error set

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "xxx\demo.py", line 59, in
asyncio.run(main())
File "C:\Users\xxx.pyenv\pyenv-win\versions\Python-3.9.4\lib\asyncio\runners.py", line 44, in run
return loop.run_until_complete(main)
File "C:\Users\xxx.pyenv\pyenv-win\versions\Python-3.9.4\lib\asyncio\base_events.py", line 642, in run_until_complete
return future.result()
SystemError: _PyEval_EvalFrameDefault returned a result with an error set

This may be related to boostorg/python#374, wherein certain Boost operations in C++ destructors can cause the interpreter to bug out in an async function.

You could check whether or not that is the case by forcing destruction of your locals before returning from the async function, like so:

def _inner():
    ctx = v8py.Contenxt()
    ctx.expose(Test)
    ctx.eval("""test = new Test()""")
    ctx.eval("""
    test.sync_func()
    """)

async def main():
    _inner()
    print("main end...")

If that solves the problem, you may be affected by the linked issue (and you should mention that in its comments!). If not, something else is likely going on.

As an aside, it also looks like your interactions with v8py do not need to be asynchronous at all. If your goal is to run v8py-invoking code in a larger async context, you may benefit from interacting with v8py exclusively from synchronous functions and calling those via asyncio.to_thread or run_in_executor.

yea, i try you code, it exit normally and not raise exception. you should mention that in its comments! sorry idk what you mean ...

actually i didn't understand what is the diffrence between the two case

@RonaldinhoL the issue has to do with when destructors are run. Python garbage collects things (and runs their destructors) when the reference count of an object drops to zero. If you allocate something in a function, when that function returns, that thing's reference count drops to zero and it is garbage collected--this happens in a weird space after the function returns, but before control is returned to whatever called that function in the first place.

Normally this works fine. However, if post-return destructors are run in async functions, something odd happens if those destructors are run in native (C++ in the case of v8py) code. Because async functions aren't really functions (they're actually generators, which return inside the guts of Python by raising StopIteration), destructors running after an async function's return seem to have trouble since the interpreter is in an unexpected state of "handling an exception".

That's about as much as I understand; the issue might be with the way C++ is bound to python in this library, or it might be with Python itself (I filed an issue on boost-python for the former, and one on the Python core for the latter, but I'm not quite sure as to the "why" here, or if either issue is expected behavior).

what is the diffrence between the two case

In the changes I proposed, post-return destruction happens in a synchronous function. In _inner, the ctx object's destructor runs right "after the last line" of that function. In your original code, the destructor ran after the last line of main, which is an async function, thus triggering the error.

You could also solve the problem by calling del ctx before returning, and forcing immediate garbage collection, or by naming ctx a global variable and postponing garbage collection until the program shuts down.

thanks for your patience and explanation 👍 :)