1use std::{
2 collections::VecDeque,
3 future::Future,
4 pin::Pin,
5 sync::{
6 atomic::{
7 AtomicUsize,
8 Ordering::{AcqRel, Acquire, Relaxed},
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,
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);
28
29pub struct ElasticObjectPool<T: Poolable> {
43 strategy: Arc<ElasticStrategy<T>>,
44}
45
46impl<T> ElasticObjectPool<T>
47where
48 T: Poolable + 'static,
49 T::Data: Default,
50{
51 pub fn with_capacity<S>(pool_name: S, min_capacity: usize, max_capacity: usize) -> (Self, impl Future<Output = ()>)
53 where
54 S: AsRef<str>,
55 {
56 Self::with_builder(pool_name, min_capacity, max_capacity, T::Data::default)
57 }
58}
59
60impl<T> ElasticObjectPool<T>
61where
62 T: Poolable + 'static,
63{
64 pub fn with_builder<S, B>(
68 pool_name: S, min_capacity: usize, max_capacity: usize, builder: B,
69 ) -> (Self, impl Future<Output = ()>)
70 where
71 S: AsRef<str>,
72 B: Fn() -> T::Data + Send + Sync + 'static,
73 {
74 let strategy = Arc::new(ElasticStrategy::with_builder(
75 pool_name,
76 min_capacity,
77 max_capacity,
78 builder,
79 ));
80 let shrinker = run_background_shrinker(Arc::clone(&strategy));
81
82 (Self { strategy }, shrinker)
83 }
84}
85
86impl<T: Poolable> Clone for ElasticObjectPool<T> {
87 fn clone(&self) -> Self {
88 Self {
89 strategy: self.strategy.clone(),
90 }
91 }
92}
93
94impl<T> ObjectPool for ElasticObjectPool<T>
95where
96 T: Poolable + Send + Unpin + 'static,
97{
98 type Item = T;
99 type AcquireFuture = ElasticAcquireFuture<T>;
100
101 fn acquire(&self) -> Self::AcquireFuture {
102 ElasticStrategy::acquire(&self.strategy)
103 }
104}
105
106struct ElasticStrategy<T: Poolable> {
107 items: Mutex<VecDeque<T::Data>>,
108 builder: Box<dyn Fn() -> T::Data + Send + Sync>,
109 available: Arc<Semaphore>,
110 active_decreased: Arc<Notify>,
111 active: AtomicUsize,
112 on_demand_allocs: AtomicUsize,
113 min_capacity: usize,
114 max_capacity: usize,
115 resource_group: ResourceGroupToken,
116 metrics: PoolMetrics,
117}
118
119impl<T: Poolable> ElasticStrategy<T> {
120 fn with_builder<S, B>(pool_name: S, min_capacity: usize, max_capacity: usize, builder: B) -> Self
121 where
122 S: AsRef<str>,
123 B: Fn() -> T::Data + Send + Sync + 'static,
124 {
125 let builder = Box::new(builder);
126
127 let mut items = VecDeque::with_capacity(max_capacity);
129 items.extend((0..min_capacity).map(|_| builder()));
130 let available = Arc::new(Semaphore::new(min_capacity));
131
132 let metrics = PoolMetrics::new(pool_name.as_ref());
133 metrics.capacity().set(min_capacity as f64);
134 metrics.created().increment(min_capacity as u64);
135
136 Self {
137 items: Mutex::new(items),
138 builder,
139 available,
140 active_decreased: Arc::new(Notify::new()),
141 active: AtomicUsize::new(min_capacity),
142 on_demand_allocs: AtomicUsize::new(0),
143 min_capacity,
144 max_capacity,
145 resource_group: ResourceGroupToken::current(),
146 metrics,
147 }
148 }
149
150 fn acquire_item(&self, permit: OwnedSemaphorePermit) -> T::Data {
151 permit.forget();
152
153 let data = { self.items.lock().unwrap().pop_back().unwrap() };
154
155 self.metrics.acquired().increment(1);
156 self.metrics.in_use().increment(1.0);
157
158 data
159 }
160}
161
162impl<T> ElasticStrategy<T>
163where
164 T: Poolable,
165 T::Data: Send + 'static,
166{
167 fn acquire(strategy: &Arc<Self>) -> ElasticAcquireFuture<T> {
168 ElasticAcquireFuture::new(Arc::clone(strategy))
169 }
170}
171
172impl<T: Poolable> ReclaimStrategy<T> for ElasticStrategy<T> {
173 fn reclaim(&self, mut data: T::Data) {
174 data.clear();
175
176 self.items.lock().unwrap().push_back(data);
177 self.available.add_permits(1);
178 self.metrics.released().increment(1);
179 self.metrics.in_use().decrement(1.0);
180 }
181}
182
183#[pin_project]
185pub struct ElasticAcquireFuture<T: Poolable> {
186 strategy: Option<Arc<ElasticStrategy<T>>>,
187 waiting_slow: bool,
188 semaphore: PollSemaphore,
189 #[pin]
190 active_decreased: OwnedNotified,
191}
192
193impl<T: Poolable> ElasticAcquireFuture<T> {
194 fn new(strategy: Arc<ElasticStrategy<T>>) -> Self {
195 let semaphore = PollSemaphore::new(Arc::clone(&strategy.available));
196 let active_decreased = strategy.active_decreased.clone().notified_owned();
197 Self {
198 strategy: Some(strategy),
199 waiting_slow: false,
200 semaphore,
201 active_decreased,
202 }
203 }
204}
205
206impl<T> Future for ElasticAcquireFuture<T>
207where
208 T: Poolable + 'static,
209{
210 type Output = T;
211
212 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
213 let mut this = self.project();
214 let strategy = this.strategy.take().unwrap();
215
216 loop {
217 while !*this.waiting_slow {
224 if let Ok(permit) = strategy.available.clone().try_acquire_owned() {
229 trace!("Acquired permit on fast path. Acquiring item from pool.");
230
231 let data = strategy.acquire_item(permit);
232
233 return Poll::Ready(T::from_data(strategy, data));
234 }
235
236 trace!("No available permits. Attempting to allocate on demand.");
237
238 let active = strategy.active.load(Acquire);
240 if active == strategy.max_capacity {
241 trace!("Pool at capacity. Falling back to waiting for next available permit.");
242
243 *this.waiting_slow = true;
244 break;
245 }
246
247 if strategy
250 .active
251 .compare_exchange_weak(active, active + 1, AcqRel, Relaxed)
252 .is_err()
253 {
254 continue;
255 }
256
257 trace!("Updated active count. Allocating on demand.");
258
259 let new_item = {
260 let _entered = strategy.resource_group.enter();
261 (strategy.builder)()
262 };
263
264 strategy.on_demand_allocs.fetch_add(1, Relaxed);
265 strategy.metrics.created().increment(1);
266 strategy.metrics.capacity().increment(1.0);
267 strategy.metrics.in_use().increment(1.0);
268
269 return Poll::Ready(T::from_data(strategy, new_item));
270 }
271
272 trace!("Waiting for next available permit.");
273 match this.semaphore.poll_acquire(cx) {
274 Poll::Ready(Some(permit)) => {
275 trace!("Acquired permit. Acquiring item from pool.");
276
277 let data = strategy.acquire_item(permit);
278
279 return Poll::Ready(T::from_data(strategy, data));
280 }
281 Poll::Ready(None) => {
282 saluki_antithesis::unreachable!("elastic object pool semaphore closed");
283 unreachable!("semaphore should never be closed")
284 }
285 Poll::Pending => {
286 trace!("Permit not yet available. Waiting for next available permit.");
287 }
288 }
289
290 match this.active_decreased.as_mut().poll(cx) {
291 Poll::Ready(()) => {
292 trace!("Active count decreased. Retrying acquisition.");
293
294 this.active_decreased
297 .set(strategy.active_decreased.clone().notified_owned());
298 *this.semaphore = PollSemaphore::new(Arc::clone(&strategy.available));
299 *this.waiting_slow = false;
300 }
301 Poll::Pending => {
302 this.strategy.replace(strategy);
303 return Poll::Pending;
304 }
305 }
306 }
307 }
308}
309
310async fn run_background_shrinker<T: Poolable>(strategy: Arc<ElasticStrategy<T>>) {
311 let mut average_demand = Ewma::new(0.1);
324 let min_capacity = strategy.min_capacity;
325
326 loop {
327 debug!("Shrinker sleeping.");
328 sleep(SHRINKER_SLEEP_DURATION).await;
329
330 let active = strategy.active.load(Relaxed);
335 if active <= min_capacity {
336 debug!("Object pool already at minimum capacity. Nothing to do.");
337 continue;
338 }
339
340 let delta_demand = strategy.on_demand_allocs.swap(0, Relaxed);
341 average_demand.update(delta_demand as f64);
342 if average_demand.value() < 1.0 {
343 debug!(
344 avg_demand = average_demand.value(),
345 active, min_capacity, "Pool qualifies for shrinking. Attempting to remove single item..."
346 );
347
348 try_shrink_one_available_item(&strategy);
349 } else {
350 debug!(
351 avg_demand = average_demand.value(),
352 active, min_capacity, "Pool does not qualify for shrinking."
353 );
354 }
355 }
356}
357
358fn try_shrink_one_available_item<T: Poolable>(strategy: &ElasticStrategy<T>) -> bool {
359 let Ok(permit) = strategy.available.try_acquire() else {
362 debug!("Pool qualifies for shrinking, but no idle item is available.");
363 return false;
364 };
365
366 shrink_available_item(strategy, permit);
368 true
369}
370
371fn shrink_available_item<T: Poolable>(strategy: &ElasticStrategy<T>, permit: SemaphorePermit<'_>) {
372 let item = {
374 let item = strategy.items.lock().unwrap().pop_back().unwrap();
375 strategy.active.fetch_sub(1, AcqRel);
376 item
377 };
378
379 drop(item);
381 strategy.metrics.deleted().increment(1);
382 strategy.metrics.capacity().decrement(1.0);
383
384 permit.forget();
386
387 strategy.active_decreased.notify_waiters();
388
389 debug!("Shrinker successfully removed an item from the pool.");
390}
391
392struct Ewma {
393 value: f64,
394 alpha: f64,
395}
396
397impl Ewma {
398 fn new(alpha: f64) -> Self {
399 Self { value: 0.0, alpha }
400 }
401
402 fn update(&mut self, new_value: f64) {
403 self.value = (1.0 - self.alpha) * self.value + self.alpha * new_value;
404 }
405
406 fn value(&self) -> f64 {
407 self.value
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use std::sync::atomic::Ordering::Acquire;
414
415 use tokio_test::{assert_pending, assert_ready, task::spawn};
416
417 use super::{shrink_available_item, try_shrink_one_available_item, ElasticObjectPool};
418 use crate::{pooled, pooling::ObjectPool as _};
419
420 pooled! {
421 struct TestObject {
422 value: u32,
423 }
424
425 clear => |this| this.value = 0
426 }
427
428 impl std::fmt::Debug for TestObject {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 f.debug_struct("TestObject").finish_non_exhaustive()
431 }
432 }
433
434 #[test]
435 fn basic() {
436 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
437 assert_eq!(pool.strategy.available.available_permits(), 1);
438
439 let mut acquire = spawn(pool.acquire());
440 let item = assert_ready!(acquire.poll());
441 assert_eq!(pool.strategy.available.available_permits(), 0);
442
443 drop(item);
444 assert_eq!(pool.strategy.available.available_permits(), 1);
445 }
446
447 #[test]
448 fn burst_allocation() {
449 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
450 assert_eq!(pool.strategy.available.available_permits(), 1);
451
452 let mut first_acquire = spawn(pool.acquire());
454 let first_item = assert_ready!(first_acquire.poll());
455 assert_eq!(pool.strategy.available.available_permits(), 0);
456
457 let mut second_acquire = spawn(pool.acquire());
459 let second_item = assert_ready!(second_acquire.poll());
460 assert_eq!(pool.strategy.available.available_permits(), 0);
461
462 let mut third_acquire = spawn(pool.acquire());
464 assert_pending!(third_acquire.poll());
465 assert!(!third_acquire.is_woken());
466
467 drop(first_item);
470 assert_eq!(pool.strategy.available.available_permits(), 0);
471 drop(second_item);
472 assert_eq!(pool.strategy.available.available_permits(), 1);
473
474 assert!(third_acquire.is_woken());
476 let third_item = assert_ready!(third_acquire.poll());
477 assert_eq!(pool.strategy.available.available_permits(), 1);
478
479 drop(third_item);
480 assert_eq!(pool.strategy.available.available_permits(), 2);
481 }
482
483 #[test]
484 fn shrinker_does_not_wait_for_returned_items() {
485 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
486
487 let mut first_acquire = spawn(pool.acquire());
488 let first_item = assert_ready!(first_acquire.poll());
489
490 let mut second_acquire = spawn(pool.acquire());
491 let second_item = assert_ready!(second_acquire.poll());
492
493 let mut third_acquire = spawn(pool.acquire());
494 assert_pending!(third_acquire.poll());
495 assert!(!third_acquire.is_woken());
496
497 assert!(!try_shrink_one_available_item(&pool.strategy));
498
499 drop(first_item);
500 assert!(third_acquire.is_woken());
501
502 let third_item = assert_ready!(third_acquire.poll());
503 assert_eq!(pool.strategy.active.load(Acquire), 2);
504
505 drop(second_item);
506 drop(third_item);
507 }
508
509 #[test]
510 fn slow_waiter_retries_allocation_when_shrink_reduces_active() {
511 let (pool, _) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 2);
512
513 let mut first_acquire = spawn(pool.acquire());
515 let first_item = assert_ready!(first_acquire.poll());
516
517 let mut second_acquire = spawn(pool.acquire());
518 let second_item = assert_ready!(second_acquire.poll());
519
520 drop(second_item);
522 assert_eq!(pool.strategy.active.load(Acquire), 2);
523 assert_eq!(pool.strategy.available.available_permits(), 1);
524
525 let permit = pool
528 .strategy
529 .available
530 .try_acquire()
531 .expect("returned item should leave one idle permit for the shrinker");
532 assert_eq!(pool.strategy.available.available_permits(), 0);
533
534 let mut third_acquire = spawn(pool.acquire());
536 assert_pending!(third_acquire.poll());
537 assert!(!third_acquire.is_woken());
538
539 shrink_available_item(&pool.strategy, permit);
541 assert_eq!(pool.strategy.active.load(Acquire), 1);
542 assert_eq!(pool.strategy.available.available_permits(), 0);
543
544 assert!(
547 third_acquire.is_woken(),
548 "slow-path waiters must wake when shrinking creates on-demand allocation capacity"
549 );
550
551 let third_item = assert_ready!(third_acquire.poll());
552 assert_eq!(pool.strategy.active.load(Acquire), 2);
553
554 drop(first_item);
555 drop(third_item);
556 }
557
558 #[tokio::test(start_paused = true)]
559 async fn background_shrinker_shrinks_idle_pool_to_min_capacity() {
560 use super::SHRINKER_SLEEP_DURATION;
561
562 let (pool, shrinker) = ElasticObjectPool::<TestObject>::with_capacity("test", 1, 3);
564
565 let mut items = Vec::new();
569 for _ in 0..3 {
570 let mut acquire = spawn(pool.acquire());
571 items.push(assert_ready!(acquire.poll()));
572 }
573 assert_eq!(pool.strategy.active.load(Acquire), 3);
574 drop(items);
575 assert_eq!(pool.strategy.available.available_permits(), 3);
576
577 let mut shrinker = spawn(shrinker);
579 assert_pending!(shrinker.poll()); tokio::time::advance(SHRINKER_SLEEP_DURATION).await;
584 assert_pending!(shrinker.poll());
585 assert_eq!(pool.strategy.active.load(Acquire), 2);
586 assert_eq!(pool.strategy.available.available_permits(), 2);
587
588 tokio::time::advance(SHRINKER_SLEEP_DURATION).await;
589 assert_pending!(shrinker.poll());
590 assert_eq!(pool.strategy.active.load(Acquire), 1);
591 assert_eq!(pool.strategy.available.available_permits(), 1);
592
593 tokio::time::advance(SHRINKER_SLEEP_DURATION).await;
596 assert_pending!(shrinker.poll());
597 assert_eq!(
598 pool.strategy.active.load(Acquire),
599 1,
600 "the shrinker must never shrink a pool below its minimum capacity"
601 );
602 assert_eq!(pool.strategy.available.available_permits(), 1);
603 }
604}