Skip to content

Defer anyio.Lock creation via cached_property to bind to running loop#5265

Merged
Kludex merged 6 commits intomainfrom
defer-anyio-lock-creation
May 1, 2026
Merged

Defer anyio.Lock creation via cached_property to bind to running loop#5265
Kludex merged 6 commits intomainfrom
defer-anyio-lock-creation

Conversation

@Kludex
Copy link
Copy Markdown
Member

@Kludex Kludex commented May 1, 2026

Replaces eager asyncio.Lock instantiation at class/instance-init time with functools.cached_property-wrapped anyio.Lock across Agent, MCPServer, FallbackModel, Provider, _VertexAIAuth, and FastMCPToolset.

anyio.Lock binds to the event loop on which it's first used, so deferring creation until first access ensures it binds to the correct running loop and avoids issues with Temporal's workflow sandbox (which blocks dynamic imports and initialization happening inside workflow code).

Picks up the lock-related portion of #3012, leaving the broader asyncio→anyio migration for a follow-up that won't conflict with the agent graph refactor.

Checklist

  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • No breaking changes in accordance with the version policy.
  • PR title is fit for the release changelog.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

…loop

Replace eager `asyncio.Lock` instantiation at class/instance-init time with
`functools.cached_property`-wrapped `anyio.Lock` across `Agent`, `MCPServer`,
`FallbackModel`, `Provider`, `_VertexAIAuth`, and `FastMCPToolset`.

`anyio.Lock` binds to the event loop on which it's first used, so deferring
creation until first access ensures it binds to the correct running loop and
avoids issues with Temporal's workflow sandbox (which blocks dynamic imports
and certain initialization happening inside workflow code).
@github-actions github-actions Bot added size: M Medium PR (101-500 weighted lines) bug Report that something isn't working, or PR implementing a fix labels May 1, 2026
Comment thread tests/test_mcp.py

server._enter_lock = InterleavingLock() # pyright: ignore[reportPrivateUsage]
# `_enter_lock` is a `cached_property`; seeding the instance dict primes the cache.
vars(server)['_enter_lock'] = InterleavingLock()
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suite has a lot of private object testing. This is not good.

@Kludex Kludex enabled auto-merge (squash) May 1, 2026 08:52
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95cdfebfbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 95 to 96
self._entered_count -= 1
if self._entered_count == 0 and self._own_http_client is not None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve no-op __aexit__ when provider was never entered

Provider.__aexit__() now always decrements _entered_count, even when __aenter__() was never called. This regresses the previously supported no-op behavior (see tests/test_agent.py::test_provider_aexit_without_aenter) and can corrupt lifecycle state: after an early __aexit__, _entered_count becomes -1, so the next real async with provider: path skips the == 0 enter logic and later exits will no longer close the owned HTTP client, causing a resource leak.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

@Kludex Kludex disabled auto-merge May 1, 2026 08:54
Kludex added 4 commits May 1, 2026 10:56
The previous `_enter_lock is None` guard short-circuited `__aexit__` when
`__aenter__` had never been called. Switching to a `cached_property` removed
that guard and allowed `_entered_count` to drift to -1, which then caused a
later `async with` to skip the close branch and leak the owned HTTP client.

Add an explicit early return when `_entered_count == 0`, plus a regression
test that re-enters the provider after a bare `__aexit__`.
- Subclassing `anyio.Lock` is silently bypassed because `anyio.Lock`
  returns a backend adapter via `__new__`, so the override's body was
  never actually executed. Replace the subclass with a duck-typed wrapper
  that delegates to an inner `anyio.Lock` so the yield point is real.
- `_VertexAIAuth._refresh_lock` is only consumed by `_refresh_token`,
  which is itself `# pragma: no cover`. Mark the cached_property body
  the same way.
@Kludex Kludex enabled auto-merge (squash) May 1, 2026 09:40
@Kludex Kludex merged commit 51eff3c into main May 1, 2026
48 checks passed
@Kludex Kludex deleted the defer-anyio-lock-creation branch May 1, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Report that something isn't working, or PR implementing a fix size: M Medium PR (101-500 weighted lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant