feat: update library lifecycle return value

This commit is contained in:
2026-08-10 22:17:19 +08:00
parent 65e825f8db
commit 44adb10989
2 changed files with 61 additions and 42 deletions
+15 -10
View File
@@ -82,15 +82,16 @@ impl<S> LibraryLifecycle<S> {
/// Increment the reference count. /// Increment the reference count.
/// ///
/// On the `0 -> 1` transition the supplied `init` closure runs and its produced `S` is stored. /// On the `0 -> 1` transition the supplied `init` closure runs and its produced `S` is stored;
/// If `init` fails, neither the count nor the state is changed and an error is returned. /// the call returns `Ok(true)`. If `init` fails, neither the count nor the state is changed and
/// an error is returned.
/// ///
/// On any other call (count already `> 0`) the supplied `init` is **dropped without being run** /// On any other call (count already `> 0`) the supplied `init` is **dropped without being run**,
/// and the count is simply incremented. /// the count is simply incremented, and the call returns `Ok(false)`.
/// ///
/// The init closure runs under the write lock and must not re-enter this lifecycle. Its error /// The init closure runs under the write lock and must not re-enter this lifecycle. Its error
/// type `E` is type-erased. /// type `E` is type-erased.
pub fn startup<E>(&self, init: impl FnOnce() -> Result<S, E>) -> Result<(), Error> pub fn startup<E>(&self, init: impl FnOnce() -> Result<S, E>) -> Result<bool, Error>
where where
E: std::error::Error + Send + Sync + 'static, E: std::error::Error + Send + Sync + 'static,
{ {
@@ -99,22 +100,24 @@ impl<S> LibraryLifecycle<S> {
let s = init().map_err(|e| Error::Init(Box::new(e)))?; let s = init().map_err(|e| Error::Init(Box::new(e)))?;
inner.state = Some(s); inner.state = Some(s);
inner.count = 1; inner.count = 1;
Ok(true)
} else { } else {
inner.count += 1; inner.count += 1;
Ok(false)
} }
Ok(())
} }
/// Decrement the reference count. /// Decrement the reference count.
/// ///
/// On the `-> 0` transition the stored `S` is taken out and handed to the supplied `destroy` /// On the `-> 0` transition the stored `S` is taken out and handed to the supplied `destroy`
/// closure, which must never fail (destructor semantics). On any other call (count stays `> 0`) /// closure, which must never fail (destructor semantics), and the call returns `Ok(true)`. On
/// the supplied `destroy` is **dropped without being run** and the count is simply decremented. /// any other call (count stays `> 0`) the supplied `destroy` is **dropped without being run**,
/// the count is simply decremented, and the call returns `Ok(false)`.
/// ///
/// Returns an error if the count was already zero. /// Returns an error if the count was already zero.
/// ///
/// The destroy closure runs under the write lock and must not re-enter this lifecycle. /// The destroy closure runs under the write lock and must not re-enter this lifecycle.
pub fn shutdown(&self, destroy: impl FnOnce(S)) -> Result<(), Error> { pub fn shutdown(&self, destroy: impl FnOnce(S)) -> Result<bool, Error> {
let mut inner = self.inner.write().expect("unexpected poison lock"); let mut inner = self.inner.write().expect("unexpected poison lock");
if inner.count == 0 { if inner.count == 0 {
return Err(Error::Underflow); return Err(Error::Underflow);
@@ -124,8 +127,10 @@ impl<S> LibraryLifecycle<S> {
// Invariant: state is Some whenever count > 0. // Invariant: state is Some whenever count > 0.
let s = inner.state.take().expect("state present while count > 0"); let s = inner.state.take().expect("state present while count > 0");
destroy(s); destroy(s);
Ok(true)
} else {
Ok(false)
} }
Ok(())
} }
/// Access the stored state by shared reference under a read lock. /// Access the stored state by shared reference under a read lock.
+26 -12
View File
@@ -16,24 +16,28 @@ struct State {
} }
/// Coverage: a single paired startup/shutdown runs init once and destroy once, destroy receives the /// Coverage: a single paired startup/shutdown runs init once and destroy once, destroy receives the
/// state produced by init, and the count returns to zero. /// state produced by init, both report that they ran the closure, and the count returns to zero.
#[test] #[test]
fn paired_startup_shutdown() { fn paired_startup_shutdown() {
let inits = AtomicUsize::new(0); let inits = AtomicUsize::new(0);
let destroys = AtomicUsize::new(0); let destroys = AtomicUsize::new(0);
let lc: LibraryLifecycle<State> = LibraryLifecycle::new(); let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
lc.startup(|| -> Result<State, TestErr> { let ran_init = lc
.startup(|| -> Result<State, TestErr> {
inits.fetch_add(1, Ordering::SeqCst); inits.fetch_add(1, Ordering::SeqCst);
Ok(State { value: 42 }) Ok(State { value: 42 })
}) })
.unwrap(); .unwrap();
assert!(ran_init);
lc.shutdown(|s| { let ran_destroy = lc
.shutdown(|s| {
destroys.fetch_add(1, Ordering::SeqCst); destroys.fetch_add(1, Ordering::SeqCst);
assert_eq!(s.value, 42); assert_eq!(s.value, 42);
}) })
.unwrap(); .unwrap();
assert!(ran_destroy);
assert_eq!(inits.load(Ordering::SeqCst), 1); assert_eq!(inits.load(Ordering::SeqCst), 1);
assert_eq!(destroys.load(Ordering::SeqCst), 1); assert_eq!(destroys.load(Ordering::SeqCst), 1);
@@ -42,29 +46,35 @@ fn paired_startup_shutdown() {
/// Coverage: multiple startups followed by matching shutdowns run init and destroy only on the /// Coverage: multiple startups followed by matching shutdowns run init and destroy only on the
/// boundary transitions (once each); the init/destroy closures passed on non-transition calls are /// boundary transitions (once each); the init/destroy closures passed on non-transition calls are
/// dropped without being run. /// dropped without being run, and the bool return reflects exactly which calls ran a closure.
#[test] #[test]
fn multiple_startups_shutdowns_run_only_on_boundaries() { fn multiple_startups_shutdowns_run_only_on_boundaries() {
let inits = AtomicUsize::new(0); let inits = AtomicUsize::new(0);
let destroys = AtomicUsize::new(0); let destroys = AtomicUsize::new(0);
let lc: LibraryLifecycle<State> = LibraryLifecycle::new(); let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
for _ in 0..3 { let startup_ran: Vec<bool> = (0..3)
.map(|_| {
lc.startup(|| -> Result<State, TestErr> { lc.startup(|| -> Result<State, TestErr> {
inits.fetch_add(1, Ordering::SeqCst); inits.fetch_add(1, Ordering::SeqCst);
Ok(State { value: 7 }) Ok(State { value: 7 })
}) })
.unwrap(); .unwrap()
} })
.collect();
assert_eq!(startup_ran, vec![true, false, false]);
assert_eq!(lc.count(), 3); assert_eq!(lc.count(), 3);
assert_eq!(inits.load(Ordering::SeqCst), 1); assert_eq!(inits.load(Ordering::SeqCst), 1);
for _ in 0..3 { let shutdown_ran: Vec<bool> = (0..3)
.map(|_| {
lc.shutdown(|_s| { lc.shutdown(|_s| {
destroys.fetch_add(1, Ordering::SeqCst); destroys.fetch_add(1, Ordering::SeqCst);
}) })
.unwrap(); .unwrap()
} })
.collect();
assert_eq!(shutdown_ran, vec![false, false, true]);
assert_eq!(inits.load(Ordering::SeqCst), 1); assert_eq!(inits.load(Ordering::SeqCst), 1);
assert_eq!(destroys.load(Ordering::SeqCst), 1); assert_eq!(destroys.load(Ordering::SeqCst), 1);
@@ -79,15 +89,19 @@ fn reinitialization_runs_init_and_destroy_again() {
let lc: LibraryLifecycle<State> = LibraryLifecycle::new(); let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
for _ in 0..2 { for _ in 0..2 {
lc.startup(|| -> Result<State, TestErr> { let ran_init = lc
.startup(|| -> Result<State, TestErr> {
inits.fetch_add(1, Ordering::SeqCst); inits.fetch_add(1, Ordering::SeqCst);
Ok(State { value: 1 }) Ok(State { value: 1 })
}) })
.unwrap(); .unwrap();
lc.shutdown(|_s| { assert!(ran_init);
let ran_destroy = lc
.shutdown(|_s| {
destroys.fetch_add(1, Ordering::SeqCst); destroys.fetch_add(1, Ordering::SeqCst);
}) })
.unwrap(); .unwrap();
assert!(ran_destroy);
} }
assert_eq!(inits.load(Ordering::SeqCst), 2); assert_eq!(inits.load(Ordering::SeqCst), 2);