feat: sub-Hz rate limits and 2.0 API polish#10
Merged
brayniac merged 4 commits intoiopsystems:mainfrom Apr 21, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
feat). NewBuilder::period(Duration),Ratelimiter::period(),set_period(Duration). Tokens accumulate atrateperperiod, 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]onErrorandTryWaitError(chore). Future variant additions won't require another major bump.try_waiterror type (feat!).try_waitnow returnsResult<(), TryWaitError>, matchingtry_wait_n. Fixes a latent spin-forever bug whenmax_tokens == 0(now returnsExceedsCapacity).try_waitis a thin delegator totry_wait_n(1).set_rate_per(rate, period)(feat). Atomic setter that avoids the mismatched-intermediate-state hazard of callingset_rateandset_periodseparately.Changes
Breaking changes (require 2.0)
try_waitreturn type:Result<(), Duration>→Result<(), TryWaitError>. Callers usingwhile let Err(d) = rl.try_wait() { sleep(d) }must matchTryWaitError::Insufficient(d)explicitly — see updated module-level example.ErrorandTryWaitErrorare now#[non_exhaustive]. Exhaustive matches require a wildcard arm.Error::PeriodTooShortvariant (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_zerostrengthened to assertExceedsCapacity.cargo clippy --all-targets -- -D warningscargo check --no-default-features/cargo clippy --no-default-features --lib -- -D warningscargo fmt --all -- --checkRUSTDOCFLAGS=\"-D warnings\" cargo doc --no-deps— module-level doctest updated to show the new match pattern.Fixes #7. Fixes #4.
🤖 Generated with Claude Code