Skip to main content

ferroui_base/utilities/
object_pool.rs

1use std::sync::atomic::{AtomicI32, Ordering};
2use std::sync::{Mutex, PoisonError};
3
4/// Provides a thread-safe object pool with size limits and object validation.
5pub 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    /// The default of the maximum number of objects to keep in the pool.
15    pub const DEFAULT_MAX_SIZE: i32 = 32;
16
17    /// Initializes a new instance of the pool.
18    ///
19    /// `factory` creates new instances. The optional `validator` cleans and
20    /// validates an object before it returns to the pool; it returns false
21    /// to discard the object. `max_size` is the maximum number of objects to
22    /// keep in the pool.
23    ///
24    /// Panics when `max_size` is less than 1.
25    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    /// Rents an object from the pool or creates a new one if the pool is empty.
44    #[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    /// Returns an object to the pool if it passes validation and the pool is not full.
56    ///
57    /// Callers must not return the same item more than once without an
58    /// intervening [`rent`](Self::rent). The pool does not detect duplicate
59    /// returns. The pool also does nothing with the items it drops (when
60    /// full or when the validator returns false) beyond dropping them; any
61    /// cleanup belongs in the validator or in the caller.
62    #[inline]
63    pub fn return_item(&self, mut item: T) {
64        // Validate and clean the object
65        if let Some(validator) = &self.validator {
66            if !validator(&mut item) {
67                return;
68            }
69        }
70
71        // Check if pool is full (fast check without lock)
72        if self.count.load(Ordering::SeqCst) >= self.max_size {
73            return;
74        }
75
76        // Try to increment count, but check again in case of race condition
77        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            // Pool is full, decrement and discard
83            self.count.fetch_sub(1, Ordering::SeqCst);
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    // The upstream tests `Constructor_Throws_When_Factory_Is_Null` and
91    // `Return_Ignores_Null` have no counterpart: a factory and an item
92    // cannot be null here.
93    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()); // pool full -> dropped
161
162        let rented = [pool.rent(), pool.rent()];
163
164        // The two rented items must come from {a, b}; c was dropped.
165        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        // The validator's job is to prepare an item for re-use *before* it goes back
192        // into the pool. Running it on Rent would either duplicate the work or imply
193        // a different contract (validate-on-take). Pin the current contract.
194        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(); // fresh from factory; validator must not run
206        let item = pool.rent();
207        pool.return_item(item); // one validator call here
208        let _ = pool.rent(); // pulled from pool; validator must not run again
209
210        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        // Validator rejected the item, so the pool is empty and the next
221        // Rent produces a fresh instance.
222        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        // Drain the pool. Items that came from the pool will still have State==1;
269        // factory-created items will have the default State==0.
270        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}