-
Notifications
You must be signed in to change notification settings - Fork 1k
fix: use SelectorEventLoop for asyncio on Windows to avoid the unity bridge closed #665
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
Changes from all commits
437dbf2
a1f4db1
cbad6f7
a25aff4
146a352
7caf958
60d0d60
1d505ad
caf6906
c688c37
993043f
221e343
624a85f
fb3c0ac
616789d
a16beac
c217f4c
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 |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| """ | ||
| Test event loop configuration for Windows. | ||
|
|
||
| This module verifies that the correct asyncio loop factory is selected | ||
| on different platforms to prevent WinError 64 on Windows. | ||
|
|
||
| WinError 64 occurs when using ProactorEventLoop with concurrent | ||
| WebSocket and HTTP connections. The fix is to use SelectorEventLoop | ||
| on Windows. | ||
|
|
||
| Related fix: Server/src/main.py | ||
| """ | ||
|
|
||
| import sys | ||
| import asyncio | ||
| from functools import partial | ||
| import pytest | ||
|
|
||
|
|
||
| @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific") | ||
| def test_windows_uses_selector_event_loop_factory(): | ||
| """ | ||
| Verify that Windows uses SelectorEventLoop via loop_factory. | ||
|
|
||
| This prevents WinError 64 when handling concurrent WebSocket and HTTP connections. | ||
|
|
||
| Regression test for Windows asyncio bug where ProactorEventLoop's IOCP | ||
| has race conditions with rapid connection changes. | ||
|
|
||
| The fix is applied in Server/src/main.py | ||
| """ | ||
| import importlib | ||
| import main # type: ignore[import] - conftest.py adds src to sys.path | ||
| importlib.reload(main) | ||
|
|
||
| loop_factory = main._get_asyncio_loop_factory() | ||
| assert loop_factory is asyncio.SelectorEventLoop, ( | ||
| "Expected SelectorEventLoop for Windows loop_factory" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.skipif(sys.platform == "win32", reason="Non-Windows only") | ||
| def test_non_windows_uses_default_loop_factory(): | ||
| """ | ||
| Verify that non-Windows platforms keep their default event loop behavior. | ||
|
|
||
| SelectorEventLoop should only be used on Windows to avoid the IOCP bug. | ||
| Other platforms should use their optimal default event loop. | ||
| """ | ||
| import importlib | ||
| import main # type: ignore[import] - conftest.py adds src to sys.path | ||
| importlib.reload(main) | ||
|
|
||
| loop_factory = main._get_asyncio_loop_factory() | ||
| assert loop_factory is None, "Non-Windows platforms should not set a loop_factory" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_async_operations_use_correct_event_loop(): | ||
| """ | ||
| Smoke test to verify async operations work with the configured event loop. | ||
|
|
||
| This test creates a simple async operation to ensure the event loop | ||
| is functional. It doesn't test WinError 64 directly (which is a | ||
| timing-dependent race condition), but confirms the basic async | ||
| infrastructure works with the policy configured in main.py. | ||
| """ | ||
|
whatevertogo marked this conversation as resolved.
|
||
| # Import main to ensure the event loop policy is configured | ||
| import importlib | ||
| import main # type: ignore[import] - conftest.py adds src to sys.path | ||
| importlib.reload(main) | ||
|
|
||
| # Simple async operation | ||
| async def simple_task(): | ||
| await asyncio.sleep(0.01) | ||
| return "success" | ||
|
|
||
| # Should complete without errors | ||
| result = await simple_task() | ||
| assert result == "success" | ||
|
|
||
| # Verify we're using the expected event loop | ||
| # Use get_running_loop() as we're in an async context | ||
| loop = asyncio.get_running_loop() | ||
| assert loop is not None, "Event loop should be running" | ||
| assert loop.is_running(), "Event loop should be in running state" | ||
|
Comment on lines
+58
to
+86
Contributor
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. Reloading
Since this test only verifies that basic async operations work, the Proposed fix: remove the reload from the async test # Import main to ensure the event loop policy is configured
- import importlib
import main
- importlib.reload(main)
+ # main's module-level code has already configured the policy on first import
# Simple async operation🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_run_mcp_uses_fastmcp_run_when_no_loop_factory(monkeypatch: pytest.MonkeyPatch): | ||
| """When no loop factory is needed, _run_mcp should delegate to FastMCP.run.""" | ||
| import importlib | ||
| import main # type: ignore[import] - conftest.py adds src to sys.path | ||
| importlib.reload(main) | ||
|
|
||
| class DummyMCP: | ||
| def __init__(self) -> None: | ||
| self.run_called = False | ||
| self.run_kwargs = {} | ||
|
|
||
| def run(self, **kwargs): | ||
| self.run_called = True | ||
| self.run_kwargs = kwargs | ||
|
|
||
| monkeypatch.setattr(main, "_get_asyncio_loop_factory", lambda: None) | ||
|
|
||
| mcp = DummyMCP() | ||
| main._run_mcp(mcp, transport="stdio") | ||
|
|
||
| assert mcp.run_called | ||
| assert mcp.run_kwargs["transport"] == "stdio" | ||
|
|
||
|
|
||
| def test_run_mcp_uses_anyio_with_loop_factory(monkeypatch: pytest.MonkeyPatch): | ||
| """When loop factory exists, _run_mcp should use anyio.run with backend options.""" | ||
| import importlib | ||
| import main # type: ignore[import] - conftest.py adds src to sys.path | ||
| importlib.reload(main) | ||
|
|
||
| class DummyMCP: | ||
| async def run_async(self, transport, show_banner=True, **kwargs): | ||
| return None | ||
|
|
||
| captured = {} | ||
|
|
||
| def fake_anyio_run(func, *args, **kwargs): | ||
| captured["func"] = func | ||
| captured["args"] = args | ||
| captured["kwargs"] = kwargs | ||
| return None | ||
|
|
||
| monkeypatch.setattr(main, "anyio", type("AnyIOStub", (), {"run": staticmethod(fake_anyio_run)})) | ||
| monkeypatch.setattr(main, "_get_asyncio_loop_factory", lambda: asyncio.SelectorEventLoop) | ||
|
|
||
| mcp = DummyMCP() | ||
| main._run_mcp(mcp, transport="http", host="localhost", port=8080) | ||
|
|
||
| assert isinstance(captured["func"], partial) | ||
| assert captured["func"].args[0] == "http" | ||
| assert captured["func"].keywords["show_banner"] is True | ||
| assert captured["func"].keywords["host"] == "localhost" | ||
| assert captured["func"].keywords["port"] == 8080 | ||
| assert captured["kwargs"]["backend_options"]["loop_factory"] is asyncio.SelectorEventLoop | ||
|
|
||
|
|
||
| @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific") | ||
| def test_windows_loop_factory_prevents_proactor(): | ||
| """ | ||
| Verify that Windows loop_factory explicitly avoids ProactorEventLoop. | ||
|
|
||
| ProactorEventLoop uses IOCP which has known issues with WinError 64 | ||
| when handling rapid connection changes. This test confirms we're | ||
| not using ProactorEventLoop. | ||
| """ | ||
| import importlib | ||
| import main # type: ignore[import] - conftest.py adds src to sys.path | ||
| importlib.reload(main) | ||
|
|
||
| loop_factory = main._get_asyncio_loop_factory() | ||
| assert loop_factory is asyncio.SelectorEventLoop, ( | ||
| "SelectorEventLoop should be used on Windows (prevents WinError 64)" | ||
| ) | ||
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.
🧩 Analysis chain
🌐 Web query:
anyio.run backend_options loop_factory asyncio💡 Result:
anyio.run()accepts backend-specific kwargs viabackend_options. For the asyncio backend, you can pass an event loop factory withloop_factory(a callable that returns a new event loop). (deepwiki.com)Example: use
uvloop(AnyIO 4+)AnyIO’s migration guide explicitly says to replace the old “event loop policy” approach with
loop_factory. (anyio.readthedocs.io)Notes
debug,loop_factory, anduse_uvloop(which internally selectsuvloop.new_event_loopwhen available). (deepwiki.com)Citations:
🏁 Script executed:
Repository: CoplayDev/unity-mcp
Length of output: 1177
Remove trailing whitespace on lines 68–69.
The
anyio.runapproach withbackend_options={"loop_factory": loop_factory}is correct and follows AnyIO's recommended migration away from deprecated event loop policies.The
show_banner=Trueparameter is only passed in theanyio.runpath (line 63), not in themcp.run()path (line 56). This asymmetry is not problematic sincemcp.run()has its own defaults, but note the difference.Lines 68–69 contain trailing whitespace that should be removed.
🤖 Prompt for AI Agents