use std::collections::VecDeque;
use crate::write::pipeline::{OutputChunk, PipelineItem, WRITE_AHEAD};
use io_uring::opcode;
use io_uring::types::Fixed;
use io_uring::IoUring;
use crate::reorder_buffer::ReorderBuffer;
use std::alloc::{self, Layout};
use std::fs::File;
use std::io;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::ptr::NonNull;
use std::sync::mpsc::{Receiver, SyncSender};
use std::sync::atomic::Ordering::Relaxed;
use super::{PAGE_SIZE, alloc_page_aligned};
use super::metrics::WRITER_METRICS;
fn elapsed_ns_u64(start: std::time::Instant) -> u64 {
u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX)
}
#[cfg(feature = "test-hooks")]
pub mod test_hooks {
use std::sync::atomic::{AtomicU64, Ordering};
pub static PANIC_AT_DISPATCH_COUNT: AtomicU64 = AtomicU64::new(u64::MAX);
pub static DISPATCH_COUNT: AtomicU64 = AtomicU64::new(0);
pub fn reset() {
PANIC_AT_DISPATCH_COUNT.store(u64::MAX, Ordering::Relaxed);
DISPATCH_COUNT.store(0, Ordering::Relaxed);
}
}
const OUT_FD_IDX: u32 = 0;
const fn ud_buf_idx(ud: u64) -> u16 {
(ud & 0x3F) as u16
}
#[allow(clippy::cast_possible_truncation)] const fn ud_expected_len(ud: u64) -> u32 {
(ud >> 16) as u32
}
const BUF_SIZE: usize = 256 * 1024;
const NUM_BUFS: u16 = 64;
struct AlignedBufferPool {
base: NonNull<u8>,
layout: Layout,
count: u16,
buf_size: usize,
free: VecDeque<u16>,
}
unsafe impl Send for AlignedBufferPool {}
impl AlignedBufferPool {
fn new(count: u16, buf_size: usize) -> io::Result<Self> {
let total = buf_size * count as usize;
let (base, layout) = alloc_page_aligned(total)?;
let mut free = VecDeque::with_capacity(count as usize);
for i in 0..count {
free.push_back(i);
}
Ok(Self { base, layout, count, buf_size, free })
}
fn buf_ptr(&self, index: u16) -> *mut u8 {
debug_assert!((index) < self.count);
unsafe { self.base.as_ptr().add(index as usize * self.buf_size) }
}
fn iovecs(&self) -> Vec<libc::iovec> {
(0..self.count)
.map(|i| libc::iovec {
iov_base: self.buf_ptr(i).cast::<libc::c_void>(),
iov_len: self.buf_size,
})
.collect()
}
fn acquire(&mut self) -> Option<u16> {
self.free.pop_front()
}
fn release(&mut self, index: u16) {
self.free.push_back(index);
}
}
impl Drop for AlignedBufferPool {
fn drop(&mut self) {
unsafe { alloc::dealloc(self.base.as_ptr(), self.layout) };
}
}
struct UringState {
ring: IoUring,
pool: AlignedBufferPool,
current_buf: Option<u16>,
current_len: usize,
write_offset: u64,
logical_size: u64,
in_flight: u32,
}
impl UringState {
#[allow(clippy::cast_possible_truncation)] fn write(&mut self, data: &[u8]) -> io::Result<()> {
self.logical_size += data.len() as u64;
let mut offset = 0;
while offset < data.len() {
if self.current_buf.is_none() {
let idx = self.acquire_buffer()?;
self.current_buf = Some(idx);
self.current_len = 0;
}
let buf_idx = match self.current_buf {
Some(idx) => idx,
None => return Err(io::Error::other("no current buffer")),
};
let remaining = self.pool.buf_size - self.current_len;
let chunk = remaining.min(data.len() - offset);
unsafe {
let dst = self.pool.buf_ptr(buf_idx).add(self.current_len);
std::ptr::copy_nonoverlapping(data.as_ptr().add(offset), dst, chunk);
}
self.current_len += chunk;
offset += chunk;
if self.current_len == self.pool.buf_size {
self.submit_current()?;
}
}
Ok(())
}
fn submit_current(&mut self) -> io::Result<()> {
let buf_idx = match self.current_buf.take() {
Some(idx) => idx,
None => return Ok(()),
};
let len = self.current_len;
self.current_len = 0;
self.submit_buffer(buf_idx, len)
}
#[allow(clippy::cast_possible_truncation)] fn submit_buffer(&mut self, buf_idx: u16, data_len: usize) -> io::Result<()> {
if data_len == 0 {
self.pool.release(buf_idx);
return Ok(());
}
let aligned_len = round_up_to_page(data_len);
if aligned_len > data_len {
unsafe {
let dst = self.pool.buf_ptr(buf_idx).add(data_len);
std::ptr::write_bytes(dst, 0, aligned_len - data_len);
}
}
let ptr = self.pool.buf_ptr(buf_idx).cast_const();
let sqe = opcode::WriteFixed::new(
Fixed(OUT_FD_IDX),
ptr,
aligned_len as u32,
buf_idx,
)
.offset(self.write_offset)
.build()
.user_data((aligned_len as u64) << 16 | buf_idx as u64);
self.push_sqe(&sqe)?;
let t_submit = std::time::Instant::now();
self.ring
.submitter()
.submit()
.map_err(|e| io::Error::new(e.kind(), format!("io_uring submit failed: {e}")))?;
WRITER_METRICS.uring_submit_calls.fetch_add(1, Relaxed);
WRITER_METRICS
.uring_submit_ns
.fetch_add(elapsed_ns_u64(t_submit), Relaxed);
self.write_offset += aligned_len as u64;
self.in_flight += 1;
Ok(())
}
fn acquire_buffer(&mut self) -> io::Result<u16> {
if let Some(idx) = self.pool.acquire() {
return Ok(idx);
}
loop {
if self.in_flight == 0 {
return Err(io::Error::other("no free buffers and nothing in-flight"));
}
self.reap_cqes(true)?;
if let Some(idx) = self.pool.acquire() {
return Ok(idx);
}
}
}
fn push_sqe(&mut self, sqe: &io_uring::squeue::Entry) -> io::Result<()> {
unsafe {
if self.ring.submission().push(sqe).is_ok() {
return Ok(());
}
}
self.ring
.submitter()
.squeue_wait()
.map_err(|e| io::Error::new(e.kind(), format!("squeue_wait failed: {e}")))?;
unsafe {
self.ring
.submission()
.push(sqe)
.map_err(|_| io::Error::other("io_uring SQ still full after squeue_wait"))?;
}
Ok(())
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] fn reap_cqes(&mut self, wait: bool) -> io::Result<()> {
if self.in_flight == 0 {
return Ok(());
}
if wait {
let t_wait = std::time::Instant::now();
self.ring
.submitter()
.submit_and_wait(1)
.map_err(|e| {
io::Error::new(e.kind(), format!("io_uring submit_and_wait failed: {e}"))
})?;
let elapsed = elapsed_ns_u64(t_wait);
WRITER_METRICS.uring_submit_and_wait_calls.fetch_add(1, Relaxed);
WRITER_METRICS
.uring_submit_and_wait_ns
.fetch_add(elapsed, Relaxed);
WRITER_METRICS.uring_cq_wait_ns.fetch_add(elapsed, Relaxed);
}
for cqe in self.ring.completion() {
let ud = cqe.user_data();
let buf_idx = ud_buf_idx(ud);
let expected_len = ud_expected_len(ud);
let result = cqe.result();
self.in_flight -= 1;
self.pool.release(buf_idx);
if result < 0 {
return Err(io::Error::from_raw_os_error(-result));
}
if result.cast_unsigned() != expected_len {
return Err(io::Error::other(format!(
"io_uring short write: expected {expected_len} bytes, got {result}"
)));
}
}
Ok(())
}
fn drain(&mut self) -> io::Result<()> {
while self.in_flight > 0 {
self.reap_cqes(true)?;
}
Ok(())
}
fn flush_final(&mut self, file: &File) -> io::Result<()> {
self.submit_current()?;
self.drain()?;
file.set_len(self.logical_size)?;
let t_sync = std::time::Instant::now();
file.sync_all()?;
WRITER_METRICS
.sync_all_ns
.fetch_add(elapsed_ns_u64(t_sync), Relaxed);
Ok(())
}
}
const fn round_up_to_page(n: usize) -> usize {
(n + PAGE_SIZE - 1) & !(PAGE_SIZE - 1)
}
#[cfg(feature = "linux-direct-io")]
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn handle_copy_range_uring(
state: &mut UringState,
in_fd: std::os::unix::io::RawFd,
mut src_offset: u64,
mut remaining: u64,
) -> io::Result<()> {
state.logical_size += remaining;
while remaining > 0 {
if state.current_buf.is_none() {
let idx = state.acquire_buffer()?;
state.current_buf = Some(idx);
state.current_len = 0;
}
let buf_idx = match state.current_buf {
Some(idx) => idx,
None => return Err(io::Error::other("no current buffer")),
};
let space = state.pool.buf_size - state.current_len;
let chunk = space.min(remaining as usize);
let dst = unsafe { state.pool.buf_ptr(buf_idx).add(state.current_len) };
let n = unsafe {
libc::pread(
in_fd,
dst.cast::<libc::c_void>(),
chunk,
src_offset as i64,
)
};
if n < 0 {
return Err(io::Error::last_os_error());
}
if n.cast_unsigned() != chunk {
return Err(io::Error::other(format!(
"io_uring CopyRange pread short: expected {chunk}, got {n}"
)));
}
state.current_len += chunk;
src_offset += chunk as u64;
remaining -= chunk as u64;
if state.current_len == state.pool.buf_size {
state.submit_current()?;
}
}
Ok(())
}
#[allow(clippy::needless_pass_by_value)] pub(crate) fn uring_writer_thread(
rx: Receiver<PipelineItem>,
path: PathBuf,
framed_header: Vec<u8>,
init_tx: SyncSender<io::Result<()>>,
) -> io::Result<()> {
let result = uring_init_and_run(&rx, &path, &framed_header, &init_tx);
if let Err(ref e) = result {
eprintln!("[uring_writer] error: {e}");
}
result
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn uring_init_and_run(
rx: &Receiver<PipelineItem>,
path: &std::path::Path,
framed_header: &[u8],
init_tx: &SyncSender<io::Result<()>>,
) -> io::Result<()> {
let pool = AlignedBufferPool::new(NUM_BUFS, BUF_SIZE)?;
let iovecs = pool.iovecs();
let mut builder = IoUring::builder();
builder.setup_clamp();
let ring_depth = u32::from(NUM_BUFS) * 2;
let ring = builder
.build(ring_depth)
.map_err(|e| io::Error::new(e.kind(), format!("io_uring creation failed: {e}")))?;
{
let mut probe = io_uring::Probe::new();
if ring.submitter().register_probe(&mut probe).is_ok()
&& !probe.is_supported(opcode::WriteFixed::CODE)
{
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"kernel does not support io_uring WriteFixed (requires Linux 5.1+)",
));
}
}
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_DIRECT)
.open(path)?;
ring.submitter()
.register_files(&[file.as_raw_fd()])
.map_err(|e| io::Error::new(e.kind(), format!("register_files failed: {e}")))?;
unsafe { ring.submitter().register_buffers(&iovecs) }.map_err(|e| {
let os_err = e.raw_os_error();
if os_err == Some(libc::ENOMEM) || os_err == Some(libc::EPERM) {
io::Error::new(
e.kind(),
format!(
"register_buffers failed: RLIMIT_MEMLOCK too low \
(need {} MB, try `ulimit -l unlimited`): {e}",
(NUM_BUFS as usize * BUF_SIZE) / (1024 * 1024)
),
)
} else {
io::Error::new(e.kind(), format!("register_buffers failed: {e}"))
}
})?;
init_tx
.send(Ok(()))
.map_err(|_| io::Error::other("constructor dropped before init completed"))?;
let mut state = UringState {
ring,
pool,
current_buf: None,
current_len: 0,
write_offset: 0,
logical_size: 0,
in_flight: 0,
};
state.write(framed_header)?;
uring_main_loop(rx, &mut state)?;
state.flush_final(&file)?;
Ok(())
}
fn uring_main_loop(
rx: &Receiver<PipelineItem>,
state: &mut UringState,
) -> io::Result<()> {
let mut pending: ReorderBuffer<io::Result<OutputChunk>> =
ReorderBuffer::with_capacity(WRITE_AHEAD);
loop {
let t_recv = std::time::Instant::now();
let item = match rx.recv() {
Ok(item) => item,
Err(_) => break,
};
WRITER_METRICS
.recv_wait_ns
.fetch_add(elapsed_ns_u64(t_recv), Relaxed);
pending.push(item.seq, item.data);
WRITER_METRICS.record_reorder_high_water(pending.pending_len());
while let Some(result) = pending.pop_ready() {
let chunk = result?;
#[cfg(feature = "test-hooks")]
{
use std::sync::atomic::Ordering;
let n = test_hooks::DISPATCH_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
if n == test_hooks::PANIC_AT_DISPATCH_COUNT.load(Ordering::Relaxed) {
panic!("test-hooks: uring_writer dispatch #{n} panicking");
}
}
match chunk {
OutputChunk::Framed(parts) => {
let bytes = parts.into_vec();
let len = bytes.len() as u64;
let t_write = std::time::Instant::now();
state.write(&bytes)?;
WRITER_METRICS
.write_ns
.fetch_add(elapsed_ns_u64(t_write), Relaxed);
WRITER_METRICS.bytes_written.fetch_add(len, Relaxed);
}
OutputChunk::Raw(bytes) => {
let len = bytes.len() as u64;
let t_write = std::time::Instant::now();
state.write(&bytes)?;
WRITER_METRICS
.write_ns
.fetch_add(elapsed_ns_u64(t_write), Relaxed);
WRITER_METRICS.bytes_written.fetch_add(len, Relaxed);
}
OutputChunk::RawChunks(chunks) => {
let total_bytes: u64 = chunks.iter().map(|chunk| chunk.len() as u64).sum();
let t_write = std::time::Instant::now();
for chunk in &chunks {
state.write(chunk)?;
}
WRITER_METRICS
.write_ns
.fetch_add(elapsed_ns_u64(t_write), Relaxed);
WRITER_METRICS.bytes_written.fetch_add(total_bytes, Relaxed);
}
#[cfg(feature = "linux-direct-io")]
OutputChunk::CopyRange { in_fd, offset, len } => {
let t_write = std::time::Instant::now();
handle_copy_range_uring(state, in_fd, offset, len)?;
WRITER_METRICS
.write_ns
.fetch_add(elapsed_ns_u64(t_write), Relaxed);
WRITER_METRICS.bytes_written.fetch_add(len, Relaxed);
}
}
}
state.reap_cqes(false)?;
}
if pending.pending_len() > 0 {
return Err(io::Error::other(format!(
"uring writer: channel closed with {} item(s) still in reorder buffer; \
an upstream framer dropped without sending an earlier seq",
pending.pending_len(),
)));
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn aligned_buffer_pool_basics() {
let mut pool = AlignedBufferPool::new(4, PAGE_SIZE * 2).unwrap();
assert_eq!(pool.buf_ptr(0) as usize % PAGE_SIZE, 0);
assert_eq!(pool.buf_ptr(1) as usize % PAGE_SIZE, 0);
let diff = unsafe { pool.buf_ptr(1).offset_from(pool.buf_ptr(0)) };
assert_eq!(diff, (PAGE_SIZE * 2).cast_signed());
assert_eq!(pool.acquire(), Some(0));
assert_eq!(pool.acquire(), Some(1));
assert_eq!(pool.acquire(), Some(2));
assert_eq!(pool.acquire(), Some(3));
assert_eq!(pool.acquire(), None);
pool.release(1);
assert_eq!(pool.acquire(), Some(1));
}
#[test]
fn round_up_to_page_correctness() {
assert_eq!(round_up_to_page(0), 0);
assert_eq!(round_up_to_page(1), PAGE_SIZE);
assert_eq!(round_up_to_page(PAGE_SIZE), PAGE_SIZE);
assert_eq!(round_up_to_page(PAGE_SIZE + 1), PAGE_SIZE * 2);
}
#[test]
fn user_data_encoding() {
let write_ud = (262_144u64 << 16) | 42u64;
assert_eq!(ud_buf_idx(write_ud), 42);
assert_eq!(ud_expected_len(write_ud), 262_144);
let max_ud = (4096u64 << 16) | 63u64;
assert_eq!(ud_buf_idx(max_ud), 63);
assert_eq!(ud_expected_len(max_ud), 4096);
}
}