Skip to main content

wal/
wal.rs

1//! Durable **file I/O**: a write-ahead-log server. Every request is appended to a
2//! log file and `fdatasync`'d before the ack — the socket-read -> file-append ->
3//! fdatasync -> socket-write path of a database or a message queue.
4//!
5//! The append and the `sync_data` are both `io_uring` submissions from the pinned
6//! worker, so the whole durable-commit path stays on one ring. This is also where
7//! the pool's `!Send` guarantee matters most: each pooled resource owns its own
8//! open log segment (an fd bound to this thread's ring), so appends from different
9//! connections never contend and a segment can never be touched from another
10//! thread.
11//!
12//! Protocol: persistent connection, one payload per request; the server appends
13//! it and replies `OK\n`. One socket read is treated as one record — fine for the
14//! loopback benchmark driver.
15//!
16//! ```text
17//! WAL_DIR=/tmp/bench-wal WAL_SYNC=1 \
18//! cargo run --release --example wal -- [ADDR] [CAPACITY]
19//!   ADDR       bind address               default 0.0.0.0:7000
20//!   CAPACITY   log segments per worker    default 1024
21//!   WAL_SYNC=0 skips the fdatasync, isolating raw append throughput
22//! ```
23//!
24//! Drive it with `examples/load_wal.rs`.
25
26use std::{
27    io,
28    net::SocketAddr,
29    path::PathBuf,
30    sync::{
31        LazyLock,
32        atomic::{AtomicU64, Ordering},
33    },
34    thread,
35};
36
37use compio::{
38    BufResult,
39    fs::{File, OpenOptions},
40    io::{AsyncRead, AsyncWriteAtExt, AsyncWriteExt},
41    net::{TcpListener, TcpStream},
42    runtime::Runtime,
43};
44use compio_pool::{LocalPool, ManageConnection, Pool, bind_reuseport, cpu};
45
46/// Where log segments are written.
47static WAL_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
48    PathBuf::from(std::env::var("WAL_DIR").unwrap_or_else(|_| "/tmp/bench-wal".into()))
49});
50
51/// Whether to `fdatasync` after every append. On by default; the whole point of
52/// a WAL is that the ack means "on disk".
53static SYNC: LazyLock<bool> =
54    LazyLock::new(|| std::env::var("WAL_SYNC").map(|v| v != "0").unwrap_or(true));
55
56/// Names segment files uniquely across every worker thread.
57static NEXT_SEGMENT: AtomicU64 = AtomicU64::new(0);
58
59/// One append-only log segment and the offset of its tail.
60struct Wal {
61    file: File,
62    offset: u64,
63}
64
65/// Opens a fresh log segment per pooled slot. Writing at an explicit offset
66/// (rather than relying on `O_APPEND`) keeps the append a single positional
67/// submission.
68struct WalManager;
69
70impl ManageConnection for WalManager {
71    type Connection = Wal;
72    type Error = io::Error;
73
74    async fn connect(&self) -> io::Result<Wal> {
75        let id = NEXT_SEGMENT.fetch_add(1, Ordering::Relaxed);
76        let path = WAL_DIR.join(format!("seg-{id}.log"));
77        let file = OpenOptions::new()
78            .write(true)
79            .create(true)
80            .truncate(true)
81            .open(&path)
82            .await?;
83        Ok(Wal { file, offset: 0 })
84    }
85
86    async fn is_valid(&self, _wal: &mut Wal) -> io::Result<()> {
87        Ok(())
88    }
89
90    fn has_broken(&self, _wal: &mut Wal) -> bool {
91        false
92    }
93}
94
95fn main() -> io::Result<()> {
96    let addr: SocketAddr = std::env::args()
97        .nth(1)
98        .unwrap_or_else(|| "0.0.0.0:7000".into())
99        .parse()
100        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad address: {e}")))?;
101    let capacity: u32 = std::env::args()
102        .nth(2)
103        .and_then(|s| s.parse().ok())
104        .unwrap_or(1024);
105
106    std::fs::create_dir_all(&*WAL_DIR)?;
107    let pool = Pool::builder().max_size(capacity).build(WalManager);
108
109    let cores = cpu::cores();
110    if cores.is_empty() {
111        return Err(io::Error::new(io::ErrorKind::Unsupported, "no cores to run on"));
112    }
113    println!(
114        "wal on {addr} -> {} (fdatasync: {}): {} workers, capacity {capacity}/worker",
115        WAL_DIR.display(),
116        if *SYNC { "on" } else { "off" },
117        cores.len()
118    );
119
120    let mut handles = Vec::new();
121    for (index, core) in cores.into_iter().enumerate() {
122        let pool = pool.clone();
123        let handle = thread::Builder::new()
124            .name(format!("worker/{index}"))
125            .spawn(move || worker(core, addr, pool))?;
126        handles.push(handle);
127    }
128    for handle in handles {
129        let _ = handle.join();
130    }
131    Ok(())
132}
133
134fn worker(core: cpu::CoreId, addr: SocketAddr, pool: Pool<WalManager>) {
135    cpu::pin_current(core);
136
137    let runtime = Runtime::builder().build().expect("build compio runtime");
138    runtime.block_on(async move {
139        let local = pool.local();
140
141        let std_listener = bind_reuseport(addr, 1024, None).expect("bind SO_REUSEPORT");
142        let listener = TcpListener::from_std(std_listener).expect("wrap listener in ring");
143
144        loop {
145            let Ok((stream, _peer)) = listener.accept().await else {
146                continue;
147            };
148            compio::runtime::spawn(serve(local.clone(), stream)).detach();
149        }
150    });
151}
152
153/// Lease one log segment for the life of the connection; append and sync every
154/// record before acking it.
155async fn serve(local: LocalPool<WalManager>, mut stream: TcpStream) {
156    let mut lease = match local.get().await {
157        Ok(lease) => lease,
158        Err(_) => return,
159    };
160    let mut buf = Vec::with_capacity(64 * 1024);
161    loop {
162        buf.clear();
163        let BufResult(read, b) = stream.read(buf).await;
164        buf = b;
165        let n = match read {
166            Ok(0) | Err(_) => return,
167            Ok(n) => n,
168        };
169
170        // Append the record at the segment tail. Read `offset` into a local so the
171        // expression does not borrow `lease` twice.
172        let offset = lease.offset;
173        let BufResult(w, b) = lease.file.write_all_at(buf, offset).await;
174        buf = b;
175        if w.is_err() {
176            return;
177        }
178        lease.offset += n as u64;
179
180        if *SYNC && lease.file.sync_data().await.is_err() {
181            return;
182        }
183
184        let BufResult(w, _) = stream.write_all(b"OK\n".to_vec()).await;
185        if w.is_err() {
186            return;
187        }
188    }
189}