Skip to main content

AbsoluteLocalRateLimiter

Struct AbsoluteLocalRateLimiter 

Source
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

  1. Window capacity: window_size × 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 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

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 bucket_size
    • 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 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));
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 × 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.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);
}
Source

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);
Source

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.

Source

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.

Source

pub fn clear(&self)

Delete all keys stored by this absolute limiter.

Source

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 resource
  • rate_limit: Per-second rate limit used to redefine the stored window limit
  • comparator: Guard evaluated against the current window total
  • count: 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));
Source

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.

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