cratestack_axum/ratelimit/
subscription.rs1use std::fmt;
8use std::net::IpAddr;
9use std::sync::Arc;
10use std::time::Duration;
11
12use async_trait::async_trait;
13use cratestack_core::subscription_wire::{
14 SubscriptionCharge, SubscriptionRateLimit, SubscriptionRefusal,
15};
16use cratestack_core::{BoundedOutcome, CratestackError, RateLimitDecision};
17use cratestack_exec::{OpAdmission, OpExecutor, OpInput, RateLimitAdmission, RateLimitBucket};
18
19use super::config::RateLimitConfig;
20use super::key_fn::sha256_hex;
21use super::layer::RateLimitLayer;
22use super::policy::{DEFAULT_STORE_TIMEOUT, StoreErrorPolicy, StoreErrorWarnings};
23use super::scope::bucket_address;
24use super::store::RateLimitStore;
25use super::store_error::{StoreFailure, classify_store_failure};
26
27type OpResolver = Arc<dyn Fn(&str) -> OpAdmission + Send + Sync>;
28
29#[derive(Clone)]
40pub struct SubscriptionRateLimiter {
41 executor: OpExecutor,
42 op_resolver: OpResolver,
43 store_error_policy: StoreErrorPolicy,
44 store_timeout: Duration,
45 warnings: Arc<StoreErrorWarnings>,
46}
47
48impl SubscriptionRateLimiter {
49 pub fn new(store: Arc<dyn RateLimitStore>, config: RateLimitConfig) -> Self {
50 Self {
51 executor: OpExecutor::new(None, Duration::ZERO).with_rate_limit(store, config),
52 op_resolver: Arc::new(|_| OpAdmission::unresolved()),
54 store_error_policy: StoreErrorPolicy::default(),
55 store_timeout: DEFAULT_STORE_TIMEOUT,
56 warnings: Arc::new(StoreErrorWarnings::default()),
57 }
58 }
59
60 pub fn with_store_error_policy(mut self, policy: StoreErrorPolicy) -> Self {
61 self.store_error_policy = policy;
62 self
63 }
64
65 pub fn with_store_timeout(mut self, timeout: Duration) -> Self {
66 self.store_timeout = timeout;
67 self
68 }
69
70 pub fn with_op_resolver(
74 mut self,
75 resolver: impl Fn(&str) -> OpAdmission + Send + Sync + 'static,
76 ) -> Self {
77 self.op_resolver = Arc::new(resolver);
78 self
79 }
80}
81
82impl RateLimitLayer {
83 pub fn for_subscriptions(&self) -> SubscriptionRateLimiter {
89 SubscriptionRateLimiter {
90 executor: OpExecutor::new(None, Duration::ZERO)
91 .with_rate_limit(self.store.clone(), self.config),
92 op_resolver: Arc::new(|_| OpAdmission::unresolved()),
93 store_error_policy: self.store_error_policy,
94 store_timeout: self.store_timeout,
95 warnings: self.warnings.clone(),
96 }
97 }
98}
99
100impl fmt::Debug for SubscriptionRateLimiter {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("SubscriptionRateLimiter")
103 .field("store_timeout", &self.store_timeout)
104 .field("store_error_policy", &self.store_error_policy)
105 .finish_non_exhaustive()
106 }
107}
108
109#[async_trait]
110impl SubscriptionRateLimit for SubscriptionRateLimiter {
111 async fn admit(&self, charge: SubscriptionCharge<'_>) -> Result<(), SubscriptionRefusal> {
112 let op = (self.op_resolver)(charge.op_id);
113 if !self.executor.rate_limit_applies(&op) {
114 return Ok(());
115 }
116 let key =
117 bucket_key(charge.principal, charge.remote).map_err(SubscriptionRefusal::Error)?;
118 let input = OpInput::for_rate_limit(op, RateLimitBucket::new(&key, None));
119 let admitted =
120 tokio::time::timeout(self.store_timeout, self.executor.admit_rate_limit(&input))
121 .await
122 .unwrap_or_else(|_| Err(super::policy::store_timeout_error()));
123 match admitted {
124 Ok(RateLimitAdmission::Bypass) => Ok(()),
125 Ok(RateLimitAdmission::Consumed(BoundedOutcome { decision, .. })) => match decision {
126 RateLimitDecision::Allowed { .. } => Ok(()),
127 RateLimitDecision::Throttled { retry_after_secs } => {
128 Err(SubscriptionRefusal::Throttled { retry_after_secs })
129 }
130 },
131 Ok(_) => Err(SubscriptionRefusal::Error(CratestackError::Internal(
134 "rate limit: unhandled admission outcome; refusing rather than opening the \
135 subscription"
136 .to_owned(),
137 ))),
138 Err(error) => {
139 match classify_store_failure(error, self.store_error_policy, &self.warnings) {
140 StoreFailure::Serve => Ok(()),
141 StoreFailure::Refuse(error) => Err(SubscriptionRefusal::Error(error)),
142 }
143 }
144 }
145 }
146}
147
148fn bucket_key(principal: Option<&str>, remote: Option<IpAddr>) -> Result<String, CratestackError> {
151 if let Some(principal) = principal.filter(|p| !p.is_empty()) {
152 return Ok(format!("princ:{}", sha256_hex(principal.as_bytes())));
153 }
154 if let Some(ip) = remote {
155 return Ok(format!("ip:{}", bucket_address(ip)));
156 }
157 Err(CratestackError::PreconditionFailed(
158 "rate limit: an anonymous subscription session has no peer address to key its bucket \
159 on; serve the router through into_make_service_with_connect_info::<SocketAddr>()"
160 .to_owned(),
161 ))
162}