1use std::sync::{Arc, Mutex, MutexGuard};
5use std::time::Duration;
6
7use tokio::sync::Notify;
8use tokio::time::Instant;
9
10use crate::hold::Hold;
11use crate::hooks::{LayerId, Refused};
12
13#[derive(Clone)]
51pub struct CapacityBucket {
52 inner: Arc<Inner>,
53}
54
55struct Inner {
56 layer: LayerId,
57 hold: Hold,
58 state: Mutex<State>,
59 sized: Notify,
62}
63
64struct State {
65 capacity: u32,
66 refill_per_sec: u32,
67 tokens: f64,
68 updated: Instant,
69 provisional: bool,
70}
71
72impl State {
73 fn refill(&mut self, now: Instant) {
75 let elapsed = now.saturating_duration_since(self.updated).as_secs_f64();
76 self.tokens =
77 (self.tokens + elapsed * f64::from(self.refill_per_sec)).min(f64::from(self.capacity));
78 self.updated = now;
79 }
80}
81
82enum Step {
84 Taken,
85 Refused(Refused),
86 WaitFor(Duration),
87 WaitForSize,
88}
89
90impl CapacityBucket {
91 pub fn new(layer: LayerId, capacity: u32, refill_per_sec: u32, hold: Hold) -> Self {
99 Self::build(layer, capacity, refill_per_sec, hold, false)
100 }
101
102 pub fn provisional(layer: LayerId, capacity: u32, refill_per_sec: u32, hold: Hold) -> Self {
110 Self::build(layer, capacity, refill_per_sec, hold, true)
111 }
112
113 fn build(
114 layer: LayerId,
115 capacity: u32,
116 refill_per_sec: u32,
117 hold: Hold,
118 provisional: bool,
119 ) -> Self {
120 assert!(refill_per_sec > 0, "a bucket that never refills");
121 Self {
122 inner: Arc::new(Inner {
123 layer,
124 hold,
125 state: Mutex::new(State {
126 capacity,
127 refill_per_sec,
128 tokens: f64::from(capacity),
129 updated: Instant::now(),
130 provisional,
131 }),
132 sized: Notify::new(),
133 }),
134 }
135 }
136
137 pub fn layer(&self) -> LayerId {
139 self.inner.layer
140 }
141
142 pub fn capacity(&self) -> u32 {
144 self.lock().capacity
145 }
146
147 pub fn hold(&self) -> &Hold {
149 &self.inner.hold
150 }
151
152 pub async fn acquire(&self, units: u32, exact: bool) -> Result<(), Refused> {
162 if let Some(refused) = self.refuses(units, exact) {
164 return Err(refused);
165 }
166 self.inner.hold.wait().await;
167 loop {
168 let sized = self.inner.sized.notified();
171 match self.step(units, exact) {
172 Step::Taken => break,
173 Step::Refused(refused) => return Err(refused),
174 Step::WaitFor(wait) => tokio::time::sleep(wait).await,
175 Step::WaitForSize => sized.await,
176 }
177 }
178 self.inner.hold.wait().await;
180 Ok(())
181 }
182
183 fn refuses(&self, units: u32, exact: bool) -> Option<Refused> {
185 let state = self.lock();
186 (exact && !state.provisional && units > state.capacity).then(|| Refused {
187 layer: self.inner.layer,
188 units,
189 capacity: state.capacity,
190 })
191 }
192
193 fn step(&self, units: u32, exact: bool) -> Step {
196 let mut state = self.lock();
197 state.refill(Instant::now());
198 let units = if units > state.capacity {
199 if state.provisional {
200 return Step::WaitForSize;
201 }
202 if exact {
203 return Step::Refused(Refused {
204 layer: self.inner.layer,
205 units,
206 capacity: state.capacity,
207 });
208 }
209 state.capacity
210 } else {
211 units
212 };
213 let missing = f64::from(units) - state.tokens;
214 if missing <= 1e-6 {
217 state.tokens = (state.tokens - f64::from(units)).max(0.0);
218 Step::Taken
219 } else {
220 Step::WaitFor(Duration::from_secs_f64(
221 missing / f64::from(state.refill_per_sec),
222 ))
223 }
224 }
225
226 pub fn resize(&self, capacity: u32, refill_per_sec: u32) {
233 assert!(refill_per_sec > 0, "a bucket that never refills");
234 {
235 let mut state = self.lock();
236 state.refill(Instant::now());
237 state.capacity = capacity;
238 state.refill_per_sec = refill_per_sec;
239 state.tokens = state.tokens.min(f64::from(capacity));
240 state.provisional = false;
241 }
242 self.inner.sized.notify_waiters();
243 }
244
245 pub fn confirm(&self) {
248 self.lock().provisional = false;
249 self.inner.sized.notify_waiters();
250 }
251
252 fn lock(&self) -> MutexGuard<'_, State> {
255 self.inner
256 .state
257 .lock()
258 .unwrap_or_else(|poisoned| poisoned.into_inner())
259 }
260}
261
262const _: fn(&CapacityBucket) = |bucket| {
264 fn send<T: Send>(_: T) {}
265 send(bucket.acquire(1, true));
266};
267
268impl std::fmt::Debug for CapacityBucket {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 let state = self.lock();
271 f.debug_struct("CapacityBucket")
272 .field("layer", &self.inner.layer)
273 .field("capacity", &state.capacity)
274 .field("refill_per_sec", &state.refill_per_sec)
275 .field("provisional", &state.provisional)
276 .finish_non_exhaustive()
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 const LAYER: LayerId = LayerId("test");
285
286 fn bucket(capacity: u32, refill_per_sec: u32) -> CapacityBucket {
287 CapacityBucket::new(LAYER, capacity, refill_per_sec, Hold::unbounded())
288 }
289
290 async fn timed(bucket: &CapacityBucket, units: u32) -> Duration {
292 let t = Instant::now();
293 bucket.acquire(units, true).await.unwrap();
294 t.elapsed()
295 }
296
297 #[tokio::test(start_paused = true)]
298 async fn it_starts_full() {
299 let bucket = bucket(10, 1);
300 assert_eq!(timed(&bucket, 10).await, Duration::ZERO);
301 }
302
303 #[tokio::test(start_paused = true)]
304 async fn it_never_holds_more_than_its_capacity() {
305 let bucket = bucket(5, 10);
306 tokio::time::sleep(Duration::from_secs(60)).await;
307 assert_eq!(timed(&bucket, 5).await, Duration::ZERO);
308 assert!(
309 timed(&bucket, 1).await >= Duration::from_millis(90),
310 "a minute idle filled the bucket past its capacity of 5"
311 );
312 }
313
314 #[tokio::test(start_paused = true)]
315 async fn it_paces_the_refill() {
316 let bucket = bucket(10, 10);
317 timed(&bucket, 10).await;
318 let waited = timed(&bucket, 5).await;
319 assert!(
320 (Duration::from_millis(500)..Duration::from_millis(510)).contains(&waited),
321 "5 tokens at 10 a second took {waited:?}"
322 );
323 }
324
325 #[tokio::test(start_paused = true)]
326 async fn an_exact_cost_above_capacity_is_refused_at_once() {
327 let bucket = bucket(10, 1);
328 let t = Instant::now();
329 assert_eq!(
330 bucket.acquire(11, true).await,
331 Err(Refused {
332 layer: LAYER,
333 units: 11,
334 capacity: 10
335 })
336 );
337 assert_eq!(t.elapsed(), Duration::ZERO);
338 assert_eq!(
339 timed(&bucket, 10).await,
340 Duration::ZERO,
341 "a refusal took tokens"
342 );
343 }
344
345 #[tokio::test(start_paused = true)]
346 async fn an_exact_cost_above_capacity_is_refused_without_waiting_out_a_hold() {
347 let bucket = bucket(10, 1);
348 bucket.hold().extend(Duration::from_secs(60));
349 let t = Instant::now();
350 assert_eq!(
351 bucket.acquire(11, true).await,
352 Err(Refused {
353 layer: LAYER,
354 units: 11,
355 capacity: 10
356 })
357 );
358 assert_eq!(
359 t.elapsed(),
360 Duration::ZERO,
361 "the refusal waited out the hold"
362 );
363 }
364
365 #[tokio::test(start_paused = true)]
366 async fn an_inexact_cost_above_capacity_is_charged_at_capacity() {
367 let bucket = bucket(10, 10);
368 let t = Instant::now();
369 bucket.acquire(50, false).await.unwrap();
370 assert_eq!(t.elapsed(), Duration::ZERO);
371 let waited = timed(&bucket, 1).await;
372 assert!(
373 (Duration::from_millis(100)..Duration::from_millis(110)).contains(&waited),
374 "the inexact cost did not drain exactly the capacity: {waited:?}"
375 );
376 }
377
378 #[tokio::test(start_paused = true)]
379 async fn a_provisional_bucket_waits_then_proceeds_after_a_resize() {
380 let bucket = CapacityBucket::provisional(LAYER, 10, 10, Hold::unbounded());
381 let waiting = tokio::spawn({
382 let bucket = bucket.clone();
383 async move { bucket.acquire(20, true).await }
384 });
385 tokio::time::sleep(Duration::from_secs(5)).await;
386 assert!(
387 !waiting.is_finished(),
388 "a provisional bucket refused or admitted 20 of 10"
389 );
390
391 bucket.resize(30, 10);
392 assert_eq!(waiting.await.unwrap(), Ok(()));
393 }
394
395 #[tokio::test(start_paused = true)]
396 async fn a_provisional_bucket_waits_then_refuses_after_confirm() {
397 let bucket = CapacityBucket::provisional(LAYER, 10, 10, Hold::unbounded());
398 let waiting = tokio::spawn({
399 let bucket = bucket.clone();
400 async move { bucket.acquire(20, true).await }
401 });
402 tokio::time::sleep(Duration::from_secs(5)).await;
403 assert!(!waiting.is_finished());
404
405 bucket.confirm();
406 assert_eq!(
407 waiting.await.unwrap(),
408 Err(Refused {
409 layer: LAYER,
410 units: 20,
411 capacity: 10
412 })
413 );
414 }
415
416 #[tokio::test(start_paused = true)]
417 async fn resize_keeps_the_tokens_clamped_and_the_hold() {
418 let grown = bucket(10, 1);
420 timed(&grown, 4).await;
421 grown.resize(100, 1);
422 assert_eq!(grown.capacity(), 100);
423 assert_eq!(timed(&grown, 6).await, Duration::ZERO);
424 assert!(timed(&grown, 1).await >= Duration::from_millis(990));
425
426 let shrunk = bucket(10, 1);
428 shrunk.resize(3, 1);
429 assert_eq!(timed(&shrunk, 3).await, Duration::ZERO);
430 assert!(timed(&shrunk, 1).await >= Duration::from_millis(990));
431
432 let held = bucket(10, 1);
434 held.hold().extend(Duration::from_secs(5));
435 held.resize(20, 2);
436 assert!(timed(&held, 1).await >= Duration::from_secs(5));
437 }
438
439 #[tokio::test(start_paused = true)]
440 async fn concurrent_acquirers_never_over_admit() {
441 let bucket = bucket(5, 10);
444 let start = Instant::now();
445 let tasks: Vec<_> = (0..25)
446 .map(|_| {
447 let bucket = bucket.clone();
448 tokio::spawn(async move {
449 bucket.acquire(1, true).await.unwrap();
450 start.elapsed()
451 })
452 })
453 .collect();
454 let mut done = Vec::new();
455 for task in tasks {
456 done.push(task.await.unwrap());
457 }
458 done.sort();
459 for (k, at) in done.iter().enumerate() {
460 let earliest = Duration::from_millis(100) * (k as u32 + 1).saturating_sub(5);
461 assert!(
462 *at + Duration::from_micros(1) >= earliest,
463 "request {} went at {at:?}, before the {earliest:?} the bucket allows",
464 k + 1
465 );
466 }
467 }
468
469 #[tokio::test(start_paused = true)]
470 async fn a_hold_set_during_a_wait_for_tokens_is_honoured() {
471 let bucket = bucket(1, 1);
472 timed(&bucket, 1).await;
473 let holder = {
474 let hold = bucket.hold().clone();
475 tokio::spawn(async move {
476 tokio::time::sleep(Duration::from_millis(500)).await;
477 hold.extend(Duration::from_secs(5));
478 })
479 };
480 let waited = timed(&bucket, 1).await;
481 holder.await.unwrap();
482 assert!(
483 waited >= Duration::from_millis(5_500),
484 "the token came at 1s and the request went at {waited:?}, inside the hold"
485 );
486 }
487}