saluki_core/pooling/
elastic.rs

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
31/// An elastic object pool.
32///
33/// Pools are configured with a minimum and maximum size, and allocate the minimum number of items up front. When an
34/// item is requested and the pool is empty, but hasn't yet reached its maximum size, it will allocate the item on
35/// demand.
36///
37/// After a grace period without growth, a background task shrinks idle capacity toward the minimum capacity. Shrinking
38/// removes a fraction of the excess idle items each interval, and returned items above the minimum are dropped
39/// immediately while shrinking is active. A new on-demand allocation disables shrinking for another grace period so
40/// nearby bursts can reuse the existing capacity.
41///
42/// # Missing
43///
44/// - Configurability for the shrinking frequency, grace period, and batch fraction.
45pub 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    /// Creates a new `ElasticObjectPool` with the given minimum and maximum capacity.
55    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    /// Creates a new `ElasticObjectPool` with the given minimum and maximum capacity and item builder.
68    ///
69    /// `builder` is called to construct each item.
70    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        // Allocate enough storage to hold the maximum number of items, but only _build_ the minimum number of items.
141        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/// A [`Future`] that acquires an item from an [`ElasticObjectPool`].
265#[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            // If we're not waiting from previously going down the slow path, we'll try to acquire a permit in a
299            // non-polling fashion, which avoids scheduling a wakeup for this task if we end up trying to allocate an
300            // item on demand.
301            //
302            // If we are waiting from previously going down the slow path, we'll always poll the semaphore to ensure
303            // we don't throw away a permit that was given to us while we were waiting.
304            while !*this.waiting_slow {
305                // Fast path: try to acquire a permit immediately.
306                //
307                // If we get one, we can acquire an item from the pool. Otherwise, we'll attempt to do a burst
308                // allocation if the pool hasn't yet reached its maximum capacity.
309                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                // If we're at capacity, we can't allocate any more items.
320                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                // Claim capacity and record growth under the same lock that guards shrink completion.
329                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                    // Shrinking lowered `active`, so retry the fast/on-demand path where this waiter may claim the
370                    // newly available capacity and allocate a replacement item.
371                    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    // Only shrink idle pool capacity. Waiting here can let the shrinker consume the next returned item ahead of
422    // application waiters that are already blocked on the same semaphore.
423    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        // Acquire the first item, which should already exist.
501        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        // Acquire a second item, which should be allocated on demand since we haven't reached our maximum capacity yet.
506        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        // Try to acquire a third item, which should block because our pool has reached its maximum capacity.
511        let mut third_acquire = spawn(pool.acquire());
512        assert_pending!(third_acquire.poll());
513        assert!(!third_acquire.is_woken());
514
515        // Drop the items to return them to the pool and observe that at least one makes it back, but the semaphore will
516        // divert the returned permit for the first item to the pending third acquire.
517        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        // Now our third acquire should have been notified and we should be able to acquire an item.
523        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        // Fill the pool to its maximum size: active = 2, permits = 0.
562        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        // Return one item while the pool is still at max capacity: active = 2, permits = 1.
569        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        // Simulate the shrinker winning the race to the idle permit before a new acquire starts waiting:
575        // active = 2, permits = 0, shrinker holds the permit.
576        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        // The new acquire sees active == max_capacity and permits = 0, so it waits on the semaphore.
584        let mut third_acquire = spawn(pool.acquire());
585        assert_pending!(third_acquire.poll());
586        assert!(!third_acquire.is_woken());
587
588        // Shrinking removes the idle item and lowers active: active = 1, permits = 0.
589        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        // The waiter must be woken by the active count change, otherwise it remains asleep even though
594        // active < max_capacity means it could allocate a replacement item.
595        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}