Skip to content

Commit bbc9124

Browse files
authored
Ensure we handle threadsafe shutdown under PyPy with multiple event loops (#800)
1 parent 9961dce commit bbc9124

3 files changed

Lines changed: 51 additions & 6 deletions

File tree

tests/test_core.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,3 +604,28 @@ async def test_open_close_twice_from_async() -> None:
604604
zc = Zeroconf(interfaces=['127.0.0.1'])
605605
zc.close()
606606
zc.close()
607+
await asyncio.sleep(0)
608+
609+
610+
@pytest.mark.asyncio
611+
async def test_multiple_sync_instances_stared_from_async_close():
612+
"""Test we can shutdown multiple sync instances from async."""
613+
614+
# instantiate a zeroconf instance
615+
zc = Zeroconf(interfaces=['127.0.0.1'])
616+
zc2 = Zeroconf(interfaces=['127.0.0.1'])
617+
618+
assert zc.loop == zc2.loop
619+
620+
zc.close()
621+
assert zc.loop.is_running()
622+
zc2.close()
623+
assert zc2.loop.is_running()
624+
625+
zc3 = Zeroconf(interfaces=['127.0.0.1'])
626+
assert zc3.loop == zc2.loop
627+
628+
zc3.close()
629+
assert zc3.loop.is_running()
630+
631+
await asyncio.sleep(0)

tests/utils/test_aio.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,27 @@
66

77
import asyncio
88
import contextlib
9+
import unittest.mock
910

1011
import pytest
1112

1213
from zeroconf._utils import aio as aioutils
1314

1415

16+
@pytest.mark.asyncio
17+
async def test_async_get_all_tasks() -> None:
18+
"""Test we can get all tasks in the event loop.
19+
20+
We make sure we handle RuntimeError here as
21+
this is not thread safe under PyPy
22+
"""
23+
await aioutils._async_get_all_tasks(aioutils.get_running_loop())
24+
if not hasattr(asyncio, 'all_tasks'):
25+
return
26+
with unittest.mock.patch("zeroconf._utils.aio.asyncio.all_tasks", side_effect=RuntimeError):
27+
await aioutils._async_get_all_tasks(aioutils.get_running_loop())
28+
29+
1530
@pytest.mark.asyncio
1631
async def test_get_running_loop_from_async() -> None:
1732
"""Test we can get the event loop."""

zeroconf/_utils/aio.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,18 @@ def _handle_wait_complete(_: asyncio.Task) -> None:
6262
await event_wait
6363

6464

65-
async def _get_all_tasks(loop: asyncio.AbstractEventLoop) -> List[asyncio.Task]:
65+
async def _async_get_all_tasks(loop: asyncio.AbstractEventLoop) -> List[asyncio.Task]:
6666
"""Return all tasks running."""
6767
await asyncio.sleep(0) # flush out any call_soon_threadsafe
68-
# Make a copy of the tasks in case they change during iteration
69-
if hasattr(asyncio, 'all_tasks'):
70-
return list(asyncio.all_tasks(loop)) # type: ignore # pylint: disable=no-member
71-
return list(asyncio.Task.all_tasks(loop)) # type: ignore # pylint: disable=no-member
68+
# If there are multiple event loops running, all_tasks is not
69+
# safe EVEN WHEN CALLED FROM THE EVENTLOOP
70+
# under PyPy so we have to try a few times.
71+
for _ in range(3):
72+
with contextlib.suppress(RuntimeError):
73+
if hasattr(asyncio, 'all_tasks'):
74+
return asyncio.all_tasks(loop) # type: ignore # pylint: disable=no-member
75+
return asyncio.Task.all_tasks(loop) # type: ignore # pylint: disable=no-member
76+
return []
7277

7378

7479
async def _wait_for_loop_tasks(wait_tasks: Set[asyncio.Task]) -> None:
@@ -78,7 +83,7 @@ async def _wait_for_loop_tasks(wait_tasks: Set[asyncio.Task]) -> None:
7883

7984
def shutdown_loop(loop: asyncio.AbstractEventLoop) -> None:
8085
"""Wait for pending tasks and stop an event loop."""
81-
pending_tasks = set(asyncio.run_coroutine_threadsafe(_get_all_tasks(loop), loop).result())
86+
pending_tasks = set(asyncio.run_coroutine_threadsafe(_async_get_all_tasks(loop), loop).result())
8287
done_tasks = set(task for task in pending_tasks if not task.done())
8388
pending_tasks -= done_tasks
8489
if pending_tasks:

0 commit comments

Comments
 (0)