Skip to content

feat: sub-Hz rate limits and 2.0 API polish#10

Merged
brayniac merged 4 commits intoiopsystems:mainfrom
brayniac:sub-hz-rates
Apr 21, 2026
Merged

feat: sub-Hz rate limits and 2.0 API polish#10
brayniac merged 4 commits intoiopsystems:mainfrom
brayniac:sub-hz-rates

Conversation

@brayniac
Copy link
Copy Markdown
Contributor

Summary

Restores Duration-based rate control lost in the 1.0.0 refactor, plus a set of breaking API refinements in preparation for a 2.0 release.

  • Sub-Hz rates (feat). New Builder::period(Duration), Ratelimiter::period(), set_period(Duration). Tokens accumulate at rate per period, default 1 second. Ratelimiter::builder(1).period(Duration::from_secs(60)) is "1 per minute". Fixes Subsecond rate limit is no longer possible in v1 #7, Detailed features of duration control deleted in 1.0.0 #4.
  • #[non_exhaustive] on Error and TryWaitError (chore). Future variant additions won't require another major bump.
  • Unified try_wait error type (feat!). try_wait now returns Result<(), TryWaitError>, matching try_wait_n. Fixes a latent spin-forever bug when max_tokens == 0 (now returns ExceedsCapacity). try_wait is a thin delegator to try_wait_n(1).
  • set_rate_per(rate, period) (feat). Atomic setter that avoids the mismatched-intermediate-state hazard of calling set_rate and set_period separately.

Changes

  • c84e537 feat: configurable rate period for sub-Hz rate limits
  • 7e6f1f3 chore: mark Error and TryWaitError as non_exhaustive
  • 8aea5a0 feat!: unify try_wait error type with try_wait_n
  • 0bb90e3 feat: add set_rate_per for atomic rate + period updates

Breaking changes (require 2.0)

  • try_wait return type: Result<(), Duration>Result<(), TryWaitError>. Callers using while let Err(d) = rl.try_wait() { sleep(d) } must match TryWaitError::Insufficient(d) explicitly — see updated module-level example.
  • Error and TryWaitError are now #[non_exhaustive]. Exhaustive matches require a wildcard arm.
  • New Error::PeriodTooShort variant (non-breaking thanks to #[non_exhaustive]).

Test plan

  • cargo test / cargo test --release — 30 tests pass. New: sub_hz_refill, sub_hz_wait_hint, set_period_changes_rate, builder_error_period_zero, set_rate_per_updates_both. max_tokens_zero strengthened to assert ExceedsCapacity.
  • cargo clippy --all-targets -- -D warnings
  • cargo check --no-default-features / cargo clippy --no-default-features --lib -- -D warnings
  • cargo fmt --all -- --check
  • RUSTDOCFLAGS=\"-D warnings\" cargo doc --no-deps — module-level doctest updated to show the new match pattern.

Fixes #7. Fixes #4.

🤖 Generated with Claude Code

brayniac and others added 4 commits April 21, 2026 11:07
Previously the rate was fixed as tokens per second (u64), making it
impossible to express rates below 1/s (e.g. "1 token per minute").
Adds a `period` alongside the existing rate: tokens accumulate at
`rate` per `period`, with `period` defaulting to one second so
existing APIs are unchanged.

- Builder::period(Duration) configures the period at construction.
- Ratelimiter::period() / set_period() query and mutate it live.
- Error::PeriodTooShort rejects period=0 from Builder::build.
- Refill and wait-hint arithmetic switched to use period_ns.

Fixes iopsystems#7, iopsystems#4 (sub-second rate limits / lost duration control).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prepares both public error enums for future variant additions without
requiring another breaking change. Harmless cost for existing callers
(they can no longer write exhaustive matches, but must include a
wildcard arm instead).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
try_wait now returns Result<(), TryWaitError>, matching try_wait_n. Both
operations can fail the same way, so callers can share match arms.

In particular, try_wait now surfaces ExceedsCapacity when max_tokens is 0
and rate is nonzero — previously it returned an Insufficient-style hint
that would spin the caller forever. Callers using the old sleep-on-Err
idiom must match on TryWaitError::Insufficient explicitly; see the
module-level example.

try_wait is now a thin delegator to try_wait_n(1).

BREAKING CHANGE: try_wait's return type changed from Result<(), Duration>
to Result<(), TryWaitError>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without this, switching from one (rate, period) pair to another requires
two separate stores with a visible intermediate state — e.g. going from
"1000/sec" to "10/hour" briefly looks like "1000/hour" or "10/sec"
depending on order.

set_rate_per stores period first, then delegates to set_rate, so
concurrent try_wait* calls never observe a nonzero rate paired with the
old period.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brayniac brayniac changed the title feat!: sub-Hz rate limits and 2.0 API polish feat: sub-Hz rate limits and 2.0 API polish Apr 21, 2026
@brayniac brayniac merged commit d4b365d into iopsystems:main Apr 21, 2026
7 checks passed
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.

Subsecond rate limit is no longer possible in v1 Detailed features of duration control deleted in 1.0.0

1 participant