From c368a9f22956440821543e243f2e6f3e08dd9b64 Mon Sep 17 00:00:00 2001 From: xiaodu55 <1704788159@qq.com> Date: Sun, 27 Sep 2026 15:07:39 +0800 Subject: [PATCH] fix(backend): stop forcing WindowsSelectorEventLoopPolicy so asyncio subprocesses work on Windows (#5937) * fix(backend): stop forcing WindowsSelectorEventLoopPolicy on Windows backend/sitecustomize.py switched every Python process with backend/ on sys.path to WindowsSelectorEventLoopPolicy (added by #3236 for 'make dev' on Windows). On Windows the selector loop cannot spawn asyncio subprocesses - asyncio.create_subprocess_exec raises a bare NotImplementedError - so ACP agents, stdio MCP servers, and local sandbox command execution are all broken on native Windows, and the swallowed exception surfaces as 'Error invoking ACP agent x: ' with no reason. The forcing has no beneficiary left on Windows: the async postgres driver is asyncpg (not psycopg, as #3236 states), and asyncpg 0.31.0 ships no Windows wheels, so the postgres backend is unavailable on native Windows regardless of the loop; the default SQLite backend (aiosqlite) runs on either loop. The make dev startup fix in #3236 came from the serve.sh / wait-for-port.sh changes, which are untouched. With the Python-default ProactorEventLoop restored, subprocess features work again on native Windows. * fix(benchmark): scope a selector loop to the postgres checkpoint benchmark on Windows Static review of the sitecustomize removal flagged the one caller that relied on the old startup policy: bench_channels.py postgres workers run bare asyncio.run() and enter AsyncPostgresSaver.from_conn_string(), which the lockfile-pinned psycopg 3.3.3 rejects on Windows under ProactorEventLoop before connecting. Instead of restoring the global selector policy, the postgres branch of the benchmark now scopes WindowsSelectorEventLoopPolicy to itself right before asyncio.run(); memory/sqlite cases and every other backend process keep the subprocess-capable Proactor default. --- .../benchmark/checkpoint/bench_channels.py | 18 +++++++++++++ backend/sitecustomize.py | 26 ------------------- 2 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 backend/sitecustomize.py diff --git a/backend/scripts/benchmark/checkpoint/bench_channels.py b/backend/scripts/benchmark/checkpoint/bench_channels.py index 9487ff7dc8..0fadaf1fca 100644 --- a/backend/scripts/benchmark/checkpoint/bench_channels.py +++ b/backend/scripts/benchmark/checkpoint/bench_channels.py @@ -629,6 +629,23 @@ async def _run_postgres_case(case: BenchmarkCase, messages: list[BaseMessage], u await _delete_postgres_benchmark_thread(uri, thread_id) +def _configure_windows_postgres_loop_policy() -> None: + """Keep psycopg's async connector usable on Windows for postgres cases. + + psycopg rejects ProactorEventLoop before connecting on Windows, so the + postgres cases need the selector loop. Scoped to this entrypoint: + process-wide, the selector loop cannot spawn asyncio subprocesses, which + the rest of the backend requires. + """ + if sys.platform != "win32": + return + selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) + if selector_policy is None: + return + if not isinstance(asyncio.get_event_loop_policy(), selector_policy): + asyncio.set_event_loop_policy(selector_policy()) + + def _run_case(case: BenchmarkCase, *, work_dir: Path) -> dict[str, Any]: row = _base_row(case) messages = [_message_for_update(index, case.payload_bytes) for index in range(case.update_count)] @@ -641,6 +658,7 @@ def _run_case(case: BenchmarkCase, *, work_dir: Path) -> dict[str, Any]: uri = os.environ.get("TEST_POSTGRES_URI") if not uri: raise RuntimeError("postgres benchmark requires TEST_POSTGRES_URI") + _configure_windows_postgres_loop_policy() measured = asyncio.run(_run_postgres_case(case, messages, uri)) reducer_writes = [[message] for message in messages] diff --git a/backend/sitecustomize.py b/backend/sitecustomize.py deleted file mode 100644 index 4b85819e18..0000000000 --- a/backend/sitecustomize.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Process-wide Python startup customizations for backend entrypoints. - -When ``backend/`` is on ``sys.path``, Python imports this module during -interpreter startup. Keep changes here suitable for all gateway, script, -migration, and test entrypoints that run in that environment. -""" - -from __future__ import annotations - -import asyncio -import sys - - -def _configure_windows_event_loop_policy() -> None: - if sys.platform != "win32": - return - - selector_policy = getattr(asyncio, "WindowsSelectorEventLoopPolicy", None) - if selector_policy is None: - return - - if not isinstance(asyncio.get_event_loop_policy(), selector_policy): - asyncio.set_event_loop_policy(selector_policy()) - - -_configure_windows_event_loop_policy()