Skip to content

Replace nest-asyncio with a dedicated background event loop#506

Open
dotsdl wants to merge 1 commit into
mainfrom
replace-nest-asyncio-background-loop
Open

Replace nest-asyncio with a dedicated background event loop#506
dotsdl wants to merge 1 commit into
mainfrom
replace-nest-asyncio-background-loop

Conversation

@dotsdl

@dotsdl dotsdl commented Jun 4, 2026

Copy link
Copy Markdown
Member

Background

The synchronous client drives async methods through BaseClient._run_async. Those methods do real concurrent I/O — batched asyncio.gather fan-out in _batched_attribute_getter/_setter, and concurrent ProtocolDAGResult retrieval — so the async stack earns its keep and isn't going anywhere.

The problem was only ever how sync called into async: _run_async used nest_asyncio to permit re-entrant run_until_complete when a loop is already running (e.g. Jupyter). nest_asyncio achieves this by monkeypatching asyncio's private internals — fragile across CPython releases and widely considered bad practice. It was also an undeclared-in-test-env dependency, which recently turned CI red across main and every open PR (ModuleNotFoundError: No module named 'nest_asyncio').

Change

Run a single persistent event loop in a dedicated daemon thread and submit coroutines with asyncio.run_coroutine_threadsafe(coro, loop).result(). The calling thread blocks on the result while the loop thread does the work.

  • Concurrency preserved — all gather/as_completed fan-out runs inside the loop thread, unchanged.
  • Re-entrancy without monkeypatching — a caller already inside a running loop (Jupyter) simply waits on another thread; no patching of asyncio internals.
  • Stable cache bindingalru_cache binds to the loop it first runs on; a single process-wide loop keeps that invariant solid (this is what the old per-class _shared_event_loop dance was straining toward).
  • Loop is created lazily under a threading.Lock so concurrent callers can't start competing loops.
  • nest-asyncio dropped from alchemiscale-client.yml — no longer used anywhere.

Verification

Behavioral smoke test of the new machinery passes, including the key cases: loop reuse across calls, concurrent gather inside a submission, and invocation from within an already-running event loop (the scenario nest_asyncio existed for). Existing client integration tests exercise the real paths.

Relationship to #505

This supersedes #505 (which added nest-asyncio to the test env as an immediate CI unblock). Once this merges, nest-asyncio is no longer needed at all and #505 can be closed. Merging #505 first is harmless — this PR then removes the now-unused dependency.

🤖 Generated with Claude Code

`BaseClient._run_async` relied on `nest_asyncio` to allow re-entrant
`run_until_complete` so the synchronous client could drive its async,
concurrency-bearing methods (batched `asyncio.gather` fan-out, concurrent
ProtocolDAGResult retrieval) even from environments with an already-running
loop (e.g. Jupyter). `nest_asyncio` does this by monkeypatching asyncio's
private internals, which is fragile across CPython releases and is widely
considered bad practice.

Instead, run a single persistent event loop in a dedicated daemon thread
and submit coroutines to it with `asyncio.run_coroutine_threadsafe(...).result()`.
The calling thread blocks on the result while the loop thread does the work,
so:

- all existing `gather`/`as_completed` concurrency is preserved (it runs
  inside the loop thread);
- re-entrancy "just works" without monkeypatching --- a caller already inside
  a running loop simply waits on another thread;
- `alru_cache` stays bound to one stable, process-wide loop, which is the
  invariant the previous shared-loop dance was straining to maintain.

The loop is created lazily under a lock so concurrent callers cannot start
competing loops. `nest-asyncio` is dropped from the client environment, as it
is no longer used anywhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 79.56%. Comparing base (4ce245d) to head (0425e28).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
alchemiscale/base/client.py 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #506      +/-   ##
==========================================
+ Coverage   78.91%   79.56%   +0.65%     
==========================================
  Files          29       29              
  Lines        4808     4830      +22     
==========================================
+ Hits         3794     3843      +49     
+ Misses       1014      987      -27     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant