Skip to main content

base_pool/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Resource admission and lifetime, independent of resource construction and I/O.
5//!
6//! A Rust adaptation of Socketry's `async-pool`, with separate singleplex and multiplex contracts. The initial API is experimental. Pools manage resource admission, construction reservations and retirement. They spawn no tasks and retain no scheduler instance of their own; resources may retain bound I/O capabilities. No HTTP-specific behavior or concrete runtime is a production dependency.
7//!
8//! # Contracts
9//!
10//! | Contract | [`SingleplexPool<R>`] | [`MultiplexPool<R>`] |
11//! | --- | --- | --- |
12//! | Acquisition | Exclusive [`SingleplexLease<R>`] with mutable resource access. | Shared [`MultiplexReservation<R>`], activated into one [`MultiplexUse<R>`]. |
13//! | Resource bounds | `R: Send`, including resources that are not `Sync`. | `R: Send + Sync`; users receive shared access to a resource handle. |
14//! | Completion | Consume the lease with `release` once clean and reusable. | The exchange owner consumes the active use with `complete` after protocol completion. |
15//! | Abandonment | Unfinished lease drop retires the resource. | Unopened reservation drop returns admission. Active use drop reports abandonment and retains its charge until driver cleanup. |
16//! | Availability changes | Return a clean lease or retire a registration. | Publish updated concurrent limits and optional opening credit through the registration handle. |
17//!
18//! Both implement [`Pool`], with distinct associated permit/build-permit types. Internal FIFO waiting, accounting, construction and retirement mechanisms are shared. The singleplex resource representation has no stream-use map or multiplex capacity counter.
19//!
20//! # Acquisition and construction
21//!
22//! `acquire_or_reserve` returns [`Acquisition::Existing`] or [`Acquisition::Build`]. Build permits reserve capacity **before** asynchronous construction. Register the progress owner, commit the resource, then acquire again to use it. The factory and its errors remain with the caller. A commit racing shutdown moves the resource into cleanup and returns [`Error::Closed`].
23//!
24//! ```rust
25//! use base_pool::{Acquisition, Options, SingleplexPool};
26//! use base_scheduler::Cancellation;
27//!
28//! futures_lite::future::block_on(async {
29//!     let pool = SingleplexPool::new(Options { max_resources: 1, ..Options::default() })?;
30//!     let cancellation = Cancellation::never();
31//!     let mut resource = loop {
32//!         match pool.acquire_or_reserve(&cancellation).await? {
33//!             Acquisition::Existing(lease) => break lease,
34//!             Acquisition::Build(build) => {
35//!                 // Construct the real resource and install its driver here.
36//!                 build.commit(String::from("hello"))?;
37//!             }
38//!         }
39//!     };
40//!     resource.push('!');
41//!     assert_eq!(&*resource, "hello!");
42//!     resource.release();
43//!
44//!     pool.begin_close();
45//!     while let Some(retired) = pool.next_retirement(&cancellation).await? {
46//!         // Await resource-specific cleanup here, then acknowledge it.
47//!         retired.complete();
48//!     }
49//!     pool.wait_closed(&cancellation).await?;
50//!     Ok::<(), base_pool::Error>(())
51//! })?;
52//! # Ok::<(), base_pool::Error>(())
53//! ```
54//!
55//! [`Options`] bounds building + live + retiring resources, simultaneous construction and queued waiters. Zero waiter capacity allows immediate acquisition but rejects waiting. Resource selection uses stable registration order and packs existing capacity before constructing. Waiters receive admission opportunities in FIFO order; constructing a resource is itself one opportunity and does not reserve its first use. Cancellation or dropping a pending acquisition removes its waiter without consuming capacity. IDs describe registrations, independent of resource equality, and are never recycled within the pool.
56//!
57//! # Multiplex admission
58//!
59//! Multiplex admission separates concurrent-use capacity from consumable opening credit. [`MultiplexReservation::activate`] revalidates a reservation against current limits and draining state; the protocol driver also validates actual protocol opening. Updating admission wakes waiters without requiring a release. Reducing a limit preserves active uses, and completing a use never replenishes consumable opening credit.
60//!
61//! ```rust
62//! use base_pool::{Acquisition, Admission, MultiplexPool, Options};
63//! use base_scheduler::Cancellation;
64//!
65//! futures_lite::future::block_on(async {
66//!     let pool = MultiplexPool::new(Options::default())?;
67//!     let cancellation = Cancellation::never();
68//!     let Acquisition::Build(build) = pool.acquire_or_reserve(&cancellation).await?
69//!         else { unreachable!() };
70//!     let registration = build.commit("connection handle", Admission::concurrent(2))?;
71//!     let Acquisition::Existing(reservation) = pool.acquire_or_reserve(&cancellation).await?
72//!         else { unreachable!() };
73//!     let active = reservation.activate()?;
74//!     assert_eq!(*active, "connection handle");
75//!
76//!     // Transfer `active` to the protocol exchange owner. On completion:
77//!     active.complete();
78//!     registration.retire();
79//!     pool.try_retirement().unwrap().complete();
80//!     pool.begin_close();
81//!     pool.wait_closed(&cancellation).await?;
82//!     Ok::<(), base_pool::Error>(())
83//! })?;
84//! # Ok::<(), base_pool::Error>(())
85//! ```
86//!
87//! A resource driver receives dropped active uses through [`MultiplexResource::next_abandoned`] or [`MultiplexResource::try_abandoned`]. It performs stream reset/termination and then completes the [`MultiplexAbandonedUse`]. Dropping that cleanup handle reports the abandoned use again. An application body close should notify its exchange owner; it must not release an active use while the other direction or reset is still running.
88//!
89//! # Maintenance and shutdown
90//!
91//! Maintenance is explicit. `prune(retain)` retires unused registrations; a registration's `retire` stops new uses and waits for existing owners. `try_retirement` and `next_retirement` transfer resources to an owned cleanup operation. [`Retirement::complete`] acknowledges finished cleanup and then frees resource capacity. Dropping unfinished retirement requeues it, preserving its charge. Cleanup must safely resume or tolerate retry after interruption.
92//!
93//! `begin_close` stops admission, wakes waiters and drains registrations. `wait_closed` waits for construction, active work and resource retirement acknowledgments. It does not run cleanup or force cancellation of resource users. A caller supervises maintenance alongside active work; sequentially waiting for closure before draining cleanup would not make progress. Construction owners retain build permits until abandoned construction I/O is safely retired. Dropping all owners provides ordinary destruction only, not an asynchronous cleanup guarantee.
94//!
95//! There is no built-in gardener. Retain/prune policy and cleanup belong in an explicitly driven operation, such as the client driver. A timer-driven maintenance future can later implement idle timeout or maximum lifetime using a supplied `Clock`. Without such an operation, unused resources remain until explicit pruning or shutdown.
96//!
97//! The [port notes](https://github.com/base-rs/base-pool/blob/main/design/port.md) record source provenance, the gardener decision, lifecycle changes, remaining work and validation.
98#![forbid(unsafe_code)]
99
100/// Compiles and runs the readme examples as doctests.
101#[cfg(doctest)]
102#[doc = include_str!("../readme.md")]
103pub struct ReadmeDoctests;
104
105mod core;
106mod multiplex;
107mod retirement;
108mod singleplex;
109
110pub use multiplex::{
111    Admission, MultiplexAbandonedUse, MultiplexBuildPermit, MultiplexPool, MultiplexReservation,
112    MultiplexResource, MultiplexUse, UseId,
113};
114pub use retirement::Retirement;
115pub use singleplex::{SingleplexBuildPermit, SingleplexLease, SingleplexPool, SingleplexResource};
116
117use base_scheduler::Cancellation;
118use std::{fmt, future::Future};
119
120/// Hard bounds. Retiring resources continue to occupy resource capacity.
121#[derive(Clone, Debug)]
122pub struct Options {
123    pub max_resources: usize,
124    pub max_constructing: usize,
125    pub max_waiters: usize,
126}
127impl Default for Options {
128    fn default() -> Self {
129        Self {
130            max_resources: 32,
131            max_constructing: 1,
132            max_waiters: 1024,
133        }
134    }
135}
136impl Options {
137    pub fn validate(&self) -> Result<(), Error> {
138        if self.max_resources == 0 || self.max_constructing == 0 {
139            Err(Error::InvalidOptions)
140        } else {
141            Ok(())
142        }
143    }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub enum Error {
148    Closed,
149    Cancelled,
150    WaitersFull,
151    AdmissionChanged,
152    InvalidOptions,
153}
154impl fmt::Display for Error {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        f.write_str(match self {
157            Self::Closed => "pool admission is closed",
158            Self::Cancelled => "pool operation cancelled",
159            Self::WaitersFull => "pool waiter limit reached",
160            Self::AdmissionChanged => "resource admission changed before use activation",
161            Self::InvalidOptions => "resource and construction limits must be positive",
162        })
163    }
164}
165impl std::error::Error for Error {}
166impl From<base_scheduler::Cancelled> for Error {
167    fn from(_: base_scheduler::Cancelled) -> Self {
168        Self::Cancelled
169    }
170}
171
172/// Identifies one registration within a pool; identifiers are never recycled.
173#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
174pub struct ResourceId(pub(crate) u64);
175
176/// An admission opportunity: use an existing resource or construct a new one.
177/// A build permit reserves capacity before any asynchronous construction.
178#[derive(Debug)]
179pub enum Acquisition<P, B> {
180    Existing(P),
181    Build(B),
182}
183
184/// Snapshot of one pool. `uses` includes reserved and abandoned multiplex uses.
185#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
186pub struct Stats {
187    pub building: usize,
188    pub live: usize,
189    pub retiring: usize,
190    pub draining: usize,
191    pub uses: usize,
192    pub available: usize,
193    pub waiters: usize,
194    pub closed: bool,
195}
196impl Stats {
197    pub fn total(&self) -> usize {
198        self.building + self.live + self.retiring
199    }
200}
201
202/// Shared vocabulary with distinct associated acquisition types.
203/// Resource-specific mutation and use completion stay on the concrete types.
204pub trait Pool: Send + Sync {
205    type Permit: Send;
206    type BuildPermit: Send;
207    fn acquire_or_reserve(
208        &self,
209        cancellation: &Cancellation,
210    ) -> impl Future<Output = Result<Acquisition<Self::Permit, Self::BuildPermit>, Error>> + Send;
211    fn begin_close(&self);
212    fn stats(&self) -> Stats;
213}