Skip to content

SMOODEV-969: Python — share rate-limiter state across fetch() calls#79

Merged
brentrager merged 1 commit into
mainfrom
SMOODEV-969-py-shared-rate-limiter
May 12, 2026
Merged

SMOODEV-969: Python — share rate-limiter state across fetch() calls#79
brentrager merged 1 commit into
mainfrom
SMOODEV-969-py-shared-rate-limiter

Conversation

@brentrager
Copy link
Copy Markdown
Contributor

Summary

  • Hoist the sliding-window rate limiter onto FetchBuilder so every fetch() call through the same builder shares state, matching the Rust / Go ports.
  • New SlidingWindowRateLimiter.acquire_wait() mirrors Rust's acquire loop — sleeps until a slot is free instead of raising. Used automatically when the limiter is builder-owned.
  • Low-level fetch() grows a keyword-only rate_limiter parameter for injection; existing container_options.rate_limit + rate_limit_retry callers are unchanged (still get raise-on-full semantics).

Test plan

  • uv run pytest tests/ — 126/126 passing, including 5 new tests under TestSharedRateLimitState:
    • 5 sequential builder fetches with max=3/window=1s wait ≥1 window for the 4th & 5th
    • 5 concurrent fetches via asyncio.gather all succeed and serialize through the window
    • acquire_wait() blocks instead of raising
    • with_rate_limit re-invocation invalidates cached state
    • Two separate builders maintain isolated state
  • uv run ruff check + format --check clean
  • basedpyright — same 6 pre-existing errors on main, zero new

🤖 Generated with Claude Code

Previously `_client.fetch()` reconstructed the SlidingWindowRateLimiter
on every invocation, so calls routed through the same FetchBuilder did
not actually share their sliding window. This left the Python port at
odds with the Rust / Go ports, where the limiter lives on the client.

Hoist the limiter onto FetchBuilder:

- New `_shared_rate_limiter()` lazily builds one SlidingWindowRateLimiter
  per builder and caches it. Calling `with_rate_limit` again with new
  options invalidates the cache so the next fetch() rebuilds.
- New `acquire_wait()` method on SlidingWindowRateLimiter mirrors the
  Rust port's `acquire` loop: it sleeps for the limiter's reported
  `Try again in N ms` hint and retries until a slot is free. The
  raise-on-full `acquire()` API is preserved so the low-level
  `fetch()` entrypoint with `container_options.rate_limit` +
  `rate_limit_retry` keeps its existing semantics.
- `fetch()` grows a keyword-only `rate_limiter` parameter so callers
  (i.e. FetchBuilder) can inject the shared instance. When supplied,
  the gated path uses `acquire_wait()` instead of `acquire()`.

Tests cover the headline scenarios: 5 sequential builder fetches with
max=3 window=1s wait >=1 window for the 4th+5th; 5 concurrent fetches
via asyncio.gather all succeed and serialize through the window;
swapping options via with_rate_limit resets cached state; two separate
builders maintain isolated state.
@brentrager brentrager merged commit 23e86e9 into main May 12, 2026
1 check passed
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

🦋 Changeset detected

Latest commit: 6aa63ba

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/fetch Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brentrager brentrager deleted the SMOODEV-969-py-shared-rate-limiter branch May 12, 2026 19:00
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