Skip to content

fix(deps): require anyio>=4.15 so CapacityLimiter holds its bound - #224

Merged
ethan-scitix merged 2 commits into
mainfrom
deps/anyio-limiter-race
Oct 23, 2026
Merged

ethan-scitix merged 2 commits into
mainfrom
deps/anyio-limiter-race

Conversation

@ethan-scitix

Copy link
Copy Markdown
Collaborator

Type

  • fix — bug fix or alignment correction

Summary

  • Every stage and model concurrency limit could run one task over its bound. They are all anyio.CapacityLimiter. Before anyio 4.15, a release woke the next waiter without handing it the token. A task scheduled ahead of that waiter found the wait queue empty and a free slot, so it took the token; then the waiter took one too.
  • This is the intermittent assert 5 <= 4 in test_runner.py::TestE2EStageConcurrency::test_stage_concurrency_limit_allows_higher. sieval and the test are both correct.
  • Production impact: concurrency_limit, concurrency_limits and a model's concurrency_limit can occasionally admit one extra in-flight request. That is harmless for most endpoints, but can draw a 429 from one rate-limited to an exact concurrency.
  • Fix: anyio>=4.15.0. 4.15 adds the waiter to the borrowers before waking it (_notify_next_waiter). Bisected with a deterministic repro: 4.12.0, 4.12.1, 4.13.0 and 4.14.0 all run 2 tasks under a limit of 1; 4.15.0 and 4.15.1 run 1.
  • anyio 4.15 requires typing-extensions>=4.16.0, so the lock moves it 4.15.0 → 4.16.0. It is declared in pyproject.toml so the bump is reviewable rather than resolved into existence (check_deps).
  • New regression test TestCapacityLimiterHandoff in tests/unit/core/utils/test_concurrency.py pins the handoff deterministically: no timing, one scheduling step.

Related Issues

Found while trimming the unit suite in #222.

Test Plan

Automated

  • Lint/format clean (ruff check && ruff format --check, pre-commit incl. taplo + lock-drift)
  • Type check clean (ty check on the changed test)
  • Unit tests pass: pytest tests/unit tests/integration tests/acceptance -m "not stress and not benchmark" -n 16 with anyio 4.15.1 + typing-extensions 4.16.0 → 9644 passed, 2 skipped, no new warnings
  • scripts/check_preflight.py --check check_deps → PASS (2 justified moves)

Manual

  • Regression test fails on anyio 4.12.0 (assert 2 == 1) and passes on 4.15.1.

  • The runner scenario (concurrency_limits={"infer": 4}, 6 samples), 16 parallel processes × 150 runs:

    anyio runs peak 4 peak 5
    4.12.0 2400 2330 70 (2.9%)
    4.15.1 2400 2400 0

Checklist

Required (all PRs)

  • PR title follows conventional format (type(scope): description)
  • No internal paths, credentials, or personal info in committed files
  • AI-generated code has AI-Generated Code - <model> (<provider>) in module docstring
  • No new upper-layer dependencies added to core/
  • Deleted code verified — no remaining call sites depend on it

If: New Dependency

  • Added to correct PDM dependency group: typing-extensions is default, as anyio's own requirement, not a new import
  • Justified in Summary

🤖 Generated with Claude Code

ethan-scitix and others added 2 commits October 23, 2026 20:06
anyio before 4.15 woke the next waiter on release without handing it the
token. A task scheduled ahead of that waiter found the wait queue empty,
took the free token, and the waiter took one too: the limiter held one task
over its bound. Every stage and model concurrency limit is a
CapacityLimiter, so `concurrency_limits={"infer": 4}` occasionally ran 5
inferences at once -- the intermittent `5 <= 4` in
TestE2EStageConcurrency::test_stage_concurrency_limit_allows_higher.

4.15 adds the waiter to the borrowers before waking it. The floor pulls
typing-extensions to 4.16 (anyio 4.15's own requirement), declared so the
lock bump is reviewable. A deterministic regression test pins the handoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ethan-scitix
ethan-scitix merged commit ca53efc into main Oct 23, 2026
9 checks passed
@ethan-scitix
ethan-scitix deleted the deps/anyio-limiter-race branch October 23, 2026 12:17
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