Expand description
A connection pool for compio, the completion-based, thread-per-core
Rust runtime.
§Why compio needs its own pool
General-purpose pools (bb8, deadpool, r2d2) assume connections are
Send: one shared Mutex<Vec<Conn>>, any worker takes any connection.
compio breaks that assumption. Each thread runs its own driver
(io_uring, IOCP or poll), IO handles are bound to the driver that created
them, and buffers are handed to the kernel by ownership — so
[compio::net::TcpStream] is !Send and cannot be put behind an Arc<Mutex<_>>
at all.
So “share one connection across threads simultaneously” is not the thing to build. Two different things hide behind that phrase:
- Concurrent use of a single connection — only meaningful for protocols that multiplex (HTTP/2, Redis pipelining). Otherwise a connection is an exclusive resource and sharing means taking turns.
- A pool shared by every thread — any thread can check out some connection. That is what this crate provides.
§Design
Pool is Send + Sync and cheap to clone, but holds no connections. The
connections live in a per-thread shard stored in a thread-local, so:
- the acquire fast path touches no atomics and takes no lock;
Manage::Connectionnever needs to beSend;- a connection is always driven by the compio driver that created it;
- at thread exit, that thread’s connections close on their own thread.
Config::max_size is therefore per shard, not per process. A global
cap would need a cross-thread semaphore on the hot path, which is the exact
contention thread-per-core exists to avoid.
§Letting connections migrate
Pure sharding wastes connections when load is skewed: a quiet thread holds
idle sockets a busy thread could use. The optional Reservoir fixes that
with a bounded, lock-free ArrayQueue shared by every thread: a shard whose
free list is over min_idle detaches the surplus into it, and a shard whose
free list is empty pops from it before paying for a handshake. The claiming
thread re-wraps the socket in its own runtime via Detach::attach.
It requires Detach, which is platform-dependent and deliberately
opt-in:
| driver | attach | connections may change threads |
|---|---|---|
| io_uring (Linux) | no-op | yes |
| poll (Unix fallback) | no-op | yes |
| IOCP (Windows) | CreateIoCompletionPort, once per handle | no |
§Cancellation is the sharp edge
With completion-based IO, dropping a future mid-operation does not undo the
operation. The connection’s protocol state is then unknown and it must be
destroyed rather than returned. See Pooled::begin_op.
§Example
use std::time::Duration;
use compio_pool::{Config, Manage, Pool, SlotMeta};
struct Tcp(String);
impl Manage for Tcp {
type Connection = compio::net::TcpStream;
type Error = std::io::Error;
async fn connect(&self) -> std::io::Result<Self::Connection> {
compio::net::TcpStream::connect(&self.0).await
}
async fn recycle(
&self,
conn: &mut Self::Connection,
_meta: &SlotMeta,
) -> std::io::Result<()> {
conn.peer_addr().map(|_| ())
}
}
let pool = Pool::builder(Tcp("127.0.0.1:6379".into()))
.max_size(8) // per thread
.min_idle(2)
.acquire_timeout(Duration::from_secs(5))
.build();
// Clone `pool` onto every compio thread; each gets its own shard.
let mut conn = pool.acquire().await?;
let op = conn.begin_op();
// ... use `conn` ...
op.complete_op();Structs§
- Builder
- Fluent constructor for
Pool. - Config
- Tunables for a
Pool. - Metrics
- A point-in-time snapshot of pool activity, from
Pool::metrics. - NoExchange
- The default: shards never share. Zero-sized, zero-cost.
- OpGuard
- Poisons its connection unless
completeis called. - Pool
- A sharded connection pool for
compio. - Pooled
- A connection checked out of the pool.
- Reservoir
- A bounded, lock-free pool of detached connections shared by every thread.
- Slot
Meta - Lifecycle metadata the pool tracks for every connection.
Enums§
- Error
- Failure modes of
Pool::acquire. - Parked
- What an
Exchangedid with a connection offered toExchange::park. - Unparked
- What an
Exchangeproduced forExchange::unpark.