From 1d1d04e3d41a06d8c6b557d2715df06e2b7c264c Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 3 Oct 2026 23:25:39 +0800 Subject: [PATCH 1/2] refactor(pool): borrow the pool during recycling UnreadyObject only lives inside Pool::get, which already borrows an Arc. Hold that borrow directly, remove the copied cancellation policy, and stop upgrading a Weak that cannot have expired. Create the returned Object's Weak in ready(), preserving independent checkout lifetimes. Cover both pool variants when a recycled object outlives its pool and is then dropped or detached. --- asyncband/src/pool/bounded.rs | 33 +++++++++----------- asyncband/src/pool/unbounded.rs | 33 +++++++++----------- tests-integration/tests/pool_test/recycle.rs | 22 +++++++++++++ 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/asyncband/src/pool/bounded.rs b/asyncband/src/pool/bounded.rs index c6309911..9b3e2fd5 100644 --- a/asyncband/src/pool/bounded.rs +++ b/asyncband/src/pool/bounded.rs @@ -293,8 +293,7 @@ impl Pool { Some(object) => { let mut unready_object = UnreadyObject { state: Some(object), - pool: Arc::downgrade(self), - recycle_cancelled_strategy: self.config.recycle_cancelled_strategy, + pool: self, }; let state = unready_object.state(); @@ -556,34 +555,32 @@ impl Object { /// If the check fails, `detach()` should be called to permanently remove the object /// from the pool. If dropped without calling either method (due to being cancelled), /// the behavior depends on the pool's [`RecycleCancelledStrategy`] configuration. -struct UnreadyObject { +struct UnreadyObject<'a, M: ManageObject> { state: Option>, - pool: Weak>, - recycle_cancelled_strategy: RecycleCancelledStrategy, + // The enclosing `get` call keeps the pool alive through cancellation cleanup. + pool: &'a Arc>, } -impl Drop for UnreadyObject { +impl Drop for UnreadyObject<'_, M> { fn drop(&mut self) { if let Some(mut state) = self.state.take() { - if let Some(pool) = self.pool.upgrade() { - match self.recycle_cancelled_strategy { - RecycleCancelledStrategy::Detach => { - pool.detach_object(&mut state.o); - } - RecycleCancelledStrategy::ReturnToPool => { - pool.restore_idle(state); - } + match self.pool.config.recycle_cancelled_strategy { + RecycleCancelledStrategy::Detach => { + self.pool.detach_object(&mut state.o); + } + RecycleCancelledStrategy::ReturnToPool => { + self.pool.restore_idle(state); } } } } } -impl UnreadyObject { +impl UnreadyObject<'_, M> { fn ready(mut self, permit: OwnedSemaphorePermit) -> Object { // INVARIANT: `state` is `Some` until this object becomes ready, detaches, or is dropped. let state = Some(self.state.take().unwrap()); - let pool = self.pool.clone(); + let pool = Arc::downgrade(self.pool); Object { state, permit, @@ -593,9 +590,7 @@ impl UnreadyObject { fn detach(&mut self) { if let Some(mut state) = self.state.take() { - if let Some(pool) = self.pool.upgrade() { - pool.detach_object(&mut state.o); - } + self.pool.detach_object(&mut state.o); } } diff --git a/asyncband/src/pool/unbounded.rs b/asyncband/src/pool/unbounded.rs index ac0baffb..44deef30 100644 --- a/asyncband/src/pool/unbounded.rs +++ b/asyncband/src/pool/unbounded.rs @@ -342,8 +342,7 @@ impl> Pool { Some(object) => { let mut unready_object = UnreadyObject { state: Some(object), - pool: Arc::downgrade(self), - recycle_cancelled_strategy: self.config.recycle_cancelled_strategy, + pool: self, }; let state = unready_object.state(); @@ -571,42 +570,38 @@ impl> Object { /// If the check fails, `detach()` should be called to permanently remove the object /// from the pool. If dropped without calling either method (due to being cancelled), /// the behavior depends on the pool's [`RecycleCancelledStrategy`] configuration. -struct UnreadyObject> { +struct UnreadyObject<'a, T, M: ManageObject> { state: Option>, - pool: Weak>, - recycle_cancelled_strategy: RecycleCancelledStrategy, + // The enclosing `get` call keeps the pool alive through cancellation cleanup. + pool: &'a Arc>, } -impl> Drop for UnreadyObject { +impl> Drop for UnreadyObject<'_, T, M> { fn drop(&mut self) { if let Some(mut state) = self.state.take() { - if let Some(pool) = self.pool.upgrade() { - match self.recycle_cancelled_strategy { - RecycleCancelledStrategy::Detach => { - pool.detach_object(&mut state.o); - } - RecycleCancelledStrategy::ReturnToPool => { - pool.restore_idle(state); - } + match self.pool.config.recycle_cancelled_strategy { + RecycleCancelledStrategy::Detach => { + self.pool.detach_object(&mut state.o); + } + RecycleCancelledStrategy::ReturnToPool => { + self.pool.restore_idle(state); } } } } } -impl> UnreadyObject { +impl> UnreadyObject<'_, T, M> { fn ready(mut self) -> Object { // INVARIANT: `state` is `Some` until this object becomes ready, detaches, or is dropped. let state = Some(self.state.take().unwrap()); - let pool = self.pool.clone(); + let pool = Arc::downgrade(self.pool); Object { state, pool } } fn detach(&mut self) { if let Some(mut state) = self.state.take() { - if let Some(pool) = self.pool.upgrade() { - pool.detach_object(&mut state.o); - } + self.pool.detach_object(&mut state.o); } } diff --git a/tests-integration/tests/pool_test/recycle.rs b/tests-integration/tests/pool_test/recycle.rs index 03ff53ef..d877cc61 100644 --- a/tests-integration/tests/pool_test/recycle.rs +++ b/tests-integration/tests/pool_test/recycle.rs @@ -113,6 +113,28 @@ macro_rules! recycle_contract { } } + #[test] + fn recycled_object_can_outlive_the_pool() { + for detach in [false, true] { + let manager = Manager::default(); + let pool = pool(1, manager.clone(), None); + drop(ready(pool.get()).unwrap()); + let object = ready(pool.get()).unwrap(); + assert_eq!(object.status().recycle_count(), 1); + + let weak_pool = Arc::downgrade(&pool); + drop(pool); + assert!(weak_pool.upgrade().is_none()); + assert_eq!(*object, 0); + if detach { + assert_eq!(object.detach(), 0); + } else { + drop(object); + } + assert!(manager.detached().is_empty()); + } + } + #[test] fn cancelling_one_validation_leaves_other_idle_objects_available() { let manager = Manager::default(); From 85becda69df53e649b1ff598dfaa74e8368ab504 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 3 Oct 2026 23:37:12 +0800 Subject: [PATCH 2/2] refactor(pool): derive the recycling guard object type from its manager UnreadyObject is private and its object type is fixed by ManageObject. Use M::Object directly instead of carrying T and repeating Object = T bounds across the guard and its implementations. --- asyncband/src/pool/unbounded.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/asyncband/src/pool/unbounded.rs b/asyncband/src/pool/unbounded.rs index 44deef30..6f7337ce 100644 --- a/asyncband/src/pool/unbounded.rs +++ b/asyncband/src/pool/unbounded.rs @@ -570,13 +570,13 @@ impl> Object { /// If the check fails, `detach()` should be called to permanently remove the object /// from the pool. If dropped without calling either method (due to being cancelled), /// the behavior depends on the pool's [`RecycleCancelledStrategy`] configuration. -struct UnreadyObject<'a, T, M: ManageObject> { - state: Option>, +struct UnreadyObject<'a, M: ManageObject> { + state: Option>, // The enclosing `get` call keeps the pool alive through cancellation cleanup. - pool: &'a Arc>, + pool: &'a Arc>, } -impl> Drop for UnreadyObject<'_, T, M> { +impl Drop for UnreadyObject<'_, M> { fn drop(&mut self) { if let Some(mut state) = self.state.take() { match self.pool.config.recycle_cancelled_strategy { @@ -591,8 +591,8 @@ impl> Drop for UnreadyObject<'_, T, M> { } } -impl> UnreadyObject<'_, T, M> { - fn ready(mut self) -> Object { +impl UnreadyObject<'_, M> { + fn ready(mut self) -> Object { // INVARIANT: `state` is `Some` until this object becomes ready, detaches, or is dropped. let state = Some(self.state.take().unwrap()); let pool = Arc::downgrade(self.pool); @@ -605,7 +605,7 @@ impl> UnreadyObject<'_, T, M> { } } - fn state(&mut self) -> &mut ObjectState { + fn state(&mut self) -> &mut ObjectState { // INVARIANT: `state` is `Some` until this object becomes ready, detaches, or is dropped. self.state.as_mut().unwrap() }