Skip to content

Commit 48e746f

Browse files
authored
fix(pool): preserve capacity across replenishment races (#181)
1 parent c67c237 commit 48e746f

6 files changed

Lines changed: 186 additions & 51 deletions

File tree

asyncband/src/pool/bounded.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,39 @@ impl<M: ManageObject> Pool<M> {
190190
///
191191
/// The pool reserves only capacity that is immediately available and never waits for
192192
/// checked-out objects. Existing idle objects count toward the target, and the pool's
193-
/// maximum size is never exceeded. Concurrent calls and checkouts can change the observed
194-
/// idle count while this method is running, so the target is best effort rather than a
195-
/// postcondition.
193+
/// maximum size is never exceeded. Targets above the maximum size are treated as the maximum.
194+
/// Concurrent calls and checkouts can change the observed idle count while this method is
195+
/// running, so the target is best effort rather than a postcondition.
196196
///
197197
/// Returns the number of objects created. If [`ManageObject::create`] fails, objects created by
198198
/// this call before the failure remain in the pool and the error is returned.
199199
pub async fn replenish_to(&self, target_idle: usize) -> Result<usize, M::Error> {
200-
let Some(mut permit) = self.permits.clone().try_acquire_up_to_owned(target_idle) else {
200+
let target_idle = target_idle.min(self.config.max_size);
201+
let Some(mut reservation) = ReplenishReservation::reserve_up_to(&self.permits, target_idle)
202+
else {
201203
return Ok(0);
202204
};
203205

204-
let idle_count = self.slots.lock().idle_count();
205-
let to_create = target_idle.saturating_sub(idle_count).min(permit.permits());
206-
permit.release(permit.permits() - to_create);
206+
let (idle_count, available_slots) = {
207+
let slots = self.slots.lock();
208+
let idle_count = slots.idle_count();
209+
210+
// Idle objects occupy pool slots without holding permits. Available permits plus this
211+
// reservation represent capacity not committed to other checkouts, creations, or
212+
// replenishments; subtracting idle objects leaves the slots this call may create.
213+
let uncommitted_capacity = self
214+
.permits
215+
.available_permits()
216+
.checked_add(reservation.permits())
217+
.expect("invariant broken: semaphore capacity must not overflow");
218+
let available_slots = uncommitted_capacity.saturating_sub(idle_count);
219+
(idle_count, available_slots)
220+
};
221+
let to_create = target_idle
222+
.saturating_sub(idle_count)
223+
.min(reservation.permits())
224+
.min(available_slots);
225+
reservation.release(reservation.permits() - to_create);
207226

208227
let mut replenished = 0;
209228
for _ in 0..to_create {
@@ -213,7 +232,7 @@ impl<M: ManageObject> Pool<M> {
213232
slots.add_idle(ObjectState::new(object));
214233
}
215234
replenished += 1;
216-
permit.release(1);
235+
reservation.release(1);
217236
}
218237

219238
Ok(replenished)
@@ -354,6 +373,40 @@ impl<M: ManageObject> Pool<M> {
354373
}
355374
}
356375

376+
// Temporarily removes capacity while `replenish_to` creates objects. Idle objects do not consume
377+
// semaphore permits, so successful insertions release their reservation. Dropping the guard
378+
// restores any unfinished capacity after an error or cancellation.
379+
struct ReplenishReservation<'a> {
380+
semaphore: &'a Semaphore,
381+
permits: usize,
382+
}
383+
384+
impl<'a> ReplenishReservation<'a> {
385+
fn reserve_up_to(semaphore: &'a Semaphore, up_to: usize) -> Option<Self> {
386+
let permits = semaphore.drain_permits(up_to);
387+
(permits != 0).then_some(Self { semaphore, permits })
388+
}
389+
390+
fn permits(&self) -> usize {
391+
self.permits
392+
}
393+
394+
fn release(&mut self, permits: usize) {
395+
assert!(
396+
permits <= self.permits,
397+
"cannot release more permits than this reservation holds"
398+
);
399+
self.permits -= permits;
400+
self.semaphore.release(permits);
401+
}
402+
}
403+
404+
impl Drop for ReplenishReservation<'_> {
405+
fn drop(&mut self) {
406+
self.semaphore.release(self.permits);
407+
}
408+
}
409+
357410
/// A wrapper of the actual pooled object.
358411
///
359412
/// This object implements [`Deref`] and [`DerefMut`]. You can use it as if it was of type

asyncband/src/pool/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub trait ManageObject: Send + Sync {
105105
fn on_detached(&self, _o: &mut Self::Object) {}
106106
}
107107

108-
/// Queue strategy when dequeuing objects from the object pool.
108+
/// Strategy for dequeuing objects from the object pool.
109109
#[derive(Debug, Default, Clone, Copy)]
110110
pub enum QueueStrategy {
111111
/// First in first out.

asyncband/src/pool/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,14 @@
171171
//! }
172172
//! ```
173173
174-
pub use common::ManageObject;
175-
pub use common::ObjectStatus;
176-
pub use common::QueueStrategy;
177-
pub use common::RecycleCancelledStrategy;
178-
pub use common::RetainResult;
179-
180174
mod common;
181175
mod state;
182176

183177
pub mod bounded;
184178
pub mod unbounded;
179+
180+
pub use self::common::ManageObject;
181+
pub use self::common::ObjectStatus;
182+
pub use self::common::QueueStrategy;
183+
pub use self::common::RecycleCancelledStrategy;
184+
pub use self::common::RetainResult;

asyncband/src/pool/state.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ use crate::pool::QueueStrategy;
2222
use crate::pool::RetainResult;
2323

2424
#[derive(Debug)]
25-
pub(crate) struct ObjectState<T> {
26-
pub(crate) o: T,
27-
pub(crate) status: ObjectStatus,
25+
pub struct ObjectState<T> {
26+
pub o: T,
27+
pub status: ObjectStatus,
2828
}
2929

3030
impl<T> ObjectState<T> {
31-
pub(crate) fn new(o: T) -> Self {
31+
pub fn new(o: T) -> Self {
3232
Self {
3333
o,
3434
status: ObjectStatus::default(),
@@ -37,59 +37,56 @@ impl<T> ObjectState<T> {
3737
}
3838

3939
#[derive(Debug)]
40-
pub(crate) struct PoolState<T> {
40+
pub struct PoolState<T> {
4141
idle: VecDeque<ObjectState<T>>,
4242
current_size: usize,
4343
}
4444

4545
impl<T> PoolState<T> {
46-
pub(crate) const fn new() -> Self {
46+
pub const fn new() -> Self {
4747
Self {
4848
idle: VecDeque::new(),
4949
current_size: 0,
5050
}
5151
}
5252

53-
pub(crate) fn current_size(&self) -> usize {
53+
pub fn current_size(&self) -> usize {
5454
self.current_size
5555
}
5656

57-
pub(crate) fn idle_count(&self) -> usize {
57+
pub fn idle_count(&self) -> usize {
5858
self.idle.len()
5959
}
6060

61-
pub(crate) fn pop(&mut self, strategy: QueueStrategy) -> Option<ObjectState<T>> {
61+
pub fn pop(&mut self, strategy: QueueStrategy) -> Option<ObjectState<T>> {
6262
match strategy {
6363
QueueStrategy::Fifo => self.idle.pop_front(),
6464
QueueStrategy::Lifo => self.idle.pop_back(),
6565
}
6666
}
6767

68-
pub(crate) fn add_idle(&mut self, state: ObjectState<T>) {
68+
pub fn add_idle(&mut self, state: ObjectState<T>) {
6969
self.current_size += 1;
7070
self.idle.push_back(state);
7171
}
7272

73-
pub(crate) fn add_active(&mut self) {
73+
pub fn add_active(&mut self) {
7474
self.current_size += 1;
7575
}
7676

77-
pub(crate) fn return_idle(&mut self, state: ObjectState<T>) {
77+
pub fn return_idle(&mut self, state: ObjectState<T>) {
7878
self.idle.push_back(state);
7979
}
8080

81-
pub(crate) fn detach(&mut self) {
81+
pub fn detach(&mut self) {
8282
self.current_size = self
8383
.current_size
8484
.checked_sub(1)
8585
.expect("detached object must belong to the pool");
8686
}
8787

8888
/// Retains matching idle objects without losing any object if the predicate panics.
89-
pub(crate) fn retain(
90-
&mut self,
91-
mut f: impl FnMut(&mut T, ObjectStatus) -> bool,
92-
) -> RetainResult<T> {
89+
pub fn retain(&mut self, mut f: impl FnMut(&mut T, ObjectStatus) -> bool) -> RetainResult<T> {
9390
let len = self.idle.len();
9491
let mut retained = 0;
9592
let mut current = 0;

asyncband/src/semaphore/mod.rs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -310,15 +310,6 @@ impl Semaphore {
310310
}
311311
}
312312

313-
#[cfg(feature = "pool")]
314-
pub(crate) fn try_acquire_up_to_owned(
315-
self: Arc<Self>,
316-
up_to: usize,
317-
) -> Option<OwnedSemaphorePermit> {
318-
let permits = self.s.drain_permits(up_to);
319-
(permits != 0).then_some(OwnedSemaphorePermit { sem: self, permits })
320-
}
321-
322313
/// Acquires `n` permits from the semaphore.
323314
///
324315
/// The semaphore must be wrapped in an [`Arc`] to call this method.
@@ -518,16 +509,6 @@ pub struct OwnedSemaphorePermit {
518509
}
519510

520511
impl OwnedSemaphorePermit {
521-
#[cfg(feature = "pool")]
522-
pub(crate) fn release(&mut self, permits: usize) {
523-
assert!(
524-
permits <= self.permits,
525-
"cannot release more permits than this permit holds"
526-
);
527-
self.permits -= permits;
528-
self.sem.release(permits);
529-
}
530-
531512
/// Forgets the permit **without** releasing it back to the semaphore.
532513
///
533514
/// This can be used to permanently reduce the number of permits available

tests-integration/tests/pool_replenish_test.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,39 @@ impl ManageObject for ControlledManager {
162162
}
163163
}
164164

165+
#[tokio::test]
166+
async fn concurrent_replenish_to_calls_respect_capacity() {
167+
let calls = Arc::new(AtomicUsize::new(0));
168+
let allow_create = Arc::new(AtomicBool::new(false));
169+
let pool = Pool::new(
170+
PoolConfig::new(2),
171+
ControlledManager {
172+
calls: calls.clone(),
173+
allow_create: allow_create.clone(),
174+
},
175+
);
176+
177+
assert_eq!(pool.replenish_to(1).await, Ok(1));
178+
179+
let mut first = Box::pin(pool.replenish_to(2));
180+
assert!(tests_integration::poll_once(first.as_mut()).is_pending());
181+
182+
let mut second = Box::pin(pool.replenish_to(2));
183+
assert_eq!(
184+
tests_integration::poll_once(second.as_mut()),
185+
Poll::Ready(Ok(0))
186+
);
187+
188+
allow_create.store(true, Ordering::Release);
189+
assert_eq!(
190+
tests_integration::poll_once(first.as_mut()),
191+
Poll::Ready(Ok(1))
192+
);
193+
assert_eq!(calls.load(Ordering::Relaxed), 2);
194+
assert_eq!(pool.status().current_size, 2);
195+
assert_eq!(pool.status().idle_count, 2);
196+
}
197+
165198
#[tokio::test]
166199
async fn concurrent_get_and_replenish_to_respect_capacity() {
167200
let calls = Arc::new(AtomicUsize::new(0));
@@ -200,3 +233,74 @@ async fn concurrent_get_and_replenish_to_respect_capacity() {
200233
drop((first, second));
201234
assert_eq!(pool.status().idle_count, 2);
202235
}
236+
237+
struct BlockingManager {
238+
allow_create: Arc<AtomicBool>,
239+
}
240+
241+
impl ManageObject for BlockingManager {
242+
type Object = ();
243+
type Error = Infallible;
244+
245+
async fn create(&self) -> Result<Self::Object, Self::Error> {
246+
poll_fn(|_| {
247+
if self.allow_create.load(Ordering::Acquire) {
248+
Poll::Ready(())
249+
} else {
250+
Poll::Pending
251+
}
252+
})
253+
.await;
254+
Ok(())
255+
}
256+
257+
async fn is_recyclable(
258+
&self,
259+
_object: &mut Self::Object,
260+
_status: &ObjectStatus,
261+
) -> Result<(), Self::Error> {
262+
Ok(())
263+
}
264+
}
265+
266+
#[tokio::test]
267+
async fn replenish_to_respects_max_size_with_active_and_idle_objects() {
268+
let pool = Pool::new(
269+
PoolConfig::new(2),
270+
BlockingManager {
271+
allow_create: Arc::new(AtomicBool::new(true)),
272+
},
273+
);
274+
275+
assert_eq!(pool.replenish_to(2).await, Ok(2));
276+
let active = pool.get().await.unwrap();
277+
assert_eq!(pool.status().current_size, 2);
278+
assert_eq!(pool.status().idle_count, 1);
279+
280+
assert_eq!(pool.replenish_to(usize::MAX).await, Ok(0));
281+
assert_eq!(pool.status().current_size, 2);
282+
assert_eq!(pool.status().idle_count, 1);
283+
284+
drop(active);
285+
assert_eq!(pool.status().idle_count, 2);
286+
}
287+
288+
#[tokio::test]
289+
async fn cancelling_replenish_to_releases_reserved_capacity() {
290+
let allow_create = Arc::new(AtomicBool::new(false));
291+
let pool = Pool::new(
292+
PoolConfig::new(1),
293+
BlockingManager {
294+
allow_create: allow_create.clone(),
295+
},
296+
);
297+
298+
let mut replenish = Box::pin(pool.replenish_to(1));
299+
assert!(tests_integration::poll_once(replenish.as_mut()).is_pending());
300+
drop(replenish);
301+
302+
allow_create.store(true, Ordering::Release);
303+
let mut get = Box::pin(pool.get());
304+
assert!(tests_integration::poll_once(get.as_mut()).is_ready());
305+
assert_eq!(pool.status().idle_count, 1);
306+
}

0 commit comments

Comments
 (0)