Skip to main content

compio_pool/
lib.rs

1//! A connection pool for [`compio`], the completion-based, thread-per-core
2//! Rust runtime.
3//!
4//! # Why compio needs its own pool
5//!
6//! General-purpose pools (`bb8`, `deadpool`, `r2d2`) assume connections are
7//! `Send`: one shared `Mutex<Vec<Conn>>`, any worker takes any connection.
8//! `compio` breaks that assumption. Each thread runs its own driver
9//! (io_uring, IOCP or poll), IO handles are bound to the driver that created
10//! them, and buffers are handed to the kernel by ownership — so
11//! [`compio::net::TcpStream`] is `!Send` and cannot be put behind an `Arc<Mutex<_>>`
12//! at all.
13//!
14//! So "share one connection across threads simultaneously" is not the thing to
15//! build. Two different things hide behind that phrase:
16//!
17//! * **Concurrent use of a single connection** — only meaningful for protocols
18//!   that multiplex (HTTP/2, Redis pipelining). Otherwise a connection is an
19//!   exclusive resource and sharing means taking turns.
20//! * **A pool shared by every thread** — any thread can check out *some*
21//!   connection. That is what this crate provides.
22//!
23//! # Design
24//!
25//! [`Pool`] is `Send + Sync` and cheap to clone, but holds no connections. The
26//! connections live in a **per-thread shard** stored in a thread-local, so:
27//!
28//! * the acquire fast path touches no atomics and takes no lock;
29//! * [`Manage::Connection`] never needs to be `Send`;
30//! * a connection is always driven by the compio driver that created it;
31//! * at thread exit, that thread's connections close on their own thread.
32//!
33//! [`Config::max_size`] is therefore **per shard**, not per process. A global
34//! cap would need a cross-thread semaphore on the hot path, which is the exact
35//! contention thread-per-core exists to avoid.
36//!
37//! ## Letting connections migrate
38//!
39//! Pure sharding wastes connections when load is skewed: a quiet thread holds
40//! idle sockets a busy thread could use. The optional [`Reservoir`] fixes that
41//! with a bounded, lock-free `ArrayQueue` shared by every thread: a shard whose
42//! free list is over `min_idle` detaches the surplus into it, and a shard whose
43//! free list is empty pops from it before paying for a handshake. The claiming
44//! thread re-wraps the socket in its own runtime via [`Detach::attach`].
45//!
46//! It requires [`Detach`], which is **platform-dependent and deliberately
47//! opt-in**:
48//!
49//! | driver | `attach` | connections may change threads |
50//! |---|---|---|
51//! | io_uring (Linux) | no-op | yes |
52//! | poll (Unix fallback) | no-op | yes |
53//! | IOCP (Windows) | `CreateIoCompletionPort`, once per handle | **no** |
54//!
55//! ## Cancellation is the sharp edge
56//!
57//! With completion-based IO, dropping a future mid-operation does not undo the
58//! operation. The connection's protocol state is then unknown and it must be
59//! destroyed rather than returned. See [`Pooled::begin_op`].
60//!
61//! # Example
62//!
63//! ```no_run
64//! use std::time::Duration;
65//! use compio_pool::{Config, Manage, Pool, SlotMeta};
66//!
67//! struct Tcp(String);
68//!
69//! impl Manage for Tcp {
70//!     type Connection = compio::net::TcpStream;
71//!     type Error = std::io::Error;
72//!
73//!     async fn connect(&self) -> std::io::Result<Self::Connection> {
74//!         compio::net::TcpStream::connect(&self.0).await
75//!     }
76//!
77//!     async fn recycle(
78//!         &self,
79//!         conn: &mut Self::Connection,
80//!         _meta: &SlotMeta,
81//!     ) -> std::io::Result<()> {
82//!         conn.peer_addr().map(|_| ())
83//!     }
84//! }
85//!
86//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
87//! let pool = Pool::builder(Tcp("127.0.0.1:6379".into()))
88//!     .max_size(8)                                   // per thread
89//!     .min_idle(2)
90//!     .acquire_timeout(Duration::from_secs(5))
91//!     .build();
92//!
93//! // Clone `pool` onto every compio thread; each gets its own shard.
94//! let mut conn = pool.acquire().await?;
95//!
96//! let op = conn.begin_op();
97//! // ... use `conn` ...
98//! op.complete_op();
99//! # Ok(())
100//! # }
101//! ```
102
103#![warn(missing_docs)]
104
105mod config;
106mod error;
107mod exchange;
108mod guard;
109mod manage;
110mod metrics;
111mod pool;
112mod shard;
113mod slot;
114
115#[cfg(test)]
116mod test_support;
117
118pub use crate::{
119    config::Config,
120    error::Error,
121    exchange::{Exchange, NoExchange, Parked, Reservoir, Unparked},
122    guard::{OpGuard, Pooled},
123    manage::{Detach, Manage},
124    metrics::Metrics,
125    pool::{Builder, Pool},
126    slot::SlotMeta,
127};