axum_limit/backend/
mod.rs1mod memory;
2
3#[cfg(feature = "redis")]
4mod redis;
5
6use crate::codec::CodecError;
7use crate::policy::{PolicyState, RateLimitPolicy};
8use crate::quota::Quota;
9use crate::snapshot::RateLimitSnapshot;
10use async_trait::async_trait;
11use std::error::Error;
12use std::fmt::{Display, Formatter, Result as FmtResult};
13
14pub use memory::MemoryBackend;
15
16#[cfg(feature = "redis")]
17pub use redis::RedisBackend;
18
19#[derive(Debug)]
21pub enum BackendError {
22 Codec(CodecError),
24 Contention,
26 #[cfg(feature = "redis")]
28 Redis(::redis::RedisError),
29}
30
31impl Display for BackendError {
32 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
33 match self {
34 BackendError::Codec(error) => write!(f, "{error}"),
35 BackendError::Contention => write!(f, "rate limit storage contention"),
36 #[cfg(feature = "redis")]
37 BackendError::Redis(error) => write!(f, "redis error: {error}"),
38 }
39 }
40}
41
42impl Error for BackendError {
43 fn source(&self) -> Option<&(dyn Error + 'static)> {
44 match self {
45 BackendError::Codec(error) => Some(error),
46 BackendError::Contention => None,
47 #[cfg(feature = "redis")]
48 BackendError::Redis(error) => Some(error),
49 }
50 }
51}
52
53impl From<CodecError> for BackendError {
54 fn from(value: CodecError) -> Self {
55 Self::Codec(value)
56 }
57}
58
59pub trait StorageKey: crate::Key {
61 fn storage_key(&self) -> String;
63}
64
65#[async_trait]
69pub trait RateLimitBackend: Send + Sync + Clone + 'static {
70 type Error: Error + Send + Sync + 'static;
72
73 fn namespace(&self) -> &str;
75
76 async fn transact<P>(
78 &self,
79 storage_key: &str,
80 quota: Quota,
81 now_ms: u64,
82 ) -> Result<RateLimitSnapshot, Self::Error>
83 where
84 P: RateLimitPolicy;
85}
86
87pub fn build_storage_key<P>(namespace: &str, subject: &str, quota: Quota) -> String
89where
90 P: RateLimitPolicy,
91{
92 let fingerprint = quota.fingerprint();
93 let burst = fingerprint
94 .burst
95 .map(|value| value.to_string())
96 .unwrap_or_else(|| "-".to_string());
97
98 format!(
99 "{namespace}:{}:{}:{}:{}:{subject}",
100 P::STATE_ID,
101 fingerprint.max,
102 fingerprint.per_ms,
103 burst,
104 )
105}
106
107pub fn apply_policy<P>(
111 bytes: Option<&[u8]>,
112 quota: Quota,
113 now_ms: u64,
114) -> Result<(Vec<u8>, RateLimitSnapshot), CodecError>
115where
116 P: RateLimitPolicy,
117{
118 let mut state = match bytes {
119 Some(payload) => P::State::decode(payload, quota)?,
120 None => P::State::create(quota, now_ms),
121 };
122 let snapshot = state.try_acquire(now_ms);
123 Ok((state.encode()?, snapshot))
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::policy::{FixedWindowPolicy, TokenBucketPolicy};
130 use crate::quota::Quota;
131
132 #[test]
133 fn build_storage_key_includes_policy_quota_and_subject() {
134 let quota = Quota::with_burst(5, 1000, 10);
135 let key = build_storage_key::<TokenBucketPolicy>("ns", "user-1", quota);
136 assert_eq!(key, "ns:token_bucket:5:1000:10:user-1");
137 }
138
139 #[test]
140 fn different_quotas_produce_different_storage_keys() {
141 let subject = "shared";
142 let key_a = build_storage_key::<FixedWindowPolicy>("ns", subject, Quota::new(1, 1000));
143 let key_b = build_storage_key::<FixedWindowPolicy>("ns", subject, Quota::new(5, 1000));
144 assert_ne!(key_a, key_b);
145 }
146}