#[cfg(not(unix))]
fn main() {
eprintln!("this example is unix-only; `Detach` is not sound on IOCP");
}
#[cfg(unix)]
fn main() -> std::io::Result<()> {
unix::run()
}
#[cfg(unix)]
mod unix {
use std::{
io,
net::SocketAddr,
os::fd::{FromRawFd, IntoRawFd, OwnedFd},
sync::{
Arc,
atomic::{AtomicU64, Ordering::Relaxed},
},
time::Duration,
};
use compio::{
buf::BufResult,
driver::ToSharedFd,
io::{AsyncRead, AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
};
use compio_pool::{Config, Detach, Manage, Pool, Reservoir, SlotMeta};
const PARKERS: usize = 4;
const PER_THREAD: usize = 3;
struct StealManager {
addr: SocketAddr,
dials: Arc<AtomicU64>,
}
impl Manage for StealManager {
type Connection = TcpStream;
type Error = io::Error;
async fn connect(&self) -> io::Result<TcpStream> {
self.dials.fetch_add(1, Relaxed);
let conn = TcpStream::connect(self.addr).await?;
conn.set_nodelay(true)?;
Ok(conn)
}
async fn recycle(&self, conn: &mut TcpStream, _meta: &SlotMeta) -> io::Result<()> {
conn.peer_addr().map(|_| ())
}
}
impl Detach for StealManager {
type Parked = OwnedFd;
fn detach(conn: TcpStream) -> Option<OwnedFd> {
let shared = conn.to_shared_fd();
drop(conn);
let socket = shared.try_unwrap().ok()?;
Some(unsafe { OwnedFd::from_raw_fd(socket.into_raw_fd()) })
}
async fn attach(fd: OwnedFd) -> io::Result<TcpStream> {
TcpStream::from_std(std::net::TcpStream::from(fd))
}
}
async fn echo(conn: &mut TcpStream, msg: &str) -> io::Result<String> {
let BufResult(written, _) = conn.write_all(msg.as_bytes().to_vec()).await;
written?;
let BufResult(read, buf) = conn.read_exact(vec![0u8; msg.len()]).await;
read?;
Ok(String::from_utf8_lossy(&buf).into_owned())
}
fn spawn_echo_server() -> io::Result<SocketAddr> {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
tx.send(listener.local_addr().unwrap()).unwrap();
loop {
let Ok((mut stream, _)) = listener.accept().await else {
return;
};
compio::runtime::spawn(async move {
loop {
let BufResult(read, buf) = stream.read(vec![0u8; 64]).await;
match read {
Ok(0) | Err(_) => return,
Ok(n) => {
let BufResult(w, _) =
stream.write_all(buf[..n].to_vec()).await;
if w.is_err() {
return;
}
}
}
}
})
.detach();
}
})
});
Ok(rx.recv().unwrap())
}
fn on_compio_thread<T: Send + 'static>(
f: impl FnOnce(Pool<StealManager, Reservoir<StealManager>>) -> T + Send + 'static,
pool: &Pool<StealManager, Reservoir<StealManager>>,
) -> T {
let pool = pool.clone();
std::thread::spawn(move || f(pool)).join().unwrap()
}
pub fn run() -> io::Result<()> {
let addr = spawn_echo_server()?;
let dials = Arc::new(AtomicU64::new(0));
let total = PARKERS * PER_THREAD;
let pool = Pool::builder(StealManager {
addr,
dials: dials.clone(),
})
.config(
Config::new()
.max_size(PER_THREAD)
.min_idle(0)
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(None)
.max_lifetime(None),
)
.exchange(Reservoir::new(total))
.build();
println!("echo server on {addr}");
println!("reservoir capacity {total}\n");
let barrier = Arc::new(std::sync::Barrier::new(PARKERS));
let parkers: Vec<_> = (0..PARKERS)
.map(|worker| {
let pool = pool.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let mut held = Vec::new();
for _ in 0..PER_THREAD {
held.push(pool.acquire().await.expect("acquire"));
}
for (i, conn) in held.iter_mut().enumerate() {
let op = conn.begin_op();
let msg = format!("w{worker}c{i}");
assert_eq!(echo(conn, &msg).await.expect("echo"), msg);
op.complete_op();
}
barrier.wait();
drop(held);
})
})
})
.collect();
for p in parkers {
p.join().unwrap();
}
let after_park = pool.metrics();
println!("after {PARKERS} threads parked their connections:");
println!(" dials {}", dials.load(Relaxed));
println!(" parked {}", after_park.parked);
println!(
" live {} (a parked connection belongs to no shard)\n",
after_park.live
);
assert_eq!(after_park.parked as usize, total, "all should be parked");
assert_eq!(after_park.live, 0);
let dials_before = dials.load(Relaxed);
on_compio_thread(
|pool| {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let mut held = Vec::new();
for i in 0..PER_THREAD {
let mut conn = pool.acquire().await.expect("acquire");
let op = conn.begin_op();
let msg = format!("stolen{i}");
assert_eq!(echo(&mut conn, &msg).await.expect("echo"), msg);
op.complete_op();
held.push(conn);
}
drop(held);
})
},
&pool,
);
let after_steal = pool.metrics();
println!("after a fresh thread served {PER_THREAD} requests:");
println!(
" dials {} (+{} — it stole instead of dialling)",
dials.load(Relaxed),
dials.load(Relaxed) - dials_before,
);
println!(" unparked {}", after_steal.unparked);
println!(" parked {}\n", after_steal.parked);
assert_eq!(
dials.load(Relaxed),
dials_before,
"a thread with an empty free list must drain the queue before dialling"
);
assert_eq!(after_steal.unparked as usize, PER_THREAD);
println!("metrics: {:#?}", pool.metrics());
println!(
"\nOK: {total} connections opened by {PARKERS} threads, \
{PER_THREAD} of them later served requests on a thread that never dialled."
);
pool.close();
Ok(())
}
}