saluki_core/pooling/
fixed.rs1use 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
15pub struct FixedSizeObjectPool<T: Poolable> {
26 strategy: Arc<FixedSizeStrategy<T>>,
27}
28
29impl<T: Poolable> FixedSizeObjectPool<T>
30where
31 T::Data: Default,
32{
33 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 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 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 let mut third_acquire = spawn(pool.acquire());
233 assert_pending!(third_acquire.poll());
234 assert!(!third_acquire.is_woken());
235
236 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 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 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 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}