1use std::{
2 collections::VecDeque,
3 future::Future,
4 pin::Pin,
5 sync::{
6 atomic::{
7 AtomicBool, AtomicUsize,
8 Ordering::{AcqRel, Acquire, Relaxed, Release},
9 },
10 Arc, Mutex,
11 },
12 task::{Context, Poll},
13 time::Duration,
14};
15
16use pin_project::pin_project;
17use saluki_common::resource_tracking::ResourceGroupToken;
18use tokio::{
19 sync::{futures::OwnedNotified, Notify, OwnedSemaphorePermit, Semaphore, SemaphorePermit},
20 time::{sleep, Instant},
21};
22use tokio_util::sync::PollSemaphore;
23use tracing::{debug, trace};
24
25use super::{Clearable, ObjectPool, PoolMetrics, Poolable, ReclaimStrategy};
26
27const SHRINKER_SLEEP_DURATION: Duration = Duration::from_secs(1);
28const SHRINK_GRACE_PERIOD: Duration = Duration::from_secs(5);
29const SHRINK_BATCH_DIVISOR: usize = 4;
30
31pub struct ElasticObjectPool<T: Poolable> {
46 strategy: Arc<ElasticStrategy<T>>,
47}
48
49impl<T> ElasticObjectPool<T>
50where
51 T: Poolable + 'static,
52 T::Data: Default,
53{
54 pub fn with_capacity<S>(pool_name: S, min_capacity: usize, max_capacity: usize) -> (Self, impl Future<Output = ()>)
56 where
57 S: AsRef<str>,
58 {
59 Self::with_builder(pool_name, min_capacity, max_capacity, T::Data::default)
60 }
61}
62
63impl<T> ElasticObjectPool<T>
64where
65 T: Poolable + 'static,
66{
67 pub fn with_builder<S, B>(
71 pool_name: S, min_capacity: usize, max_capacity: usize, builder: B,
72 ) -> (Self, impl Future<Output = ()>)
73 where
74 S: AsRef<str>,
75 B: Fn() -> T::Data + Send + Sync + 'static,
76 {
77 let strategy = Arc::new(ElasticStrategy::with_builder(
78 pool_name,
79 min_capacity,
80 max_capacity,
81 builder,
82 ));
83 let shrinker = run_background_shrinker(Arc::clone(&strategy));
84
85 (Self { strategy }, shrinker)
86 }
87}
88
89impl<T: Poolable> Clone for ElasticObjectPool<T> {
90 fn clone(&self) -> Self {
91 Self {
92 strategy: self.strategy.clone(),
93 }
94 }
95}
96
97impl<T> ObjectPool for ElasticObjectPool<T>
98where
99 T: Poolable + Send + Unpin + 'static,
100{
101 type Item = T;
102 type AcquireFuture = ElasticAcquireFuture<T>;
103
104 fn acquire(&self) -> Self::AcquireFuture {
105 ElasticStrategy::acquire(&self.strategy)
106 }
107}
108
109struct ElasticStrategy<T: Poolable> {
110 items: Mutex<VecDeque<T::Data>>,
111 builder: Box<dyn Fn() -> T::Data + Send + Sync>,
112 available: Arc<Semaphore>,
113 active_decreased: Arc<Notify>,
114 active: AtomicUsize,
115 shrinking_enabled: AtomicBool,
116 shrink_state: Mutex<ShrinkState>,
117 min_capacity: usize,
118 max_capacity: usize,
119 resource_group: ResourceGroupToken,
120 metrics: PoolMetrics,
121}
122
123struct ShrinkState {
124 last_growth: Instant,
125}
126
127impl<T: Poolable> ElasticStrategy<T> {
128 fn with_builder<S, B>(pool_name: S, min_capacity: usize, max_capacity: usize, builder: B) -> Self
129 where
130 S: AsRef<str>,
131 B: Fn() -> T::Data + Send + Sync + 'static,
132 {
133 assert!(
134 min_capacity <= max_capacity,
135 "minimum capacity must not exceed maximum capacity"
136 );
137
138 let builder = Box::new(builder);
139
140 let mut items = VecDeque::with_capacity(max_capacity);
142 items.extend((0..min_capacity).map(|_| builder()));
143 let available = Arc::new(Semaphore::new(min_capacity));
144
145 let metrics = PoolMetrics::new(pool_name.as_ref());
146 metrics.capacity().set(min_capacity as f64);
147 metrics.created().increment(min_capacity as u64);
148
149 Self {
150 items: Mutex::new(items),
151 builder,
152 available,
153 active_decreased: Arc::new(Notify::new()),
154 active: AtomicUsize::new(min_capacity),
155 shrinking_enabled: AtomicBool::new(false),
156 shrink_state: Mutex::new(ShrinkState {
157 last_growth: Instant::now(),
158 }),
159 min_capacity,
160 max_capacity,
161 resource_group: ResourceGroupToken::current(),
162 metrics,
163 }
164 }
165
166 fn acquire_item(&self, permit: OwnedSemaphorePermit) -> T::Data {
167 permit.forget();
168
169 let data = { self.items.lock().unwrap().pop_back().unwrap() };
170
171 self.metrics.acquired().increment(1);
172 self.metrics.in_use().increment(1.0);
173
174 data
175 }
176
177 fn try_increase_active_for_growth(&self, active: usize) -> bool {
178 let mut shrink_state = self.shrink_state.lock().unwrap();
179 if self
180 .active
181 .compare_exchange_weak(active, active + 1, AcqRel, Relaxed)
182 .is_err()
183 {
184 return false;
185 }
186
187 shrink_state.last_growth = Instant::now();
188 self.shrinking_enabled.store(false, Release);
189 true
190 }
191
192 fn enable_shrinking_if_grace_elapsed(&self) -> bool {
193 let shrink_state = self.shrink_state.lock().unwrap();
194 if shrink_state.last_growth.elapsed() < SHRINK_GRACE_PERIOD {
195 return false;
196 }
197
198 self.shrinking_enabled.store(true, Release);
199 true
200 }
201
202 fn try_decrease_active(&self) -> bool {
203 self.active
204 .fetch_update(AcqRel, Acquire, |active| {
205 (active > self.min_capacity).then_some(active - 1)
206 })
207 .is_ok()
208 }
209
210 fn shrinking_is_current(&self, shrink_state: &ShrinkState) -> bool {
211 self.shrinking_enabled.load(Acquire) && shrink_state.last_growth.elapsed() >= SHRINK_GRACE_PERIOD
212 }
213
214 fn excess_idle_capacity(&self) -> usize {
215 let idle_excess = self.available.available_permits().saturating_sub(self.min_capacity);
216 let capacity_excess = self.active.load(Acquire).saturating_sub(self.min_capacity);
217
218 idle_excess.min(capacity_excess)
219 }
220
221 fn record_deletion(&self) {
222 self.metrics.deleted().increment(1);
223 self.metrics.capacity().decrement(1.0);
224 self.active_decreased.notify_waiters();
225 }
226}
227
228impl<T> ElasticStrategy<T>
229where
230 T: Poolable,
231 T::Data: Send + 'static,
232{
233 fn acquire(strategy: &Arc<Self>) -> ElasticAcquireFuture<T> {
234 ElasticAcquireFuture::new(Arc::clone(strategy))
235 }
236}
237
238impl<T: Poolable> ReclaimStrategy<T> for ElasticStrategy<T> {
239 fn reclaim(&self, mut data: T::Data) {
240 data.clear();
241
242 self.metrics.released().increment(1);
243 self.metrics.in_use().decrement(1.0);
244
245 if self.shrinking_enabled.load(Acquire) {
246 let shrink_state = self.shrink_state.lock().unwrap();
247 if self.shrinking_is_current(&shrink_state)
248 && self.available.available_permits() >= self.min_capacity
249 && self.try_decrease_active()
250 {
251 drop(shrink_state);
252 drop(data);
253 self.record_deletion();
254 trace!("Dropped returned item above the minimum capacity.");
255 return;
256 }
257 }
258
259 self.items.lock().unwrap().push_back(data);
260 self.available.add_permits(1);
261 }
262}
263
264#[pin_project]
266pub struct ElasticAcquireFuture<T: Poolable> {
267 strategy: Option<Arc<ElasticStrategy<T>>>,
268 waiting_slow: bool,
269 semaphore: PollSemaphore,
270 #[pin]
271 active_decreased: OwnedNotified,
272}
273
274impl<T: Poolable> ElasticAcquireFuture<T> {
275 fn new(strategy: Arc<ElasticStrategy<T>>) -> Self {
276 let semaphore = PollSemaphore::new(Arc::clone(&strategy.available));
277 let active_decreased = strategy.active_decreased.clone().notified_owned();
278 Self {
279 strategy: Some(strategy),
280 waiting_slow: false,
281 semaphore,
282 active_decreased,
283 }
284 }
285}
286
287impl<T> Future for ElasticAcquireFuture<T>
288where
289 T: Poolable + 'static,
290{
291 type Output = T;
292
293 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
294 let mut this = self.project();
295 let strategy = this.strategy.take().unwrap();
296
297 loop {
298 while !*this.waiting_slow {
305 if let Ok(permit) = strategy.available.clone().try_acquire_owned() {
310 trace!("Acquired permit on fast path. Acquiring item from pool.");
311
312 let data = strategy.acquire_item(permit);
313
314 return Poll::Ready(T::from_data(strategy, data));
315 }
316
317 trace!("No available permits. Attempting to allocate on demand.");
318
319 let active = strategy.active.load(Acquire);
321 if active == strategy.max_capacity {
322 trace!("Pool at capacity. Falling back to waiting for next available permit.");
323
324 *this.waiting_slow = true;
325 break;
326 }
327
328 if !strategy.try_increase_active_for_growth(active) {
330 continue;
331 }
332
333 trace!("Updated active count. Allocating on demand.");
334
335 let new_item = {
336 let _entered = strategy.resource_group.enter();
337 (strategy.builder)()
338 };
339
340 strategy.metrics.created().increment(1);
341 strategy.metrics.capacity().increment(1.0);
342 strategy.metrics.in_use().increment(1.0);
343
344 return Poll::Ready(T::from_data(strategy, new_item));
345 }
346
347 trace!("Waiting for next available permit.");
348 match this.semaphore.poll_acquire(cx) {
349 Poll::Ready(Some(permit)) => {
350 trace!("Acquired permit. Acquiring item from pool.");
351
352 let data = strategy.acquire_item(permit);
353
354 return Poll::Ready(T::from_data(strategy, data));
355 }
356 Poll::Ready(None) => {
357 saluki_antithesis::unreachable!("elastic object pool semaphore closed");
358 unreachable!("semaphore should never be closed")
359 }
360 Poll::Pending => {
361 trace!("Permit not yet available. Waiting for next available permit.");
362 }
363 }
364
365 match this.active_decreased.as_mut().poll(cx) {
366 Poll::Ready(()) => {
367 trace!("Active count decreased. Retrying acquisition.");
368
369 this.active_decreased
372 .set(strategy.active_decreased.clone().notified_owned());
373 *this.semaphore = PollSemaphore::new(Arc::clone(&strategy.available));
374 *this.waiting_slow = false;
375 }
376 Poll::Pending => {
377 this.strategy.replace(strategy);
378 return Poll::Pending;
379 }
380 }
381 }
382 }
383}
384
385async fn run_background_shrinker<T: Poolable>(strategy: Arc<ElasticStrategy<T>>) {
386 loop {
387 debug!("Shrinker sleeping.");
388 sleep(SHRINKER_SLEEP_DURATION).await;
389
390 if !strategy.enable_shrinking_if_grace_elapsed() {
391 debug!("Object pool is within the post-growth grace period. Skipping shrinking.");
392 continue;
393 }
394
395 let idle_excess = strategy.excess_idle_capacity();
396 if idle_excess == 0 {
397 debug!("Object pool has no excess idle capacity. Nothing to shrink.");
398 continue;
399 }
400
401 let batch_size = idle_excess.div_ceil(SHRINK_BATCH_DIVISOR);
402 let removed = try_shrink_available_items(&strategy, batch_size);
403 debug!(
404 idle_excess,
405 batch_size, removed, "Shrank excess idle object pool capacity."
406 );
407 }
408}
409
410fn try_shrink_available_items<T: Poolable>(strategy: &ElasticStrategy<T>, count: usize) -> usize {
411 (0..count)
412 .take_while(|_| try_shrink_one_available_item(strategy))
413 .count()
414}
415
416fn try_shrink_one_available_item<T: Poolable>(strategy: &ElasticStrategy<T>) -> bool {
417 if strategy.excess_idle_capacity() == 0 {
418 return false;
419 }
420
421 let Ok(permit) = strategy.available.try_acquire() else {
424 debug!("Pool has excess idle capacity, but no idle item is available.");
425 return false;
426 };
427
428 try_shrink_available_item(strategy, permit)
429}
430
431fn try_shrink_available_item<T: Poolable>(strategy: &ElasticStrategy<T>, permit: SemaphorePermit<'_>) -> bool {
432 let shrink_state = strategy.shrink_state.lock().unwrap();
433 if !strategy.shrinking_is_current(&shrink_state) {
434 return false;
435 }
436
437 let mut items = strategy.items.lock().unwrap();
438 if !strategy.try_decrease_active() {
439 return false;
440 }
441 let item = items.pop_back().unwrap();
442 drop(items);
443 drop(shrink_state);
444 drop(item);
445
446 permit.forget();
447 strategy.record_deletion();
448 true
449}
450
451#[cfg(test)]
452mod tests {
453 use std::sync::atomic::Ordering::{Acquire, Release};
454
455 use tokio_test::{assert_pending, assert_ready, task::spawn};
456
457 use super::{
458 try_shrink_available_item, try_shrink_one_available_item, ElasticObjectPool, SHRINKER_SLEEP_DURATION,
459 SHRINK_GRACE_PERIOD,
460 };
461 use crate::{pooled, pooling::ObjectPool as _};
462
463 pooled! {
464 struct TestObject {
465 value: u32,
466 }
467
468 clear => |this| this.value = 0
469 }
470
471 impl std::fmt::Debug for TestObject {
472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473 f.debug_struct("TestObject").finish_non_exhaustive()
474 }
475 }
476
477 fn make_shrink_eligible(pool: &ElasticObjectPool<TestObject>) {
478 pool.strategy.shrink_state.lock().unwrap().last_growth = tokio::time::Instant::now() - SHRINK_GRACE_PERIOD;
479 pool.strategy.shrinking_enabled.store(true, Release);
480 }
481
482 #[test]
483 fn basic() {
484 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
485 assert_eq!(pool.strategy.available.available_permits(), 1);
486
487 let mut acquire = spawn(pool.acquire());
488 let item = assert_ready!(acquire.poll());
489 assert_eq!(pool.strategy.available.available_permits(), 0);
490
491 drop(item);
492 assert_eq!(pool.strategy.available.available_permits(), 1);
493 }
494
495 #[test]
496 fn burst_allocation() {
497 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
498 assert_eq!(pool.strategy.available.available_permits(), 1);
499
500 let mut first_acquire = spawn(pool.acquire());
502 let first_item = assert_ready!(first_acquire.poll());
503 assert_eq!(pool.strategy.available.available_permits(), 0);
504
505 let mut second_acquire = spawn(pool.acquire());
507 let second_item = assert_ready!(second_acquire.poll());
508 assert_eq!(pool.strategy.available.available_permits(), 0);
509
510 let mut third_acquire = spawn(pool.acquire());
512 assert_pending!(third_acquire.poll());
513 assert!(!third_acquire.is_woken());
514
515 drop(first_item);
518 assert_eq!(pool.strategy.available.available_permits(), 0);
519 drop(second_item);
520 assert_eq!(pool.strategy.available.available_permits(), 1);
521
522 assert!(third_acquire.is_woken());
524 let third_item = assert_ready!(third_acquire.poll());
525 assert_eq!(pool.strategy.available.available_permits(), 1);
526
527 drop(third_item);
528 assert_eq!(pool.strategy.available.available_permits(), 2);
529 }
530
531 #[test]
532 fn shrinker_does_not_wait_for_returned_items() {
533 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
534
535 let mut first_acquire = spawn(pool.acquire());
536 let first_item = assert_ready!(first_acquire.poll());
537
538 let mut second_acquire = spawn(pool.acquire());
539 let second_item = assert_ready!(second_acquire.poll());
540
541 let mut third_acquire = spawn(pool.acquire());
542 assert_pending!(third_acquire.poll());
543 assert!(!third_acquire.is_woken());
544
545 assert!(!try_shrink_one_available_item(&pool.strategy));
546
547 drop(first_item);
548 assert!(third_acquire.is_woken());
549
550 let third_item = assert_ready!(third_acquire.poll());
551 assert_eq!(pool.strategy.active.load(Acquire), 2);
552
553 drop(second_item);
554 drop(third_item);
555 }
556
557 #[test]
558 fn slow_waiter_retries_allocation_when_shrink_reduces_active() {
559 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
560
561 let mut first_acquire = spawn(pool.acquire());
563 let first_item = assert_ready!(first_acquire.poll());
564
565 let mut second_acquire = spawn(pool.acquire());
566 let second_item = assert_ready!(second_acquire.poll());
567
568 drop(second_item);
570 assert_eq!(pool.strategy.active.load(Acquire), 2);
571 assert_eq!(pool.strategy.available.available_permits(), 1);
572 make_shrink_eligible(&pool);
573
574 let permit = pool
577 .strategy
578 .available
579 .try_acquire()
580 .expect("returned item should leave one idle permit for the shrinker");
581 assert_eq!(pool.strategy.available.available_permits(), 0);
582
583 let mut third_acquire = spawn(pool.acquire());
585 assert_pending!(third_acquire.poll());
586 assert!(!third_acquire.is_woken());
587
588 assert!(try_shrink_available_item(&pool.strategy, permit));
590 assert_eq!(pool.strategy.active.load(Acquire), 1);
591 assert_eq!(pool.strategy.available.available_permits(), 0);
592
593 assert!(
596 third_acquire.is_woken(),
597 "slow-path waiters must wake when shrinking creates on-demand allocation capacity"
598 );
599
600 let third_item = assert_ready!(third_acquire.poll());
601 assert_eq!(pool.strategy.active.load(Acquire), 2);
602
603 drop(first_item);
604 drop(third_item);
605 }
606
607 #[test]
608 fn eligible_pool_drops_returned_items_above_minimum_capacity() {
609 let (pool, _) = ElasticObjectPool::<TestObject>::with_builder("test", 1, 4, || TestObjectInner { value: 0 });
610
611 let mut items = Vec::new();
612 for _ in 0..4 {
613 let mut acquire = spawn(pool.acquire());
614 items.push(assert_ready!(acquire.poll()));
615 }
616 make_shrink_eligible(&pool);
617
618 drop(items);
619
620 assert_eq!(pool.strategy.active.load(Acquire), 1);
621 assert_eq!(pool.strategy.available.available_permits(), 1);
622 assert_eq!(pool.strategy.items.lock().unwrap().len(), 1);
623 }
624
625 #[test]
626 fn fresh_growth_cancels_an_in_flight_shrink() {
627 let (pool, _) = ElasticObjectPool::<TestObject>::with_builder("test", 1, 4, || TestObjectInner { value: 0 });
628
629 let mut first_acquire = spawn(pool.acquire());
630 let first_item = assert_ready!(first_acquire.poll());
631 let mut second_acquire = spawn(pool.acquire());
632 let second_item = assert_ready!(second_acquire.poll());
633 let mut third_acquire = spawn(pool.acquire());
634 let third_item = assert_ready!(third_acquire.poll());
635 drop(second_item);
636 drop(third_item);
637 make_shrink_eligible(&pool);
638
639 let shrink_permit = pool
640 .strategy
641 .available
642 .try_acquire()
643 .expect("an idle item should be available to the shrinker");
644 let mut fourth_acquire = spawn(pool.acquire());
645 let fourth_item = assert_ready!(fourth_acquire.poll());
646 let mut fifth_acquire = spawn(pool.acquire());
647 let fifth_item = assert_ready!(fifth_acquire.poll());
648
649 assert!(!pool.strategy.shrinking_enabled.load(Acquire));
650 assert!(
651 !try_shrink_available_item(&pool.strategy, shrink_permit),
652 "growth must cancel shrink work that sampled an older grace period"
653 );
654 assert_eq!(pool.strategy.active.load(Acquire), 4);
655 assert_eq!(pool.strategy.available.available_permits(), 1);
656
657 drop(first_item);
658 drop(fourth_item);
659 drop(fifth_item);
660 }
661
662 #[tokio::test(start_paused = true)]
663 async fn background_shrinker_honors_grace_and_shrinks_idle_excess_in_batches() {
664 let (pool, shrinker) =
665 ElasticObjectPool::<TestObject>::with_builder("test", 1, 10, || TestObjectInner { value: 0 });
666
667 let mut items = Vec::new();
668 for _ in 0..10 {
669 let mut acquire = spawn(pool.acquire());
670 items.push(assert_ready!(acquire.poll()));
671 }
672 assert_eq!(pool.strategy.active.load(Acquire), 10);
673 drop(items);
674 assert_eq!(pool.strategy.available.available_permits(), 10);
675
676 let mut shrinker = spawn(shrinker);
677 assert_pending!(shrinker.poll());
678
679 tokio::time::advance(SHRINK_GRACE_PERIOD - SHRINKER_SLEEP_DURATION).await;
680 assert_pending!(shrinker.poll());
681 assert_eq!(pool.strategy.active.load(Acquire), 10);
682
683 tokio::time::advance(SHRINKER_SLEEP_DURATION).await;
684 assert_pending!(shrinker.poll());
685 assert_eq!(pool.strategy.active.load(Acquire), 7);
686 assert_eq!(pool.strategy.available.available_permits(), 7);
687
688 for _ in 0..8 {
689 tokio::time::advance(SHRINKER_SLEEP_DURATION).await;
690 assert_pending!(shrinker.poll());
691 }
692 assert_eq!(pool.strategy.active.load(Acquire), 1);
693 assert_eq!(pool.strategy.available.available_permits(), 1);
694 }
695
696 #[tokio::test(start_paused = true)]
697 async fn on_demand_growth_restarts_shrink_grace_period() {
698 let (pool, shrinker) =
699 ElasticObjectPool::<TestObject>::with_builder("test", 1, 3, || TestObjectInner { value: 0 });
700 let mut shrinker = spawn(shrinker);
701 assert_pending!(shrinker.poll());
702
703 tokio::time::advance(SHRINK_GRACE_PERIOD).await;
704 assert_pending!(shrinker.poll());
705 assert!(pool.strategy.shrinking_enabled.load(Acquire));
706
707 let mut first_acquire = spawn(pool.acquire());
708 let first_item = assert_ready!(first_acquire.poll());
709 let mut second_acquire = spawn(pool.acquire());
710 let second_item = assert_ready!(second_acquire.poll());
711 assert!(!pool.strategy.shrinking_enabled.load(Acquire));
712 drop(first_item);
713 drop(second_item);
714 assert_eq!(pool.strategy.active.load(Acquire), 2);
715
716 tokio::time::advance(SHRINK_GRACE_PERIOD - SHRINKER_SLEEP_DURATION).await;
717 assert_pending!(shrinker.poll());
718 assert_eq!(pool.strategy.active.load(Acquire), 2);
719
720 tokio::time::advance(SHRINKER_SLEEP_DURATION).await;
721 assert_pending!(shrinker.poll());
722 assert_eq!(pool.strategy.active.load(Acquire), 1);
723 }
724}