saluki_core/pooling/
fixed.rs

1use std::{
2    collections::VecDeque,
3    future::Future,
4    pin::Pin,
5    sync::{Arc, Mutex},
6    task::{ready, Context, Poll},
7};
8
9use pin_project::pin_project;
10use tokio::sync::Semaphore;
11use tokio_util::sync::PollSemaphore;
12
13use super::{Clearable, ObjectPool, PoolMetrics, Poolable, ReclaimStrategy};
14
15/// A fixed-size object pool.
16///
17/// All items for this object pool are created up front, and when the object pool is empty, calls to `acquire` will
18/// block until an item is returned to the pool.
19///
20/// ## Metrics
21///
22/// - `object_pool.acquired{pool_name="<pool_name>"}` - total count of the number of objects acquired from the pool (counter)
23/// - `object_pool.released{pool_name="<pool_name>"}` - total count of the number of objects released back to the pool (counter)
24/// - `object_pool.in_use{pool_name="<pool_name>"}` - number of objects from the pool that are currently in use (gauge)
25pub struct FixedSizeObjectPool<T: Poolable> {
26    strategy: Arc<FixedSizeStrategy<T>>,
27}
28
29impl<T: Poolable> FixedSizeObjectPool<T>
30where
31    T::Data: Default,
32{
33    /// Creates a new `FixedSizeObjectPool` with the given capacity.
34    ///
35    /// Metrics are emitted for the pool with a tag (`pool_name`) that's set to the value of the given pool name.
36    pub fn with_capacity<S>(pool_name: S, capacity: usize) -> Self
37    where
38        S: AsRef<str>,
39    {
40        Self {
41            strategy: Arc::new(FixedSizeStrategy::new(pool_name, capacity)),
42        }
43    }
44}
45
46impl<T: Poolable> FixedSizeObjectPool<T> {
47    /// Creates a new `FixedSizeObjectPool` with the given capacity and item builder.
48    ///
49    /// `builder` is called to construct each item.
50    ///
51    /// Metrics are emitted for the pool with a tag (`pool_name`) that's set to the value of the given pool name.
52    pub fn with_builder<S, B>(pool_name: S, capacity: usize, builder: B) -> Self
53    where
54        S: AsRef<str>,
55        B: Fn() -> T::Data,
56    {
57        Self {
58            strategy: Arc::new(FixedSizeStrategy::with_builder(pool_name, capacity, builder)),
59        }
60    }
61}
62
63impl<T: Poolable> Clone for FixedSizeObjectPool<T> {
64    fn clone(&self) -> Self {
65        Self {
66            strategy: self.strategy.clone(),
67        }
68    }
69}
70
71impl<T> ObjectPool for FixedSizeObjectPool<T>
72where
73    T: Poolable + Send + Unpin + 'static,
74{
75    type Item = T;
76    type AcquireFuture = FixedSizeAcquireFuture<T>;
77
78    fn acquire(&self) -> Self::AcquireFuture {
79        FixedSizeStrategy::acquire(&self.strategy)
80    }
81}
82
83struct FixedSizeStrategy<T: Poolable> {
84    items: Mutex<VecDeque<T::Data>>,
85    available: Arc<Semaphore>,
86    metrics: PoolMetrics,
87}
88
89impl<T> FixedSizeStrategy<T>
90where
91    T: Poolable,
92    T::Data: Default,
93{
94    fn new<S>(pool_name: S, capacity: usize) -> Self
95    where
96        S: AsRef<str>,
97    {
98        let mut items = VecDeque::with_capacity(capacity);
99        items.extend((0..capacity).map(|_| T::Data::default()));
100        let available = Arc::new(Semaphore::new(capacity));
101
102        Self {
103            items: Mutex::new(items),
104            available,
105            metrics: PoolMetrics::new(pool_name.as_ref()),
106        }
107    }
108}
109
110impl<T: Poolable> FixedSizeStrategy<T> {
111    fn with_builder<S, B>(pool_name: S, capacity: usize, builder: B) -> Self
112    where
113        S: AsRef<str>,
114        B: Fn() -> T::Data,
115    {
116        let mut items = VecDeque::with_capacity(capacity);
117        items.extend((0..capacity).map(|_| builder()));
118        let available = Arc::new(Semaphore::new(capacity));
119
120        let metrics = PoolMetrics::new(pool_name.as_ref());
121        metrics.created().increment(capacity as u64);
122        metrics.capacity().set(capacity as f64);
123
124        Self {
125            items: Mutex::new(items),
126            available,
127            metrics,
128        }
129    }
130}
131
132impl<T> FixedSizeStrategy<T>
133where
134    T: Poolable,
135    T::Data: Send + 'static,
136{
137    fn acquire(strategy: &Arc<Self>) -> FixedSizeAcquireFuture<T> {
138        FixedSizeAcquireFuture::new(Arc::clone(strategy))
139    }
140}
141
142impl<T: Poolable> ReclaimStrategy<T> for FixedSizeStrategy<T> {
143    fn reclaim(&self, mut data: T::Data) {
144        data.clear();
145
146        self.items.lock().unwrap().push_back(data);
147        self.available.add_permits(1);
148        self.metrics.released().increment(1);
149        self.metrics.in_use().decrement(1.0);
150    }
151}
152
153#[pin_project]
154pub struct FixedSizeAcquireFuture<T: Poolable> {
155    strategy: Option<Arc<FixedSizeStrategy<T>>>,
156    semaphore: PollSemaphore,
157}
158
159impl<T: Poolable> FixedSizeAcquireFuture<T> {
160    fn new(strategy: Arc<FixedSizeStrategy<T>>) -> Self {
161        let semaphore = PollSemaphore::new(Arc::clone(&strategy.available));
162        Self {
163            strategy: Some(strategy),
164            semaphore,
165        }
166    }
167}
168
169impl<T> Future for FixedSizeAcquireFuture<T>
170where
171    T: Poolable + 'static,
172{
173    type Output = T;
174
175    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
176        let this = self.project();
177
178        match ready!(this.semaphore.poll_acquire(cx)) {
179            Some(permit) => {
180                permit.forget();
181
182                let strategy = this.strategy.take().unwrap();
183                let data = strategy.items.lock().unwrap().pop_back().unwrap();
184                strategy.metrics.acquired().increment(1);
185                strategy.metrics.in_use().increment(1.0);
186                Poll::Ready(T::from_data(strategy, data))
187            }
188            None => {
189                saluki_antithesis::unreachable!("fixed object pool semaphore closed");
190                unreachable!("semaphore should never be closed")
191            }
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use saluki_metrics::test::TestRecorder;
199    use tokio_test::{assert_pending, assert_ready, task::spawn};
200
201    use super::*;
202    use crate::pooled;
203
204    pooled! {
205        struct PooledValue {
206            value: u32,
207        }
208
209        clear => |this| this.value = 0
210    }
211
212    impl std::fmt::Debug for PooledValue {
213        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214            f.debug_struct("PooledValue").finish_non_exhaustive()
215        }
216    }
217
218    #[test]
219    fn preallocates_capacity_and_blocks_when_pool_is_empty() {
220        // Documented contract: all items are created up front, and once the pool is empty `acquire`
221        // blocks until an item is returned.
222        let pool = FixedSizeObjectPool::<PooledValue>::with_capacity("test", 2);
223        assert_eq!(pool.strategy.available.available_permits(), 2);
224
225        let mut first_acquire = spawn(pool.acquire());
226        let first = assert_ready!(first_acquire.poll());
227        let mut second_acquire = spawn(pool.acquire());
228        let second = assert_ready!(second_acquire.poll());
229        assert_eq!(pool.strategy.available.available_permits(), 0);
230
231        // The pool is empty, so a third acquire must block rather than allocate a new item.
232        let mut third_acquire = spawn(pool.acquire());
233        assert_pending!(third_acquire.poll());
234        assert!(!third_acquire.is_woken());
235
236        // Returning an item wakes the blocked acquire and lets it complete.
237        drop(first);
238        assert!(third_acquire.is_woken());
239        let third = assert_ready!(third_acquire.poll());
240        assert_eq!(pool.strategy.available.available_permits(), 0);
241
242        // Returning the remaining items restores the pool to its full capacity.
243        drop(second);
244        drop(third);
245        assert_eq!(pool.strategy.available.available_permits(), 2);
246    }
247
248    #[test]
249    fn clears_items_before_returning_them_to_the_pool() {
250        // A capacity-1 pool forces reuse of the same backing item, so a value written before release
251        // must have been cleared by the time the item is re-acquired.
252        let pool = FixedSizeObjectPool::<PooledValue>::with_capacity("test", 1);
253
254        let mut first_acquire = spawn(pool.acquire());
255        let mut item = assert_ready!(first_acquire.poll());
256        item.data_mut().value = 42;
257        drop(item);
258
259        let mut second_acquire = spawn(pool.acquire());
260        let item = assert_ready!(second_acquire.poll());
261        assert_eq!(item.data().value, 0, "a released item must be cleared before reuse");
262        drop(item);
263    }
264
265    #[test]
266    fn tracks_acquire_and_release_metrics() {
267        // Documented metrics: `object_pool_acquired`/`object_pool_released` count acquisitions and
268        // releases, and `object_pool_in_use` tracks the number of currently-outstanding items.
269        let recorder = TestRecorder::default();
270        let _guard = metrics::set_default_local_recorder(&recorder);
271
272        let pool = FixedSizeObjectPool::<PooledValue>::with_capacity("test", 2);
273
274        let mut first_acquire = spawn(pool.acquire());
275        let item = assert_ready!(first_acquire.poll());
276        assert_eq!(
277            recorder.counter((PoolMetrics::acquired_name(), &[("pool_name", "test")])),
278            Some(1)
279        );
280        assert_eq!(
281            recorder.gauge((PoolMetrics::in_use_name(), &[("pool_name", "test")])),
282            Some(1.0)
283        );
284
285        drop(item);
286        assert_eq!(
287            recorder.counter((PoolMetrics::released_name(), &[("pool_name", "test")])),
288            Some(1)
289        );
290        assert_eq!(
291            recorder.gauge((PoolMetrics::in_use_name(), &[("pool_name", "test")])),
292            Some(0.0)
293        );
294    }
295}