Skip to main content

AbsoluteLocalRateLimiter

Struct AbsoluteLocalRateLimiter 

Source
pub struct AbsoluteLocalRateLimiter { /* private fields */ }
Expand description

Strict sliding-window 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

  1. Window capacity: window_size_seconds × rate_limit
  2. Admission check: Sum all bucket counts within the window
  3. Decision: Allow if total < capacity, reject otherwise
  4. Increment: If allowed, add count to current (or coalesced) bucket

§Thread Safety

  • Uses DashMap for 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 the rate limit
  • Subsequent calls for the same key do not update it
  • 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_seconds (lazy eviction may delay removal until next call)

Memory growth:

  • Keys are not automatically removed
  • Unbounded key cardinality will grow memory
  • Use run_cleanup_loop() to periodically remove stale keys

Lazy eviction:

  • Expired buckets are only removed when is_allowed() or inc() is called
  • Stale buckets remain in memory until accessed or cleanup runs

§Performance

  • Admission check: O(buckets_in_window) — typically < 10 buckets
  • Increment: O(1) amortised (coalesced into existing bucket or appended via push_back)
  • Memory: ~50–200 bytes per key (depends on bucket count)

§Examples

use trypema::{RateLimit, RateLimitDecision};

let limiter = rl.local().absolute();
let rate = RateLimit::try_from(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

Source

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 calls
  • count: Amount to increment (typically 1 for single requests, or batch size)
§Returns
§Behavior
  1. Check current window usage via is_allowed(key)
  2. If over limit, return Rejected (no state change)
  3. If allowed:
    • Check if recent bucket exists within rate_group_size_ms
    • If yes: add count to existing bucket (coalescing)
    • If no: create new bucket with count
    • Return Allowed
§Concurrency

Not atomic across calls. Under concurrent load:

  • Multiple threads may observe Allowed simultaneously
  • 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 rate_group_size_ms 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.local().absolute();
let rate = RateLimit::try_from(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));
Source

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
§Behavior
  1. If key doesn’t exist, return Allowed (no state yet)
  2. Perform lazy eviction of expired buckets
  3. Sum remaining bucket counts
  4. Compare against window_capacity = window_size_seconds × rate_limit
  5. 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.local().absolute();
let rate = RateLimit::try_from(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);
}

Trait Implementations§

Source§

impl Debug for AbsoluteLocalRateLimiter

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more