ferroui_base/utilities/
object_pool.rs1use std::sync::atomic::{AtomicI32, Ordering};
2use std::sync::{Mutex, PoisonError};
3
4pub struct ObjectPool<T> {
6 factory: Box<dyn Fn() -> T + Send + Sync>,
7 validator: Option<Box<dyn Fn(&mut T) -> bool + Send + Sync>>,
8 items: Mutex<Vec<T>>,
9 max_size: i32,
10 count: AtomicI32,
11}
12
13impl<T> ObjectPool<T> {
14 pub const DEFAULT_MAX_SIZE: i32 = 32;
16
17 pub fn new(
26 factory: impl Fn() -> T + Send + Sync + 'static,
27 validator: Option<Box<dyn Fn(&mut T) -> bool + Send + Sync>>,
28 max_size: i32,
29 ) -> Self {
30 if max_size < 1 {
31 panic!("maxSize must be at least 1. (Parameter 'maxSize')\nActual value was {max_size}.");
32 }
33
34 Self {
35 factory: Box::new(factory),
36 validator,
37 max_size,
38 items: Mutex::new(Vec::new()),
39 count: AtomicI32::new(0),
40 }
41 }
42
43 #[inline]
45 pub fn rent(&self) -> T {
46 let item = self.items.lock().unwrap_or_else(PoisonError::into_inner).pop();
47 if let Some(item) = item {
48 self.count.fetch_sub(1, Ordering::SeqCst);
49 return item;
50 }
51
52 (self.factory)()
53 }
54
55 #[inline]
63 pub fn return_item(&self, mut item: T) {
64 if let Some(validator) = &self.validator {
66 if !validator(&mut item) {
67 return;
68 }
69 }
70
71 if self.count.load(Ordering::SeqCst) >= self.max_size {
73 return;
74 }
75
76 let current_count = self.count.fetch_add(1, Ordering::SeqCst) + 1;
78
79 if current_count <= self.max_size {
80 self.items.lock().unwrap_or_else(PoisonError::into_inner).push(item);
81 } else {
82 self.count.fetch_sub(1, Ordering::SeqCst);
84 }
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
94 use std::sync::Arc;
95
96 #[derive(Default)]
97 struct Item {
98 state: AtomicI32,
99 }
100
101 type Pool = ObjectPool<Arc<Item>>;
102
103 fn new_item() -> Arc<Item> {
104 Arc::new(Item::default())
105 }
106
107 fn contains(items: &[Arc<Item>], item: &Arc<Item>) -> bool {
108 items.iter().any(|other| Arc::ptr_eq(other, item))
109 }
110
111 #[test]
112 fn constructor_throws_when_max_size_is_less_than_one() {
113 for max_size in [0, -1, i32::MIN] {
114 let result = std::panic::catch_unwind(|| Pool::new(new_item, None, max_size));
115 assert!(result.is_err(), "max size {max_size}");
116 }
117 }
118
119 #[test]
120 fn constructor_accepts_max_size_of_one() {
121 let pool = Pool::new(new_item, None, 1);
122
123 let item = pool.rent();
124
125 assert_eq!(0, item.state.load(Ordering::SeqCst));
126 }
127
128 #[test]
129 fn rent_creates_new_item_when_pool_is_empty() {
130 let pool = Pool::new(new_item, None, Pool::DEFAULT_MAX_SIZE);
131
132 let first = pool.rent();
133 let second = pool.rent();
134
135 assert!(!Arc::ptr_eq(&first, &second));
136 }
137
138 #[test]
139 fn returned_item_is_reused_by_subsequent_rent() {
140 let pool = Pool::new(new_item, None, Pool::DEFAULT_MAX_SIZE);
141
142 let item = pool.rent();
143 pool.return_item(item.clone());
144
145 let rented = pool.rent();
146
147 assert!(Arc::ptr_eq(&item, &rented));
148 }
149
150 #[test]
151 fn return_drops_item_when_pool_is_full() {
152 let pool = Pool::new(new_item, None, 2);
153
154 let a = new_item();
155 let b = new_item();
156 let c = new_item();
157
158 pool.return_item(a.clone());
159 pool.return_item(b.clone());
160 pool.return_item(c.clone()); let rented = [pool.rent(), pool.rent()];
163
164 assert!(contains(&rented, &a));
166 assert!(contains(&rented, &b));
167 assert!(!contains(&rented, &c));
168 }
169
170 #[test]
171 fn validator_is_invoked_on_return() {
172 let validator_calls = Arc::new(AtomicI32::new(0));
173 let calls = validator_calls.clone();
174 let pool = Pool::new(
175 new_item,
176 Some(Box::new(move |_: &mut Arc<Item>| {
177 calls.fetch_add(1, Ordering::SeqCst);
178 true
179 })),
180 Pool::DEFAULT_MAX_SIZE,
181 );
182
183 let item = pool.rent();
184 pool.return_item(item);
185
186 assert_eq!(1, validator_calls.load(Ordering::SeqCst));
187 }
188
189 #[test]
190 fn validator_is_not_invoked_on_rent() {
191 let validator_calls = Arc::new(AtomicI32::new(0));
195 let calls = validator_calls.clone();
196 let pool = Pool::new(
197 new_item,
198 Some(Box::new(move |_: &mut Arc<Item>| {
199 calls.fetch_add(1, Ordering::SeqCst);
200 true
201 })),
202 Pool::DEFAULT_MAX_SIZE,
203 );
204
205 let _ = pool.rent(); let item = pool.rent();
207 pool.return_item(item); let _ = pool.rent(); assert_eq!(1, validator_calls.load(Ordering::SeqCst));
211 }
212
213 #[test]
214 fn return_drops_item_when_validator_returns_false() {
215 let pool = Pool::new(new_item, Some(Box::new(|_: &mut Arc<Item>| false)), Pool::DEFAULT_MAX_SIZE);
216
217 let item = pool.rent();
218 pool.return_item(item.clone());
219
220 let rented = pool.rent();
223
224 assert!(!Arc::ptr_eq(&item, &rented));
225 }
226
227 #[test]
228 fn validator_can_reset_item_state_before_pooling() {
229 let pool = Pool::new(
230 new_item,
231 Some(Box::new(|i: &mut Arc<Item>| {
232 i.state.store(0, Ordering::SeqCst);
233 true
234 })),
235 Pool::DEFAULT_MAX_SIZE,
236 );
237
238 let item = pool.rent();
239 item.state.store(42, Ordering::SeqCst);
240 pool.return_item(item.clone());
241
242 let rented = pool.rent();
243
244 assert!(Arc::ptr_eq(&item, &rented));
245 assert_eq!(0, rented.state.load(Ordering::SeqCst));
246 }
247
248 #[test]
249 fn pool_stays_within_max_size_under_concurrent_returns() {
250 const MAX_SIZE: i32 = 8;
251 const RETURNS_PER_THREAD: i32 = 100;
252 const THREAD_COUNT: i32 = 16;
253
254 let pool = Pool::new(new_item, None, MAX_SIZE);
255
256 std::thread::scope(|scope| {
257 for _ in 0..THREAD_COUNT {
258 scope.spawn(|| {
259 for _ in 0..RETURNS_PER_THREAD {
260 let item = new_item();
261 item.state.store(1, Ordering::SeqCst);
262 pool.return_item(item);
263 }
264 });
265 }
266 });
267
268 let mut pooled = 0;
271 let mut seen: Vec<Arc<Item>> = Vec::new();
272
273 for _ in 0..MAX_SIZE * 2 {
274 let item = pool.rent();
275 if contains(&seen, &item) {
276 panic!("ObjectPool returned the same instance twice without an intervening Return.");
277 }
278 seen.push(item.clone());
279
280 if item.state.load(Ordering::SeqCst) == 1 {
281 pooled += 1;
282 }
283 }
284
285 assert!(pooled <= MAX_SIZE, "Expected to observe at most {MAX_SIZE} pooled items, observed {pooled}.");
286 }
287
288 #[test]
289 fn rent_and_return_survive_parallel_use_without_losing_or_duplicating_items() {
290 const ITERATIONS: i32 = 5_000;
291 const THREAD_COUNT: i32 = 8;
292
293 let pool = Pool::new(new_item, None, 32);
294
295 std::thread::scope(|scope| {
296 for _ in 0..THREAD_COUNT {
297 scope.spawn(|| {
298 for _ in 0..ITERATIONS {
299 let item = pool.rent();
300 pool.return_item(item);
301 }
302 });
303 }
304 });
305 }
306}