pub struct AbsoluteLocalRateLimiter { /* private fields */ }Expand description
Sliding-window allow/reject rate limiter for in-process use.
Provides deterministic rate limiting with per-key state maintained in memory. Uses a sliding time window to track request counts and enforce limits.
§Algorithm
- Window capacity:
window_size × rate_limit - Admission check: Sum all bucket counts within the window
- Decision: Allow if
total < capacity, reject otherwise - Increment: If allowed, add count to current (or coalesced) bucket
§Thread Safety
- Uses
DashMapfor concurrent key access - Uses atomics for per-bucket counters
- Safe for multi-threaded use without external synchronization
§Semantics & Limitations
Sticky rate limits:
- The first call for a key stores its computed window limit
- Subsequent calls for the same key do not update that stored window limit
- Rationale: Avoids races where concurrent calls specify different limits
Best-effort concurrency:
- Admission check and increment are not atomic across calls
- Multiple threads can observe “allowed” simultaneously
- All may proceed, causing temporary overshoot
- This is expected behavior, not a bug
Eviction granularity:
- Uses
Instant::elapsed().as_millis()(whole-millisecond truncation) - Buckets expire close to
window_size(lazy eviction may delay removal until next call)
Lazy eviction:
- Reads and admission operations may remove expired buckets
- Provider builders start stale-key cleanup by default
§Examples
use trypema::{RateLimit, RateLimitDecision};
let limiter = rl.absolute();
let rate = RateLimit::per_second(10.0).unwrap();
assert!(matches!(limiter.inc("user_123", &rate, 1), RateLimitDecision::Allowed));
assert!(matches!(limiter.is_allowed("user_123"), RateLimitDecision::Allowed));Implementations§
Source§impl AbsoluteLocalRateLimiter
impl AbsoluteLocalRateLimiter
Sourcepub fn inc(
&self,
key: &str,
rate_limit: &RateLimit,
count: u64,
) -> RateLimitDecision
pub fn inc( &self, key: &str, rate_limit: &RateLimit, count: u64, ) -> RateLimitDecision
Check admission and, if allowed, record the increment for key.
This is the primary method for rate limiting. It performs an admission check and, if allowed, records the increment in the key’s state.
§Arguments
key: Unique identifier for the rate-limited resource (e.g.,"user_123","api_endpoint")rate_limit: Per-second rate limit. Sticky: stored on first call, ignored on subsequent callscount: Amount to increment (typically1for single requests, or batch size)
§Returns
RateLimitDecision::Allowed: Request admitted, increment recordedRateLimitDecision::Rejected: Over limit, increment not recorded
§Behavior
- Check current window usage via
is_allowed(key) - If over limit, return
Rejected(no state change) - If allowed:
- Check if recent bucket exists within
bucket_size - If yes: add count to existing bucket (coalescing)
- If no: create new bucket with count
- Return
Allowed
- Check if recent bucket exists within
§Concurrency
Not atomic across calls. Under concurrent load:
- Multiple threads may observe
Allowedsimultaneously - All may proceed and increment, causing temporary overshoot
- This is expected and by design for performance
For strict enforcement, use external synchronization (e.g., per-key locks).
§Bucket Coalescing
Increments within bucket_size of the most recent bucket are merged
into that bucket. This reduces memory usage and improves performance.
§Examples
use trypema::{RateLimit, RateLimitDecision};
let limiter = rl.absolute();
let rate = RateLimit::per_second(10.0).unwrap();
// Single request
assert!(matches!(limiter.inc("user_123", &rate, 1), RateLimitDecision::Allowed));
// Batch of 10
assert!(matches!(limiter.inc("user_456", &rate, 10), RateLimitDecision::Allowed));Sourcepub fn is_allowed(&self, key: &str) -> RateLimitDecision
pub fn is_allowed(&self, key: &str) -> RateLimitDecision
Check if key is currently under its rate limit (read-only).
Performs an admission check without recording an increment. Useful for previewing whether a request would be allowed before doing expensive work.
§Arguments
key: Unique identifier for the rate-limited resource
§Returns
RateLimitDecision::Allowed: Key is under limitRateLimitDecision::Rejected: Key is over limit, includes backoff hints
§Behavior
- If key doesn’t exist, return
Allowed(no state yet) - Perform lazy eviction of expired buckets
- Sum remaining bucket counts
- Compare against
window_capacity = window_size × rate_limit - Return decision with metadata if rejected
§Side Effects
- Lazy eviction: Removes expired buckets from key’s state
- No increment: Does not modify counters (read-only check)
§Use Cases
- Preview: Check before expensive operations
- Metrics: Sample rate limit status without affecting state
- Testing: Verify rate limit behavior
§Examples
use trypema::{RateLimit, RateLimitDecision};
let limiter = rl.absolute();
let rate = RateLimit::per_second(10.0).unwrap();
// Unknown key → always allowed
assert!(matches!(limiter.is_allowed("new_key"), RateLimitDecision::Allowed));
// Check before recording
if matches!(limiter.is_allowed("user_123"), RateLimitDecision::Allowed) {
limiter.inc("user_123", &rate, 1);
}Sourcepub fn get(&self, key: &str) -> u64
pub fn get(&self, key: &str) -> u64
Current live window total for key.
Evicts expired buckets when necessary and returns the sum of live bucket
counts. Unknown keys return 0 without inserting state.
§Examples
use trypema::RateLimit;
let limiter = rl.absolute();
assert_eq!(limiter.get("user_123"), 0);
let rate = RateLimit::per_second(10.0).unwrap();
limiter.inc("user_123", &rate, 3);
assert_eq!(limiter.get("user_123"), 3);Sourcepub fn set_rate_limit(
&self,
key: &str,
rate_limit: &RateLimit,
) -> Option<RateLimit>
pub fn set_rate_limit( &self, key: &str, rate_limit: &RateLimit, ) -> Option<RateLimit>
Change the stored rate limit for an existing key without changing its history.
Returns the previous effective rate limit, or None when the key does not exist. An
equivalent effective limit is a no-op. Later calls to Self::inc keep using the new
stored limit.
Sourcepub fn delete(&self, key: &str) -> Option<u64>
pub fn delete(&self, key: &str) -> Option<u64>
Delete all rate-limit state for key.
Returns the key’s live total before deletion, or None when it did not exist. A later
increment starts with fresh history and the rate supplied to that increment.
Sourcepub fn set_if(
&self,
key: &str,
rate_limit: &RateLimit,
comparator: RateLimitComparator,
count: u64,
) -> ConditionalSetOutcome
pub fn set_if( &self, key: &str, rate_limit: &RateLimit, comparator: RateLimitComparator, count: u64, ) -> ConditionalSetOutcome
Conditionally replace the window total for key.
When comparator matches the key’s current window total, the
window contents are replaced by a single current-timestamp bucket holding
count and the stored window limit is recomputed from rate_limit;
otherwise nothing is written. Unlike inc, a matched call can therefore
replace a previously stored limit.
The entire operation runs while holding the key’s map entry, so it is
consistent with respect to other set_if calls; interleaving with concurrent
inc calls follows the limiter’s usual best-effort concurrency model.
§Arguments
key: Unique identifier for the rate-limited resourcerate_limit: Per-second rate limit used to redefine the stored window limitcomparator: Guard evaluated against the current window totalcount: The total to write when the guard matches
§Returns
A ConditionalSetOutcome describing whether the comparator matched and the totals
before and after the operation.
§Priming Idiom
set_if(key, rate, RateLimitComparator::Lt(count), count) raises the window
total to at least count and never lowers it — idempotent and safe to retry.
§Examples
use trypema::{RateLimit, RateLimitComparator};
let limiter = rl.absolute();
let rate = RateLimit::per_second(10.0).unwrap();
// Prime the window to 40.
let outcome = limiter.set_if("user_123", &rate, RateLimitComparator::Lt(40), 40);
assert!(outcome.matched);
assert_eq!((outcome.current_total, outcome.previous_total), (40, 0));
// Re-priming is a no-op: the guard no longer matches.
let outcome = limiter.set_if("user_123", &rate, RateLimitComparator::Lt(40), 40);
assert!(!outcome.matched);
assert_eq!((outcome.current_total, outcome.previous_total), (40, 40));Sourcepub fn set_if_preserve_history(
&self,
key: &str,
rate_limit: &RateLimit,
comparator: RateLimitComparator,
count: u64,
preservation: HistoryPreservation,
) -> ConditionalSetOutcome
pub fn set_if_preserve_history( &self, key: &str, rate_limit: &RateLimit, comparator: RateLimitComparator, count: u64, preservation: HistoryPreservation, ) -> ConditionalSetOutcome
Conditionally set the current total while retaining the selected side of the existing sliding-window history.