Skip to main content

compio_pool/
lib.rs

1//! A bb8-style connection pool for [`compio`] on Linux `io_uring`.
2//!
3//! You build one [`Pool`] — the manager plus a handful of [`Builder`] knobs —
4//! then clone that handle onto each thread of your own thread-per-core runtime.
5//! On every thread you call [`Pool::local`] once to get that thread's
6//! [`LocalPool`], and [`LocalPool::get`] to lease a connection. The pool is the
7//! product; spawning threads, pinning them and running an accept loop are yours.
8//!
9//! # Why it is shaped this way
10//!
11//! A `compio` connection is bound to the `io_uring` ring of the thread that
12//! opened it: its buffers are registered with that ring and its completions are
13//! delivered there. It cannot be used from another thread. So, unlike bb8 on
14//! `tokio`, the pool of connections cannot be a single shared, work-stealing
15//! store. What *is* shared is only the blueprint:
16//!
17//! * **[`Pool`] — `Send + Sync + Clone`.** The manager and the configuration,
18//!   behind one `Arc`. Cloning is an `Arc` bump. This is the only thing that
19//!   crosses threads.
20//! * **[`LocalPool`] — `!Send`, one per thread.** The idle connections and the
21//!   slot count, all `Rc`/`Cell`, no lock, no atom. The compiler keeps it, and
22//!   the ring-bound connections it holds, on its own thread.
23//!
24//! The parts of bb8 that make sense per thread are here — [`max_size`], idle and
25//! lifetime timeouts, `test_on_check_out`, [`min_idle`] warm-up — sized per
26//! thread, not per process.
27//!
28//! [`max_size`]: Builder::max_size
29//! [`min_idle`]: Builder::min_idle
30//!
31//! # Running it in production
32//!
33//! Three things a long-lived server needs are built on the same split:
34//!
35//! * **Metrics.** Each [`LocalPool`] keeps its own counters and registers them
36//!   with the [`Pool`]. [`LocalPool::state`] / [`LocalPool::statistics`] read one
37//!   thread's lock-free; [`Pool::state`] / [`Pool::statistics`] sum every thread's
38//!   for a process-wide [`State`] and [`Statistics`] to feed a metrics endpoint.
39//! * **Maintenance.** Expiry is lazy, so a quiet pool would hold dead backend
40//!   connections open. Call [`LocalPool::maintain`] on a timer on each worker — it
41//!   reaps expired idle connections ([`LocalPool::reap`]) and tops back up to
42//!   [`min_idle`] — since thread-per-core has no shared reaper thread.
43//! * **Shutdown.** [`Pool::close`] makes every thread's [`get`](LocalPool::get)
44//!   fail with [`RunError::Closed`] and stops recycling; [`LocalPool::clear`]
45//!   drains a thread's idle connections for a prompt drain.
46//!
47//! # Quickstart
48//!
49//! Define a manager, build the pool, and use it from a compio task:
50//!
51//! ```no_run
52//! use std::io;
53//!
54//! use compio_pool::{ManageConnection, Pool};
55//!
56//! /// A pool of reusable 16 KiB scratch buffers. A real manager would open a
57//! /// backend connection (Redis, Postgres, an upstream socket) in `connect`.
58//! struct Buffers;
59//!
60//! impl ManageConnection for Buffers {
61//!     type Connection = Vec<u8>;
62//!     type Error = io::Error;
63//!
64//!     async fn connect(&self) -> io::Result<Vec<u8>> {
65//!         Ok(Vec::with_capacity(16 * 1024))
66//!     }
67//!     async fn is_valid(&self, _buf: &mut Vec<u8>) -> io::Result<()> {
68//!         Ok(())
69//!     }
70//!     fn has_broken(&self, _buf: &mut Vec<u8>) -> bool {
71//!         false
72//!     }
73//! }
74//!
75//! // Build once; `Pool` is `Send + Clone`, so hand a clone to each thread.
76//! let pool = Pool::builder().max_size(1024).build(Buffers);
77//!
78//! // On each compio thread you spawn (see `examples/echo.rs` for the full
79//! // pinned, SO_REUSEPORT thread-per-core setup):
80//! let local = pool.local(); // this thread's own pool, `!Send`
81//! # async fn run(
82//! #     local: compio_pool::LocalPool<Buffers>,
83//! # ) -> Result<(), compio_pool::RunError<io::Error>> {
84//! let mut buf = local.get().await?; // PooledConnection, returned on drop
85//! buf.clear();
86//! # Ok(())
87//! # }
88//! ```
89//!
90//! The crate also ships the two helpers that building your own thread-per-core
91//! loop needs: [`cpu`] for core enumeration and pinning, and
92//! [`listener::bind_reuseport`] for a shared-address `SO_REUSEPORT` socket.
93//!
94//! # Platform
95//!
96//! Linux is the only supported target: `SO_REUSEPORT` load balancing and
97//! `io_uring` are Linux semantics. The crate type-checks on other Unixes so
98//! editors work, but nothing is tested there.
99
100#![cfg_attr(docsrs, feature(doc_cfg))]
101#![forbid(unsafe_code)]
102#![warn(missing_docs, rust_2018_idioms)]
103
104#[cfg(not(unix))]
105compile_error!(
106    "compio-pool needs SO_REUSEPORT and an fd-based socket model; it supports Linux and only type-checks on other Unixes"
107);
108
109pub mod builder;
110pub mod cpu;
111pub mod listener;
112pub mod manage;
113pub mod pool;
114
115pub use builder::Builder;
116pub use cpu::CoreId;
117pub use listener::bind_reuseport;
118pub use manage::{ManageConnection, RunError};
119pub use pool::{LocalPool, Pool, PooledConnection, State, Statistics};