Skip to content

Commit 7c1f241

Browse files
authored
ref(server): Separate bandwidth and throughput rate limiters (#257)
1 parent 71ef635 commit 7c1f241

1 file changed

Lines changed: 44 additions & 33 deletions

File tree

objectstore-server/src/rate_limits.rs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,29 @@ pub struct BandwidthLimits {
108108

109109
#[derive(Debug)]
110110
pub struct RateLimiter {
111-
config: RateLimits,
112111
bandwidth: BandwidthRateLimiter,
113-
global: Option<Mutex<TokenBucket>>,
114-
// NB: These maps grow unbounded but we accept this as we expect an overall limited
115-
// number of usecases and scopes. We emit gauge metrics to monitor their size.
116-
usecases: papaya::HashMap<String, Mutex<TokenBucket>>,
117-
scopes: papaya::HashMap<Scopes, Mutex<TokenBucket>>,
118-
rules: papaya::HashMap<usize, Mutex<TokenBucket>>,
112+
throughput: ThroughputRateLimiter,
113+
}
114+
115+
impl RateLimiter {
116+
pub fn new(config: RateLimits) -> Self {
117+
Self {
118+
bandwidth: BandwidthRateLimiter::new(config.bandwidth),
119+
throughput: ThroughputRateLimiter::new(config.throughput),
120+
}
121+
}
122+
123+
/// Checks if the given context is within the rate limits.
124+
///
125+
/// Returns `true` if the context is within the rate limits, `false` otherwise.
126+
pub fn check(&self, context: &ObjectContext) -> bool {
127+
self.throughput.check(context) && self.bandwidth.check()
128+
}
129+
130+
/// Returns a reference to the shared bytes accumulator, used for bandwidth-based rate-limiting.
131+
pub fn bytes_accumulator(&self) -> Arc<AtomicU64> {
132+
Arc::clone(&self.bandwidth.accumulator)
133+
}
119134
}
120135

121136
#[derive(Debug)]
@@ -176,36 +191,33 @@ impl BandwidthRateLimiter {
176191
}
177192
}
178193

179-
impl RateLimiter {
180-
pub fn new(config: RateLimits) -> Self {
194+
#[derive(Debug)]
195+
struct ThroughputRateLimiter {
196+
config: ThroughputLimits,
197+
global: Option<Mutex<TokenBucket>>,
198+
// NB: These maps grow unbounded but we accept this as we expect an overall limited
199+
// number of usecases and scopes. We emit gauge metrics to monitor their size.
200+
usecases: papaya::HashMap<String, Mutex<TokenBucket>>,
201+
scopes: papaya::HashMap<Scopes, Mutex<TokenBucket>>,
202+
rules: papaya::HashMap<usize, Mutex<TokenBucket>>,
203+
}
204+
205+
impl ThroughputRateLimiter {
206+
fn new(config: ThroughputLimits) -> Self {
181207
let global = config
182-
.throughput
183208
.global_rps
184-
.map(|rps| Mutex::new(TokenBucket::new(rps, config.throughput.burst)));
209+
.map(|rps| Mutex::new(TokenBucket::new(rps, config.burst)));
185210

186211
Self {
187-
config: config.clone(),
188-
bandwidth: BandwidthRateLimiter::new(config.bandwidth),
212+
config,
189213
global,
190214
usecases: papaya::HashMap::new(),
191215
scopes: papaya::HashMap::new(),
192216
rules: papaya::HashMap::new(),
193217
}
194218
}
195219

196-
/// Returns a reference to the shared bytes accumulator, used for bandwidth-based rate-limiting.
197-
pub fn bytes_accumulator(&self) -> Arc<AtomicU64> {
198-
Arc::clone(&self.bandwidth.accumulator)
199-
}
200-
201-
/// Checks if the given context is within the rate limits.
202-
///
203-
/// Returns `true` if the context is within the rate limits, `false` otherwise.
204-
pub fn check(&self, context: &ObjectContext) -> bool {
205-
self.check_throughput(context) && self.bandwidth.check()
206-
}
207-
208-
fn check_throughput(&self, context: &ObjectContext) -> bool {
220+
fn check(&self, context: &ObjectContext) -> bool {
209221
// NB: We intentionally use unwrap and crash the server if the mutexes are poisoned.
210222

211223
// Global check
@@ -236,7 +248,7 @@ impl RateLimiter {
236248
}
237249

238250
// Rule checks - each matching rule has its own dedicated bucket
239-
for (idx, rule) in self.config.throughput.rules.iter().enumerate() {
251+
for (idx, rule) in self.config.rules.iter().enumerate() {
240252
if !rule.matches(context) {
241253
continue;
242254
}
@@ -254,28 +266,27 @@ impl RateLimiter {
254266
}
255267

256268
fn create_bucket(&self, rps: u32) -> Mutex<TokenBucket> {
257-
Mutex::new(TokenBucket::new(rps, self.config.throughput.burst))
269+
Mutex::new(TokenBucket::new(rps, self.config.burst))
258270
}
259271

260272
/// Returns the effective RPS for per-usecase limiting, if configured.
261273
fn usecase_rps(&self) -> Option<u32> {
262-
let global_rps = self.config.throughput.global_rps?;
263-
let pct = self.config.throughput.usecase_pct?;
274+
let global_rps = self.config.global_rps?;
275+
let pct = self.config.usecase_pct?;
264276
Some(((global_rps as f64) * (pct as f64 / 100.0)) as u32)
265277
}
266278

267279
/// Returns the effective RPS for per-scope limiting, if configured.
268280
fn scope_rps(&self) -> Option<u32> {
269-
let global_rps = self.config.throughput.global_rps?;
270-
let pct = self.config.throughput.scope_pct?;
281+
let global_rps = self.config.global_rps?;
282+
let pct = self.config.scope_pct?;
271283
Some(((global_rps as f64) * (pct as f64 / 100.0)) as u32)
272284
}
273285

274286
/// Returns the effective RPS for a rule, if it has a valid limit.
275287
fn rule_rps(&self, rule: &ThroughputRule) -> Option<u32> {
276288
let pct_limit = rule.pct.and_then(|p| {
277289
self.config
278-
.throughput
279290
.global_rps
280291
.map(|g| ((g as f64) * (p as f64 / 100.0)) as u32)
281292
});

0 commit comments

Comments
 (0)