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