#[cfg(unix)]
mod uds {
use std::{
io,
mem::ManuallyDrop,
net::Shutdown,
os::fd::{AsFd, AsRawFd, FromRawFd, RawFd},
path::{Path, PathBuf},
sync::{
Arc, Barrier, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
},
thread::JoinHandle,
time::{Duration, Instant},
};
use compio::{
buf::BufResult,
io::{AsyncRead, AsyncWriteExt},
net::{UnixListener, UnixStream},
};
use compio_pool::{Config, Manage, Metrics, Pool, SlotMeta};
const PROBE_AFTER: Duration = Duration::from_millis(250);
struct Params {
threads: usize,
rounds: usize,
max_size: usize,
}
impl Params {
fn from_env() -> Self {
Self {
threads: env_usize("THREADS", 4),
rounds: env_usize("ROUNDS", 200),
max_size: env_usize("MAX_SIZE", 4),
}
}
}
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
unsafe fn with_borrowed<T>(
fd: RawFd,
f: impl FnOnce(&std::os::unix::net::UnixStream) -> T,
) -> T {
let borrowed =
ManuallyDrop::new(unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd) });
f(&borrowed)
}
struct SocketPath(PathBuf);
impl SocketPath {
fn new() -> Self {
Self(std::env::temp_dir().join(format!("compio-pool-{}.sock", std::process::id())))
}
fn as_path(&self) -> &Path {
&self.0
}
}
impl Drop for SocketPath {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
async fn read_line(conn: &mut UnixStream) -> io::Result<String> {
let mut line = Vec::new();
loop {
let BufResult(read, buf) = conn.read(vec![0u8; 64]).await;
match read? {
0 => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"peer closed the socket",
));
}
n => line.extend_from_slice(&buf[..n]),
}
if let Some(end) = line.iter().position(|b| *b == b'\n') {
if end + 1 != line.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"trailing bytes after the reply: this stream is out of step",
));
}
line.truncate(end);
return String::from_utf8(line)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "reply is not utf-8"));
}
}
}
async fn request(conn: &mut UnixStream, command: &str) -> io::Result<String> {
let BufResult(written, _) = conn.write_all(format!("{command}\n").into_bytes()).await;
written?;
read_line(conn).await
}
async fn serve(mut stream: UnixStream, clients: Clients, fd: RawFd) {
converse(&mut stream).await;
clients.lock().unwrap().retain(|&client| client != fd);
}
async fn converse(stream: &mut UnixStream) {
loop {
let Ok(line) = read_line(stream).await else {
return;
};
let reply = match line.split_once(' ') {
Some(("ECHO", rest)) => rest.to_owned(),
_ => match line.as_str() {
"PING" => "PONG".to_owned(),
"QUIT" => return,
other => format!("ERR unknown command {other:?}"),
},
};
let BufResult(written, _) = stream.write_all(format!("{reply}\n").into_bytes()).await;
if written.is_err() {
return;
}
}
}
type Clients = Arc<Mutex<Vec<RawFd>>>;
struct Server {
path: PathBuf,
stop: Arc<AtomicBool>,
clients: Clients,
thread: Option<JoinHandle<()>>,
}
impl Server {
fn start(path: &Path) -> io::Result<Self> {
let path = path.to_path_buf();
let _ = std::fs::remove_file(&path);
let stop = Arc::new(AtomicBool::new(false));
let clients: Clients = Arc::new(Mutex::new(Vec::new()));
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let thread = std::thread::spawn({
let path = path.clone();
let stop = stop.clone();
let clients = clients.clone();
move || {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let listener = match UnixListener::bind(&path).await {
Ok(listener) => {
ready_tx.send(Ok(())).unwrap();
listener
}
Err(e) => {
ready_tx.send(Err(e)).unwrap();
return;
}
};
loop {
let Ok((stream, _)) = listener.accept().await else {
return;
};
if stop.load(Relaxed) {
return;
}
let fd = stream.as_fd().as_raw_fd();
clients.lock().unwrap().push(fd);
compio::runtime::spawn(serve(stream, clients.clone(), fd)).detach();
}
});
}
});
ready_rx.recv().unwrap()?;
Ok(Self {
path,
stop,
clients,
thread: Some(thread),
})
}
fn stop(mut self) {
for fd in self.clients.lock().unwrap().drain(..) {
unsafe { with_borrowed(fd, |sock| sock.shutdown(Shutdown::Both)) }.ok();
}
self.stop.store(true, Relaxed);
let _ = std::os::unix::net::UnixStream::connect(&self.path);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
let _ = std::fs::remove_file(&self.path);
}
}
#[derive(Default)]
struct Instrumentation {
dials: AtomicU64,
dial_nanos: AtomicU64,
probes: AtomicU64,
probe_nanos: AtomicU64,
probe_failures: AtomicU64,
fast_path: AtomicU64,
goodbyes: AtomicU64,
}
impl Instrumentation {
fn mean_dial_nanos(&self) -> u64 {
self.dial_nanos.load(Relaxed) / self.dials.load(Relaxed).max(1)
}
fn mean_probe_nanos(&self) -> u64 {
self.probe_nanos.load(Relaxed) / self.probes.load(Relaxed).max(1)
}
}
struct UnixManager {
path: PathBuf,
probe_after: Duration,
stats: Arc<Instrumentation>,
}
impl Manage for UnixManager {
type Connection = UnixStream;
type Error = io::Error;
async fn connect(&self) -> io::Result<UnixStream> {
let start = Instant::now();
let conn = UnixStream::connect(&self.path).await;
self.stats.dials.fetch_add(1, Relaxed);
self.stats
.dial_nanos
.fetch_add(start.elapsed().as_nanos() as u64, Relaxed);
conn
}
async fn recycle(&self, conn: &mut UnixStream, meta: &SlotMeta) -> io::Result<()> {
if meta.idle_for() < self.probe_after {
self.stats.fast_path.fetch_add(1, Relaxed);
return Ok(());
}
let start = Instant::now();
let replied = request(conn, "PING").await;
self.stats.probes.fetch_add(1, Relaxed);
self.stats
.probe_nanos
.fetch_add(start.elapsed().as_nanos() as u64, Relaxed);
let outcome = match replied {
Ok(reply) if reply == "PONG" => Ok(()),
Ok(other) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("bad PING reply: {other:?}"),
)),
Err(e) => Err(e),
};
if outcome.is_err() {
self.stats.probe_failures.fetch_add(1, Relaxed);
}
outcome
}
fn disconnect(&self, conn: UnixStream) {
use std::io::Write;
self.stats.goodbyes.fetch_add(1, Relaxed);
unsafe {
with_borrowed(conn.as_fd().as_raw_fd(), |sock| {
let mut sock = sock;
let _ = sock.write_all(b"QUIT\n");
})
}
}
}
#[derive(Default)]
struct Samples {
rtt: Vec<u64>,
acquire: Vec<u64>,
acquire_err: u64,
io_err: u64,
first_err: Option<String>,
}
impl Samples {
fn with_capacity(n: usize) -> Self {
Self {
rtt: Vec::with_capacity(n),
acquire: Vec::with_capacity(n),
..Self::default()
}
}
fn record_err(&mut self, e: &dyn std::fmt::Display) {
if self.first_err.is_none() {
self.first_err = Some(e.to_string());
}
}
fn errors(&self) -> u64 {
self.acquire_err + self.io_err
}
fn merge(&mut self, other: Samples) {
self.rtt.extend_from_slice(&other.rtt);
self.acquire.extend_from_slice(&other.acquire);
self.acquire_err += other.acquire_err;
self.io_err += other.io_err;
if self.first_err.is_none() {
self.first_err = other.first_err;
}
}
}
struct Phase {
name: &'static str,
what: &'static str,
note: Option<&'static str>,
}
const PHASES: [Phase; 2] = [
Phase {
name: "A steady",
what: "warm shards, no probes: acquire is a thread-local pop",
note: None,
},
Phase {
name: "B restart",
what: "same load, but every pooled socket died between the phases",
note: Some(
"closed = one per shard discarded by `recycle`, plus one per shard \
closed at thread exit",
),
},
];
struct PhaseResult {
samples: Samples,
elapsed: Duration,
delta: Metrics,
after: Metrics,
}
fn delta(after: &Metrics, before: &Metrics) -> Metrics {
Metrics {
live: 0,
idle: 0,
parked: 0,
created: after.created - before.created,
closed: after.closed - before.closed,
acquires: after.acquires - before.acquires,
waits: after.waits - before.waits,
timeouts: after.timeouts - before.timeouts,
poisoned: after.poisoned - before.poisoned,
recycle_failures: after.recycle_failures - before.recycle_failures,
unparked: after.unparked - before.unparked,
}
}
async fn exercise(
pool: &Pool<UnixManager>,
worker: usize,
phase: &str,
rounds: usize,
) -> Samples {
let mut samples = Samples::with_capacity(rounds);
for round in 0..rounds {
let checkout = Instant::now();
let mut conn = match pool.acquire().await {
Ok(conn) => conn,
Err(e) => {
samples.acquire_err += 1;
samples.record_err(&e);
continue;
}
};
samples.acquire.push(checkout.elapsed().as_nanos() as u64);
let op = conn.begin_op();
let sent = format!("{phase}/{worker}/{round}");
let started = Instant::now();
match request(&mut conn, &format!("ECHO {sent}")).await {
Ok(echoed) => {
op.complete_op();
samples.rtt.push(started.elapsed().as_nanos() as u64);
assert_eq!(echoed, sent, "echo mismatch");
}
Err(e) => {
samples.io_err += 1;
samples.record_err(&e);
}
}
}
samples
}
struct WorkerResult {
phases: Vec<(Samples, Duration)>,
held: usize,
}
fn percentile(sorted: &[u64], p: f64) -> u64 {
if sorted.is_empty() {
return 0;
}
let idx = ((sorted.len() - 1) as f64 * p).round() as usize;
sorted[idx]
}
fn us(nanos: u64) -> String {
format!("{:.1}", nanos as f64 / 1_000.0)
}
fn latency_row(label: &str, samples: &mut [u64]) {
samples.sort_unstable();
let mean = if samples.is_empty() {
0
} else {
samples.iter().sum::<u64>() / samples.len() as u64
};
println!(
" {label:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
us(mean),
us(percentile(samples, 0.50)),
us(percentile(samples, 0.90)),
us(percentile(samples, 0.99)),
us(percentile(samples, 0.999)),
us(percentile(samples, 1.0)),
);
}
fn report_phase(phase: &Phase, result: &mut PhaseResult, params: &Params) {
let ops = result.samples.rtt.len() as u64;
let secs = result.elapsed.as_secs_f64();
println!(
"\n[{}] {} workers x {} requests — {}",
phase.name, params.threads, params.rounds, phase.what,
);
println!(
" {ops} ok / {} err in {:.3}s -> {:.0} req/s",
result.samples.errors(),
secs,
ops as f64 / secs,
);
println!(
" {:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
"latency (us)", "mean", "p50", "p90", "p99", "p99.9", "max"
);
latency_row("acquire", &mut result.samples.acquire);
latency_row("round trip", &mut result.samples.rtt);
if result.samples.errors() > 0 {
println!(
" errors: {} getting a connection, {} on an open one; first was: {}",
result.samples.acquire_err,
result.samples.io_err,
result.samples.first_err.as_deref().unwrap_or("-"),
);
}
let m = &result.delta;
println!(
" pool: acquires={} waits={} timeouts={} created={} closed={} poisoned={} recycle_failures={} | live at end={}",
m.acquires,
m.waits,
m.timeouts,
m.created,
m.closed,
m.poisoned,
m.recycle_failures,
result.after.live,
);
if let Some(note) = phase.note {
println!(" {note}");
}
}
pub fn run() -> io::Result<()> {
let params = Params::from_env();
let socket = SocketPath::new();
let server = Server::start(socket.as_path())?;
let stats = Arc::new(Instrumentation::default());
let pool = Pool::new(
UnixManager {
path: socket.as_path().to_owned(),
probe_after: PROBE_AFTER,
stats: stats.clone(),
},
Config::new()
.max_size(params.max_size)
.min_idle(0)
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(Duration::from_secs(60)),
);
println!(
"socket {}\n{} workers x {} requests x {} phases, max_size {} per shard\n",
socket.as_path().display(),
params.threads,
params.rounds,
PHASES.len(),
params.max_size,
);
let gate = Arc::new(Barrier::new(params.threads + 1));
let rounds = params.rounds;
let workers: Vec<_> = (0..params.threads)
.map(|worker| {
let pool = pool.clone();
let gate = gate.clone();
std::thread::spawn(move || {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let mut phases = Vec::with_capacity(PHASES.len());
gate.wait();
let started = Instant::now();
let samples = exercise(&pool, worker, "A", rounds).await;
phases.push((samples, started.elapsed()));
gate.wait(); gate.wait();
let started = Instant::now();
let samples = exercise(&pool, worker, "B", rounds).await;
phases.push((samples, started.elapsed()));
WorkerResult {
phases,
held: pool.local_size(),
}
})
})
})
.collect();
let mut snapshots = vec![pool.metrics()];
let wall = Instant::now();
gate.wait();
gate.wait(); snapshots.push(pool.metrics());
println!("-- killing the server; every pooled connection dies with it");
server.stop();
let server = Server::start(socket.as_path())?;
std::thread::sleep(PROBE_AFTER + Duration::from_millis(150));
println!("-- server back up on the same path");
snapshots.push(pool.metrics());
gate.wait();
let finished: Vec<WorkerResult> = workers.into_iter().map(|w| w.join().unwrap()).collect();
let wall = wall.elapsed();
snapshots.push(pool.metrics());
server.stop();
const WINDOWS: [(usize, usize); PHASES.len()] = [(0, 1), (2, 3)];
let held: Vec<usize> = finished.iter().map(|w| w.held).collect();
let mut folded: Vec<(Samples, Duration)> = (0..PHASES.len())
.map(|_| (Samples::default(), Duration::ZERO))
.collect();
for worker in finished {
for (phase, (samples, elapsed)) in worker.phases.into_iter().enumerate() {
folded[phase].0.merge(samples);
folded[phase].1 = folded[phase].1.max(elapsed);
}
}
let mut results: Vec<PhaseResult> = folded
.into_iter()
.zip(WINDOWS)
.map(|((samples, elapsed), (before, after))| PhaseResult {
samples,
elapsed,
delta: delta(&snapshots[after], &snapshots[before]),
after: snapshots[after],
})
.collect();
for (phase, result) in PHASES.iter().zip(results.iter_mut()) {
report_phase(phase, result, ¶ms);
}
let metrics = *snapshots.last().unwrap();
let requests = (params.threads * params.rounds * PHASES.len()) as u64;
let dials = stats.dials.load(Relaxed);
let probes = stats.probes.load(Relaxed);
let fast_path = stats.fast_path.load(Relaxed);
let mean_rtt = {
let total: u64 = results
.iter()
.map(|r| r.samples.rtt.iter().sum::<u64>())
.sum();
let count: u64 = results.iter().map(|r| r.samples.rtt.len() as u64).sum();
total / count.max(1)
};
println!("\n--- pool counters (cumulative, both phases) ---");
println!("{metrics:#?}");
println!("\n--- what the numbers say ---");
println!(
" requests served per dial {} ({requests} requests over {dials} connections)",
requests / dials.max(1),
);
println!(
" mean dial cost {} us ({:.1}x a round trip, paid once per connection)",
us(stats.mean_dial_nanos()),
stats.mean_dial_nanos() as f64 / mean_rtt.max(1) as f64,
);
println!(
" mean PING probe cost {} us (on the acquire path, when armed)",
us(stats.mean_probe_nanos()),
);
println!(
" checkouts on the fast path {fast_path} of {} ({:.2}%) — popped and handed over, no IO",
metrics.acquires,
100.0 * fast_path as f64 / metrics.acquires.max(1) as f64,
);
println!(
" checkouts that paid a probe {probes} of {} ({:.2}%), of which {} found a dead peer",
metrics.acquires,
100.0 * probes as f64 / metrics.acquires.max(1) as f64,
stats.probe_failures.load(Relaxed),
);
println!(
" checkouts that had to wait {} of {} ({:.2}%)",
metrics.waits,
metrics.acquires,
100.0 * metrics.waits as f64 / metrics.acquires.max(1) as f64,
);
println!(
" timeouts / poisoned / recycle failures {} / {} / {}",
metrics.timeouts, metrics.poisoned, metrics.recycle_failures,
);
println!(
" QUIT goodbyes sent {} (one per connection closed, from `disconnect`)",
stats.goodbyes.load(Relaxed),
);
println!(
" connections held per worker {held:?} at exit, {} live",
metrics.live,
);
println!(
" total wall time {:.2}s (of which {:.2}s is the pause that lets \
the pooled connections go stale)",
wall.as_secs_f64(),
(PROBE_AFTER + Duration::from_millis(150)).as_secs_f64(),
);
assert_eq!(metrics.acquires, requests, "every request checked out once");
assert_eq!(
dials,
(params.threads * 2) as u64,
"{requests} requests should cost {} dials, not one per request",
params.threads * 2
);
assert_eq!(
metrics.recycle_failures, params.threads as u64,
"each shard's dead connection should be caught by recycle"
);
assert_eq!(metrics.timeouts, 0, "no checkout should have timed out");
assert_eq!(metrics.poisoned, 0, "no checkout was cancelled");
assert_eq!(
stats.goodbyes.load(Relaxed),
(params.threads * 2) as u64,
"every connection should have been said goodbye to"
);
assert_eq!(metrics.live, 0, "every connection closed on its own thread");
println!(
"\nOK: {requests} requests over {} connections, across a server restart \
no caller saw.",
params.threads * 2
);
Ok(())
}
}
#[cfg(unix)]
fn main() -> std::io::Result<()> {
uds::run()
}
#[cfg(not(unix))]
fn main() {
println!("examples/unix_socket: skipped, Unix domain sockets only.");
}