Title: Share one event loop per test module to stop Windows socketpair churn by maxisbey · Pull Request #3070 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Share one event loop per test module to stop Windows socketpair churn by maxisbey · Pull Request #3070 · modelcontextprotocol/python-sdk
X Title: Share one event loop per test module to stop Windows socketpair churn by maxisbey · Pull Request #3070 · modelcontextprotocol/python-sdk
Description: Share one event loop per test module instead of creating a fresh one for every async test, to stop a Windows CI flake caused by event-loop churn. Motivation and Context test (3.12, locked, windows-latest) flaked with two failures that are really one event: test_client_tolerates_405_on_get_and_delete failed before its body ran with OSError: [WinError 10055] ("insufficient buffer space or queue full"), raised from asyncio.new_event_loop() while anyio's pytest plugin was creating the test's runner. On Windows every new event loop builds its self-pipe via socket.socketpair(), which CPython emulates as a real loopback TCP listen/connect/accept. The next test on the same worker was then blamed for the half-built loop's ResourceWarning: unclosed event loop (GC → unraisable → promoted to error by filterwarnings = ["error"]). The churn behind it: anyio's runner is lease-counted, so with only function-scoped async fixtures every async test creates and destroys its own event loop (anyio docs, agronholm/anyio#686). This suite has ~2,740 async tests and runs them in ~30s across 4 xdist workers on Windows CI — hundreds of loopback socketpair create/close cycles per second, each parking a connection in TIME_WAIT for ~120s. WinError 10055 is a transient kernel buffer failure under that churn; each loop creation is a lottery ticket and a Windows job currently buys ~2,740 of them (measured: exactly one socketpair() call per async test). The fix holds a module-scoped anyio runner lease (autouse fixture in tests/conftest.py), so each xdist worker reuses one loop per module. Measured on the flaked SHA with a socket.socketpair counter: 2,737 → ~380 socketpair calls per run on windows-latest, peak TIME_WAIT 4,164 → 400, with identical test outcomes on both windows-latest and ubuntu-latest. Six modules parametrize anyio_backend (trio + MockClock, SelectorEventLoop) and shadow the lease fixture with a sync no-op: a module-scoped lease can't depend on the function-scoped parameter (pytest fails with ScopeMismatch — loud, so a future module that forgets the shadow can't misbehave silently), the held runner wouldn't match the requested backend anyway, and tests/client/test_stdio.py's direct trio.run(...) calls collide with a lingering asyncio loop's wakeup fd on Windows. When these modules run, the previous module's runner has already been torn down, so their behavior is unchanged. Session scope would cut churn further (~10 socketpairs/run) but was rejected: it breaks all anyio_backend-parametrized tests with ScopeMismatch and the direct-trio tests on Windows via the wakeup-fd collision (both verified empirically). How Has This Been Tested? Full suite on windows-latest and ubuntu-latest at the flaked SHA with this patch: identical pass/skip counts to baseline, zero new failures, socketpair churn measured via an injected socket.socketpair counter and a TIME_WAIT sampler. Alternative scopes (session, package) were also run on both platforms to map exactly which tests conflict; the module + shadow shape is the one with zero behavior change. ./scripts/test locally: 100.00% coverage, strict-no-cover clean. Breaking Changes None — test infrastructure only. New convention for contributors: a test module that parametrizes anyio_backend must shadow _module_runner_lease (documented in the fixture's docstring in tests/conftest.py; forgetting it fails loudly at setup). Types of changes Bug fix (non-breaking change which fixes an issue) Checklist I have read the MCP Documentation My code follows the repository's style guidelines New and existing tests pass locally I have added appropriate error handling I have added or updated documentation as needed Additional context Tests in the same module now share loop-scoped state. An audit found no bare asyncio.create_task, set_event_loop, exception-handler, or executor usage anywhere in tests/ or src/mcp/, so nothing depends on per-test loop isolation today; a future test leaking a background task would surface as order-dependence within its own module. The residual ~380 loop creations (the six opt-out modules plus one loop per module per worker) keep a small tail of exposure. If the flake ever reappears, the next increment is moving the ~12 parametrized/direct-trio tests into small sibling modules so the two big opted-out files (test_jsonrpc_dispatcher.py, test_streamable_http_modern.py) rejoin the shared loop. AI Disclaimer
Open Graph Description: Share one event loop per test module instead of creating a fresh one for every async test, to stop a Windows CI flake caused by event-loop churn. Motivation and Context test (3.12, locked, windows-...
X Description: Share one event loop per test module instead of creating a fresh one for every async test, to stop a Windows CI flake caused by event-loop churn. Motivation and Context test (3.12, locked, windows-...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/3070
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:953d6299-faa4-7393-2ea5-4e1b80cc933e |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | DAE2:BF910:4A0F19:6741D5:6A586BA2 |
| html-safe-nonce | 69ba7782859a710abd09c2a447315b83087a13bb107737ab2c21fa933d7fb79c |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEQUUyOkJGOTEwOjRBMEYxOTo2NzQxRDU6NkE1ODZCQTIiLCJ2aXNpdG9yX2lkIjoiNTg1MTE3NDM5NDU1NTU1ODgxOCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | bc0886f747997cf671a3724c12152e855fc286497c4ca10d08fb8351bf6d0bc4 |
| hovercard-subject-tag | pull_request:4001066330 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/3070/files |
| twitter:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| og:image:alt | Share one event loop per test module instead of creating a fresh one for every async test, to stop a Windows CI flake caused by event-loop churn. Motivation and Context test (3.12, locked, windows-... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5f2a0c7865178af3d91dd9f13b0cdfc3c73a2529c873d2780bb4c01271a57ec6 |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 8aae7b8d6caacacf5c66eaeb2702d8dc88d85b4a |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width