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}