-
Notifications
You must be signed in to change notification settings - Fork 104
Handle async proxy calls after event loop closes #730
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: dev
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,23 @@ | |
| ) | ||
| ) | ||
|
|
||
| if asyncio.iscoroutinefunction(func): | ||
|
Check warning on line 91 in bellows/thread.py
|
||
|
|
||
| async def async_func_wrapper(*args, **kwargs): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Behaviour note for the record — no change requested. Making this an All four in-repo call sites await immediately, so nothing breaks today. But it does mean:
Probably worth a one-line docstring or comment on the wrapper so the eager-vs-lazy distinction isn't rediscovered later. |
||
| loop = self._obj_loop | ||
| curr_loop = asyncio.get_running_loop() | ||
| call = functools.partial(func, *args, **kwargs) | ||
| if loop == curr_loop: | ||
| return await call() | ||
| if loop.is_closed(): | ||
| # Disconnected | ||
| LOGGER.warning("Attempted to use a closed event loop") | ||
| return None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes a closed loop a silent success for all async proxy methods, which is broader than the bug being fixed.
On One option: raise a |
||
| future = asyncio.run_coroutine_threadsafe(call(), loop) | ||
| return await asyncio.wrap_future(future, loop=curr_loop) | ||
|
|
||
| return async_func_wrapper | ||
|
|
||
| def func_wrapper(*args, **kwargs): | ||
| loop = self._obj_loop | ||
| curr_loop = asyncio.get_running_loop() | ||
|
|
@@ -98,21 +115,17 @@ | |
| # Disconnected | ||
| LOGGER.warning("Attempted to use a closed event loop") | ||
| return | ||
| if asyncio.iscoroutinefunction(func): | ||
| future = asyncio.run_coroutine_threadsafe(call(), loop) | ||
| return asyncio.wrap_future(future, loop=curr_loop) | ||
| else: | ||
|
|
||
| def check_result_wrapper(): | ||
| result = call() | ||
| if result is not None: | ||
| raise TypeError( | ||
| ( | ||
| "ThreadsafeProxy can only wrap functions with no return" | ||
| "value \nUse an async method to return values: {}.{}" | ||
| ).format(self._obj.__class__.__name__, name) | ||
| ) | ||
|
|
||
| loop.call_soon_threadsafe(check_result_wrapper) | ||
|
|
||
| def check_result_wrapper(): | ||
| result = call() | ||
| if result is not None: | ||
| raise TypeError( | ||
| ( | ||
| "ThreadsafeProxy can only wrap functions with no return" | ||
| "value \nUse an async method to return values: {}.{}" | ||
| ).format(self._obj.__class__.__name__, name) | ||
| ) | ||
|
|
||
| loop.call_soon_threadsafe(check_result_wrapper) | ||
|
|
||
| return func_wrapper | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,20 @@ async def test_proxy_loop_closed(): | |
| assert obj.test.call_count == 0 | ||
|
|
||
|
|
||
| async def test_proxy_async_loop_closed(): | ||
| loop = asyncio.new_event_loop() | ||
| obj = mock.MagicMock() | ||
|
|
||
| async def test(): | ||
| return mock.sentinel.result | ||
|
|
||
| obj.test = test | ||
| proxy = ThreadsafeProxy(obj, loop) | ||
| loop.close() | ||
|
|
||
| assert await proxy.test() is None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test proves the call is awaitable and yields
Also worth considering: a test pinning the new eager-vs-lazy semantics (that calling without awaiting schedules nothing), so a future refactor back to a sync wrapper doesn't silently flip it. |
||
|
|
||
|
|
||
| async def test_thread_task_cancellation_after_stop(thread): | ||
| loop = asyncio.get_event_loop() | ||
| obj = mock.MagicMock() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional drive-by, since this line is being moved anyway:
asyncio.iscoroutinefunctionis deprecated and slated for removal in Python 3.16. It's already emitting aDeprecationWarningin the test suite on this file:I checked that
inspect.iscoroutinefunctionis a drop-in for every callable shape this proxy sees — boundasync defmethods (the real case, e.g.Gateway.send_data), plainasync def,AsyncMock, andMagicMockattributes all agree between the two. Pre-existing, so feel free to leave it for a separate PR.