Skip to main content

pdk_rate_limit_lib/
lib.rs

1// Copyright (c) 2026, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! PDK Rate Limit Library
6//!
7//! Provides rate limiting functionality with support for both local and distributed
8//! storage backends.
9//!
10//! It allows to control the rate at which operations can be performed by
11//! implementing token bucket algorithms with configurable limits and time windows.
12//!
13//! **Local mode** handles all rate limiting quota in-memory, in a single node.
14//!
15//! **Clustered mode** enables distributed rate limiting across multiple nodes, coordinating quota and statistics.
16//!
17//! ## Highlights
18//!
19//! - Local and distributed rate limiting
20//! - Configurable quota limits and time windows
21//! - Support for grouped rate limits with selectors
22//! - Asynchronous API for high-performance applications
23//!
24//! ## Primary types
25//!
26//! - [`RateLimit`]: trait which checks if a request should be allowed based on rules
27//! - [`RateLimitResult`]: result indicating if request is allowed or rejected
28//! - [`RateLimitStatistics`]: metadata about remaining quota and limits
29//! - [`RateLimitBuilder`]: instances builder with configurable options (e.g. clustering, custom buckets, distributed storage)
30//! - [`RateLimitError`]: error type for rate limiting operations
31
32use data_storage_lib::ll::distributed::DistributedStorageError;
33use thiserror::Error;
34
35use data_storage_lib::ll::local::LocalStorageError;
36
37mod bucket;
38mod builder;
39mod distribution_formula;
40mod implementation;
41mod key_manager;
42
43pub use builder::{RateLimitBuilder, RateLimitBuilderInstance};
44pub use implementation::RateLimitInstance;
45
46use crate::bucket::Bucket;
47
48/// Metadata about the remaining quota for a particular group and key pair.
49///
50/// This struct provides information about the current state of a rate limit bucket,
51/// including how much quota remains, the total limit, and when the quota will reset.
52pub struct RateLimitStatistics {
53    /// The amount of quota still available.
54    ///
55    /// For cluster scenarios, to improve performance, this value is a best effort
56    /// approximation and may not be perfectly accurate across all nodes.
57    pub remaining: u64,
58
59    /// The maximum amount of quota that can be consumed in a given time frame.
60    ///
61    /// This represents the total capacity of the rate limit bucket.
62    pub limit: u64,
63
64    /// The amount of time in milliseconds until more quota is refreshed.
65    ///
66    /// This indicates when the rate limit window will reset and quota will be replenished.
67    pub reset: u64,
68}
69
70/// This enum represents the result of a rate limit operation, indicating whether a
71/// request should be allowed or rejected based on the current rate limit state,
72/// along with statistics about the rate limit bucket.
73pub enum RateLimitResult {
74    /// The request is allowed to proceed.
75    ///
76    /// Contains statistics about the current state of the rate limit bucket.
77    Allowed(RateLimitStatistics),
78
79    /// The request should be rejected due to rate limit exceeded.
80    ///
81    /// Contains statistics about the current state of the rate limit bucket,
82    /// including when the limit will reset.
83    TooManyRequests(RateLimitStatistics),
84}
85
86/// Errors that can occur during rate limiting operations.
87///
88/// This enum represents all possible error conditions that can arise when
89/// performing rate limit checks or managing rate limit state.
90#[non_exhaustive]
91#[derive(Debug, Error)]
92pub enum RateLimitError {
93    /// An unexpected error occurred during rate limiting operations.
94    ///
95    /// This wraps any underlying errors from storage operations or other
96    /// unexpected conditions.
97    #[error("Unexpected error {0}.")]
98    Unexpected(Box<dyn std::error::Error>),
99
100    /// The maximum allowed number of hops was reached in distributed rate limiting.
101    ///
102    /// This error occurs when the distributed rate limiting algorithm
103    /// has reached its maximum number of outgoing requests while trying to find available quota.
104    #[error("Max Hops.")]
105    MaxHops,
106
107    /// A serialization error occurred.
108    ///
109    /// This error occurs when there is an error serializing or deserializing data.
110    #[error("Serialization error: {0}.")]
111    Serialization(#[from] bincode::Error),
112}
113
114/// RateLimit trait defines the core interface for rate limiting operations.
115/// Implementations can use different storage backends (local, distributed)
116/// and different algorithms while providing a consistent API.
117#[allow(async_fn_in_trait)]
118pub trait RateLimit {
119    /// Check if a request should be allowed based on rate limiting rules.
120    ///
121    /// # Arguments
122    ///
123    /// * `group_selector` - A string identifying the rate limit group. This allows
124    ///   different rate limits to be applied to different categories of requests.
125    /// * `bucket_selector` - A string identifying the specific bucket within the group.
126    ///   This is typically used to identify individual users, IPs, or other entities.
127    /// * `increment` - The amount of quota to consume for this request. Typically 1,
128    ///   but can be higher for operations that should consume more quota.
129    ///
130    /// # Returns
131    ///
132    /// Returns a [`RateLimitResult`] indicating whether the request should be allowed
133    /// or rejected, along with statistics about the current rate limit state.
134    ///
135    /// # Errors
136    ///
137    /// Returns a [`RateLimitError`] if there was an error checking the rate limit,
138    /// such as storage errors or network issues in distributed scenarios.
139    ///
140    /// # Example
141    ///
142    /// ```no_run
143    /// # use rate_limit_lib::{RateLimit, RateLimitResult};
144    /// # async fn example(rate_limiter: impl RateLimit) -> Result<(), rate_limit_lib::RateLimitError> {
145    /// let result = rate_limiter.is_allowed("sla1", "550e8400-e29b-41d4-a716-446655440000", 1).await?;
146    /// match result {
147    ///     RateLimitResult::Allowed(stats) => {
148    ///         println!("Request allowed. Remaining: {}", stats.remaining);
149    ///         // Process the request
150    ///     }
151    ///     RateLimitResult::TooManyRequests(stats) => {
152    ///         println!("Rate limit exceeded. Reset in: {}ms", stats.reset);
153    ///         // Reject the request
154    ///     }
155    /// }
156    /// # Ok(())
157    /// # }
158    /// ```
159    async fn is_allowed(
160        &self,
161        group_selector: &str,
162        bucket_selector: &str,
163        increment: usize,
164    ) -> Result<RateLimitResult, RateLimitError>;
165}
166
167impl From<LocalStorageError> for RateLimitError {
168    fn from(value: LocalStorageError) -> Self {
169        RateLimitError::Unexpected(value.into())
170    }
171}
172
173impl From<DistributedStorageError> for RateLimitError {
174    fn from(value: DistributedStorageError) -> Self {
175        RateLimitError::Unexpected(value.into())
176    }
177}
178
179impl RateLimitStatistics {
180    fn from(bucket: &Bucket, now: u128) -> Self {
181        if let Some(status) = bucket.status() {
182            Self {
183                remaining: status.remaining(),
184                limit: status.limit(),
185                reset: status.reset(now) as u64,
186            }
187        } else {
188            Self {
189                remaining: 0,
190                limit: 0,
191                reset: 0,
192            }
193        }
194    }
195}