#[cfg(feature = "io_uring")]
use io_uring::{opcode, types, IoUring, Probe};
use std::collections::VecDeque;
use std::fs::File;
use std::io;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::ThreadId;
use crate::engine::errors::FatalError;
use crate::engine::format::{
calculate_padding, compute_chain_hash, create_sentinel, frame_size, LogHeader, LogMetadata,
GENESIS_HASH, HEADER_SIZE, LOG_METADATA_SIZE, MAX_PAYLOAD_SIZE, SENTINEL_SIZE,
};
pub const DEFAULT_QUEUE_DEPTH: u32 = 128;
pub const MAX_BATCH_WRITES: usize = 64;
const TAG_WRITE: u64 = 1;
const TAG_FSYNC: u64 = 2;
pub const DMA_ALIGNMENT: usize = 4096;
pub const DEFAULT_DMA_BUFFER_SIZE: usize = 64 * 1024;
pub const DEFAULT_DMA_POOL_SIZE: usize = 32;
#[cfg(feature = "io_uring")]
pub struct DmaBuffer {
ptr: *mut u8,
capacity: usize,
len: usize,
layout: std::alloc::Layout,
}
#[cfg(feature = "io_uring")]
impl DmaBuffer {
pub fn new(min_capacity: usize) -> io::Result<Self> {
let capacity = (min_capacity + DMA_ALIGNMENT - 1) & !(DMA_ALIGNMENT - 1);
let layout = std::alloc::Layout::from_size_align(capacity, DMA_ALIGNMENT)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
if ptr.is_null() {
return Err(io::Error::new(io::ErrorKind::OutOfMemory, "DMA buffer allocation failed"));
}
Ok(DmaBuffer {
ptr,
capacity,
len: 0,
layout,
})
}
pub fn capacity(&self) -> usize { self.capacity }
pub fn len(&self) -> usize { self.len }
pub fn is_empty(&self) -> bool { self.len == 0 }
pub fn as_ptr(&self) -> *const u8 { self.ptr }
pub fn as_mut_ptr(&mut self) -> *mut u8 { self.ptr }
pub fn as_slice(&self) -> &[u8] { unsafe { std::slice::from_raw_parts(self.ptr, self.len) } }
pub fn as_mut_slice(&mut self) -> &mut [u8] { unsafe { std::slice::from_raw_parts_mut(self.ptr, self.capacity) } }
pub fn set_len(&mut self, len: usize) { assert!(len <= self.capacity); self.len = len; }
pub fn clear(&mut self) { self.len = 0; }
pub fn write(&mut self, data: &[u8]) -> usize {
let available = self.capacity - self.len;
let to_write = data.len().min(available);
unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr(),
self.ptr.add(self.len),
to_write,
);
}
self.len += to_write;
to_write
}
pub fn pad_to_alignment(&mut self) -> usize {
let aligned_len = (self.len + DMA_ALIGNMENT - 1) & !(DMA_ALIGNMENT - 1);
let padding = aligned_len - self.len;
if padding > 0 && aligned_len <= self.capacity {
unsafe {
std::ptr::write_bytes(self.ptr.add(self.len), 0, padding);
}
self.len = aligned_len;
}
padding
}
pub fn aligned_len(&self) -> usize {
(self.len + DMA_ALIGNMENT - 1) & !(DMA_ALIGNMENT - 1)
}
}
#[cfg(feature = "io_uring")]
impl Drop for DmaBuffer {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
std::alloc::dealloc(self.ptr, self.layout);
}
}
}
}
#[cfg(feature = "io_uring")]
unsafe impl Send for DmaBuffer {}
#[cfg(feature = "io_uring")]
pub struct DmaBufferPool {
free: Vec<DmaBuffer>,
default_size: usize,
max_size: usize,
total_allocations: u64,
pool_hits: u64,
}
#[cfg(feature = "io_uring")]
impl DmaBufferPool {
pub fn new(default_size: usize, max_size: usize) -> Self {
DmaBufferPool {
free: Vec::with_capacity(max_size),
default_size,
max_size,
total_allocations: 0,
pool_hits: 0,
}
}
pub fn acquire(&mut self, min_size: usize) -> io::Result<DmaBuffer> {
let required_size = min_size.max(self.default_size);
if let Some(idx) = self.free.iter().position(|b| b.capacity() >= required_size) {
self.pool_hits += 1;
let mut buf = self.free.swap_remove(idx);
buf.clear();
return Ok(buf);
}
self.total_allocations += 1;
DmaBuffer::new(required_size)
}
pub fn release(&mut self, mut buffer: DmaBuffer) {
buffer.clear();
if self.free.len() < self.max_size { self.free.push(buffer); }
}
pub fn stats(&self) -> (u64, u64, usize) {
(self.total_allocations, self.pool_hits, self.free.len())
}
}
#[derive(Debug)]
struct PendingWrite {
index: u64,
offset: u64,
size: usize,
completed: bool,
}
#[derive(Debug)]
struct PendingFsync {
up_to_index: u64,
completed: bool,
}
#[cfg(feature = "io_uring")]
pub struct IoUringWriter {
ring: IoUring,
fd: RawFd,
_file: File,
write_offset: u64,
next_index: u64,
tail_hash: [u8; 16],
view_id: u64,
committed_index: AtomicU64,
owner_thread: ThreadId,
pending_writes: VecDeque<PendingWrite>,
pending_fsyncs: VecDeque<PendingFsync>,
writes_since_fsync: usize,
fsync_count: AtomicU64,
dma_pool: Option<DmaBufferPool>,
pending_dma_buffers: VecDeque<DmaBuffer>,
write_buffers: VecDeque<Vec<u8>>,
}
#[cfg(feature = "io_uring")]
impl IoUringWriter {
pub fn open(
path: &Path,
next_index: u64,
write_offset: u64,
tail_hash: [u8; 16],
view_id: u64,
queue_depth: Option<u32>,
) -> io::Result<Self> {
Self::open_with_options(path, next_index, write_offset, tail_hash, view_id, queue_depth, false)
}
pub fn open_direct(
path: &Path,
next_index: u64,
write_offset: u64,
tail_hash: [u8; 16],
view_id: u64,
queue_depth: Option<u32>,
) -> io::Result<Self> {
if write_offset % DMA_ALIGNMENT as u64 != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("write_offset {} is not {}-byte aligned for O_DIRECT", write_offset, DMA_ALIGNMENT),
));
}
Self::open_with_options(path, next_index, write_offset, tail_hash, view_id, queue_depth, true)
}
fn open_with_options(
path: &Path,
next_index: u64,
write_offset: u64,
tail_hash: [u8; 16],
view_id: u64,
queue_depth: Option<u32>,
use_direct_io: bool,
) -> io::Result<Self> {
let depth = queue_depth.unwrap_or(DEFAULT_QUEUE_DEPTH);
let ring = IoUring::new(depth)?;
let mut probe = Probe::new();
ring.submitter().register_probe(&mut probe)?;
if !probe.is_supported(opcode::Write::CODE) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"io_uring WRITE not supported",
));
}
if !probe.is_supported(opcode::Fsync::CODE) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"io_uring FSYNC not supported",
));
}
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let path_cstr = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid path"))?;
let mut flags = libc::O_RDWR | libc::O_CREAT;
if use_direct_io {
flags |= libc::O_DIRECT;
}
let mode = 0o644;
let fd = unsafe { libc::open(path_cstr.as_ptr(), flags, mode) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let file = unsafe { File::from_raw_fd(fd) };
let owner_thread = std::thread::current().id();
let committed_index = if next_index > 0 { AtomicU64::new(next_index - 1) } else { AtomicU64::new(u64::MAX) };
let dma_pool = if use_direct_io {
Some(DmaBufferPool::new(DEFAULT_DMA_BUFFER_SIZE, DEFAULT_DMA_POOL_SIZE))
} else {
None
};
Ok(IoUringWriter {
ring,
fd,
_file: file,
write_offset,
next_index,
tail_hash,
view_id,
committed_index,
owner_thread,
pending_writes: VecDeque::new(),
pending_fsyncs: VecDeque::new(),
writes_since_fsync: 0,
fsync_count: AtomicU64::new(0),
dma_pool,
pending_dma_buffers: VecDeque::new(),
write_buffers: VecDeque::new(),
})
}
pub fn is_direct_io(&self) -> bool { self.dma_pool.is_some() }
pub fn create(path: &Path, view_id: u64, queue_depth: Option<u32>) -> io::Result<Self> { Self::open(path, 0, 0, GENESIS_HASH, view_id, queue_depth) }
pub fn create_direct(path: &Path, view_id: u64, queue_depth: Option<u32>) -> io::Result<Self> { Self::open_direct(path, 0, 0, GENESIS_HASH, view_id, queue_depth) }
pub fn dma_pool_stats(&self) -> Option<(u64, u64, usize)> { self.dma_pool.as_ref().map(|p| p.stats()) }
pub fn submit_write(&mut self, payload: &[u8], timestamp_ns: u64) -> io::Result<u64> {
self.check_owner_thread()?;
if payload.len() > MAX_PAYLOAD_SIZE as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Payload size {} exceeds max {}", payload.len(), MAX_PAYLOAD_SIZE),
));
}
let index = self.next_index;
let prev_hash = self.tail_hash;
let header = LogHeader::new(
index,
self.view_id,
0, prev_hash,
payload,
timestamp_ns,
0, 1, );
let header_bytes = header.as_bytes();
let padding_size = calculate_padding(payload.len() as u32);
let sentinel = create_sentinel(index);
let logical_size = HEADER_SIZE + payload.len() + padding_size + SENTINEL_SIZE;
let offset = self.write_offset;
if let Some(ref mut pool) = self.dma_pool {
let aligned_size = (logical_size + DMA_ALIGNMENT - 1) & !(DMA_ALIGNMENT - 1);
let mut dma_buf = pool.acquire(aligned_size)?;
dma_buf.write(header_bytes);
dma_buf.write(payload);
if padding_size > 0 {
let padding = [0u8; 8];
dma_buf.write(&padding[..padding_size]);
}
dma_buf.write(&sentinel);
dma_buf.pad_to_alignment();
let write_size = dma_buf.len();
let write_op = opcode::Write::new(types::Fd(self.fd), dma_buf.as_ptr(), write_size as u32)
.offset(offset)
.build()
.user_data(TAG_WRITE | (index << 8));
unsafe {
self.ring
.submission()
.push(&write_op)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "SQ full"))?;
}
self.pending_writes.push_back(PendingWrite {
index,
offset,
size: write_size,
completed: false,
});
self.pending_dma_buffers.push_back(dma_buf);
self.tail_hash = compute_chain_hash(&header, payload);
self.next_index += 1;
self.write_offset += write_size as u64;
self.writes_since_fsync += 1;
} else {
let mut buffer = vec![0u8; logical_size];
buffer[..HEADER_SIZE].copy_from_slice(header_bytes);
buffer[HEADER_SIZE..HEADER_SIZE + payload.len()].copy_from_slice(payload);
let sentinel_offset = HEADER_SIZE + payload.len() + padding_size;
buffer[sentinel_offset..sentinel_offset + SENTINEL_SIZE].copy_from_slice(&sentinel);
let write_op = opcode::Write::new(types::Fd(self.fd), buffer.as_ptr(), logical_size as u32)
.offset(offset)
.build()
.user_data(TAG_WRITE | (index << 8));
unsafe {
self.ring
.submission()
.push(&write_op)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "SQ full"))?;
}
self.pending_writes.push_back(PendingWrite {
index,
offset,
size: logical_size,
completed: false,
});
self.write_buffers.push_back(buffer);
self.tail_hash = compute_chain_hash(&header, payload);
self.next_index += 1;
self.write_offset += logical_size as u64;
self.writes_since_fsync += 1;
}
self.ring.submit()?;
Ok(index)
}
pub fn submit_write_batch(&mut self, payloads: &[Vec<u8>], timestamp_ns: u64) -> io::Result<u64> {
self.check_owner_thread()?;
if payloads.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Empty batch"));
}
let start_index = self.next_index;
let mut chain_hash = self.tail_hash;
let use_dma = self.dma_pool.is_some();
for (i, payload) in payloads.iter().enumerate() {
if payload.len() > MAX_PAYLOAD_SIZE as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Payload {} size {} exceeds max", i, payload.len()),
));
}
let index = start_index + i as u64;
let header = LogHeader::new(
index,
self.view_id,
0,
chain_hash,
payload,
timestamp_ns,
0,
1,
);
let header_bytes = header.as_bytes();
let padding_size = calculate_padding(payload.len() as u32);
let sentinel = create_sentinel(index);
let logical_size = HEADER_SIZE + payload.len() + padding_size + SENTINEL_SIZE;
let offset = self.write_offset;
if use_dma {
let aligned_size = (logical_size + DMA_ALIGNMENT - 1) & !(DMA_ALIGNMENT - 1);
let mut dma_buf = self.dma_pool.as_mut().unwrap().acquire(aligned_size)?;
dma_buf.write(header_bytes);
dma_buf.write(payload);
if padding_size > 0 {
let padding = [0u8; 8];
dma_buf.write(&padding[..padding_size]);
}
dma_buf.write(&sentinel);
dma_buf.pad_to_alignment();
let write_size = dma_buf.len();
let write_op = opcode::Write::new(types::Fd(self.fd), dma_buf.as_ptr(), write_size as u32)
.offset(offset)
.build()
.user_data(TAG_WRITE | (index << 8));
unsafe {
self.ring
.submission()
.push(&write_op)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "SQ full"))?;
}
self.pending_writes.push_back(PendingWrite {
index,
offset,
size: write_size,
completed: false,
});
self.pending_dma_buffers.push_back(dma_buf);
self.write_offset += write_size as u64;
} else {
let mut buffer = vec![0u8; logical_size];
buffer[..HEADER_SIZE].copy_from_slice(header_bytes);
buffer[HEADER_SIZE..HEADER_SIZE + payload.len()].copy_from_slice(payload);
let sentinel_offset = HEADER_SIZE + payload.len() + padding_size;
buffer[sentinel_offset..sentinel_offset + SENTINEL_SIZE].copy_from_slice(&sentinel);
let write_op = opcode::Write::new(types::Fd(self.fd), buffer.as_ptr(), logical_size as u32)
.offset(offset)
.build()
.user_data(TAG_WRITE | (index << 8));
unsafe {
self.ring
.submission()
.push(&write_op)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "SQ full"))?;
}
self.pending_writes.push_back(PendingWrite {
index,
offset,
size: logical_size,
completed: false,
});
self.write_buffers.push_back(buffer);
self.write_offset += logical_size as u64;
}
chain_hash = compute_chain_hash(&header, payload);
}
self.tail_hash = chain_hash;
let last_index = self.next_index + payloads.len() as u64 - 1;
self.next_index += payloads.len() as u64;
self.writes_since_fsync += payloads.len();
self.ring.submit()?;
Ok(last_index)
}
pub fn flush(&mut self) -> io::Result<u64> {
self.check_owner_thread()?;
if self.pending_writes.is_empty() {
return Ok(self.committed_index.load(Ordering::Acquire));
}
let highest_pending = self.pending_writes.back().map(|w| w.index).unwrap_or(0);
let fsync_op = opcode::Fsync::new(types::Fd(self.fd))
.build()
.flags(io_uring::squeue::Flags::IO_DRAIN)
.user_data(TAG_FSYNC | (highest_pending << 8));
unsafe {
self.ring
.submission()
.push(&fsync_op)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "SQ full"))?;
}
self.pending_fsyncs.push_back(PendingFsync {
up_to_index: highest_pending,
completed: false,
});
self.ring.submit()?;
self.fsync_count.fetch_add(1, Ordering::Relaxed);
self.writes_since_fsync = 0;
self.wait_for_fsync(highest_pending)?;
Ok(highest_pending)
}
pub fn poll_completions(&mut self) -> io::Result<usize> {
let mut count = 0;
let use_dma = self.dma_pool.is_some();
while let Some(cqe) = self.ring.completion().next() {
let user_data = cqe.user_data();
let tag = user_data & 0xFF;
let index = user_data >> 8;
if cqe.result() < 0 {
return Err(io::Error::from_raw_os_error(-cqe.result()));
}
match tag {
TAG_WRITE => {
for pw in self.pending_writes.iter_mut() {
if pw.index == index {
pw.completed = true;
break;
}
}
while self.pending_writes.front().map(|w| w.completed).unwrap_or(false) {
self.pending_writes.pop_front();
if use_dma {
if let Some(dma_buf) = self.pending_dma_buffers.pop_front() {
if let Some(ref mut pool) = self.dma_pool {
pool.release(dma_buf);
}
}
} else {
self.write_buffers.pop_front();
}
}
}
TAG_FSYNC => {
for pf in self.pending_fsyncs.iter_mut() {
if pf.up_to_index == index {
pf.completed = true;
self.committed_index.store(index, Ordering::Release);
break;
}
}
while self.pending_fsyncs.front().map(|f| f.completed).unwrap_or(false) {
self.pending_fsyncs.pop_front();
}
}
_ => {}
}
count += 1;
}
Ok(count)
}
fn wait_for_fsync(&mut self, up_to_index: u64) -> io::Result<()> {
loop {
let current = self.committed_index.load(Ordering::Acquire);
if current != u64::MAX && current >= up_to_index {
return Ok(());
}
self.ring.submit_and_wait(1)?;
self.poll_completions()?;
}
}
fn check_owner_thread(&self) -> io::Result<()> {
if std::thread::current().id() != self.owner_thread {
return Err(io::Error::new(
io::ErrorKind::Other,
"IoUringWriter accessed from wrong thread",
));
}
Ok(())
}
pub fn next_index(&self) -> u64 {
self.next_index
}
pub fn committed_index(&self) -> Option<u64> {
let idx = self.committed_index.load(Ordering::Acquire);
if idx == u64::MAX {
None
} else {
Some(idx)
}
}
pub fn tail_hash(&self) -> [u8; 16] {
self.tail_hash
}
pub fn fsync_count(&self) -> u64 {
self.fsync_count.load(Ordering::Relaxed)
}
pub fn view_id(&self) -> u64 {
self.view_id
}
pub fn set_view_id(&mut self, view_id: u64) {
self.view_id = view_id;
}
pub fn has_pending(&self) -> bool {
!self.pending_writes.is_empty() || !self.pending_fsyncs.is_empty()
}
pub fn pending_write_count(&self) -> usize {
self.pending_writes.len()
}
}
#[cfg(feature = "io_uring")]
pub struct IoUringLogWriter {
inner: IoUringWriter,
}
#[cfg(feature = "io_uring")]
impl IoUringLogWriter {
pub fn open(
path: &Path,
next_index: u64,
write_offset: u64,
tail_hash: [u8; 16],
view_id: u64,
) -> io::Result<Self> {
Ok(IoUringLogWriter {
inner: IoUringWriter::open(path, next_index, write_offset, tail_hash, view_id, None)?,
})
}
pub fn create(path: &Path, view_id: u64) -> io::Result<Self> {
Ok(IoUringLogWriter {
inner: IoUringWriter::create(path, view_id, None)?,
})
}
pub fn append(
&mut self,
payload: &[u8],
_stream_id: u32,
_flags: u32,
timestamp_ns: u64,
) -> io::Result<u64> {
let index = self.inner.submit_write(payload, timestamp_ns)?;
self.inner.flush()?;
Ok(index)
}
pub fn append_batch(&mut self, payloads: &[Vec<u8>], timestamp_ns: u64) -> io::Result<u64> {
let last_index = self.inner.submit_write_batch(payloads, timestamp_ns)?;
self.inner.flush()?;
Ok(last_index)
}
pub fn next_index(&self) -> u64 {
self.inner.next_index()
}
pub fn committed_index(&self) -> Option<u64> {
self.inner.committed_index()
}
pub fn tail_hash(&self) -> [u8; 16] {
self.inner.tail_hash()
}
pub fn fsync_count(&self) -> u64 {
self.inner.fsync_count()
}
pub fn view_id(&self) -> u64 {
self.inner.view_id()
}
pub fn set_view_id(&mut self, view_id: u64) {
self.inner.set_view_id(view_id);
}
}
#[cfg(all(test, feature = "io_uring"))]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_uring_writer_basic() {
let path = Path::new("/tmp/chr_uring_test.log");
let _ = fs::remove_file(path);
let mut writer = IoUringWriter::create(path, 1, None).unwrap();
let idx0 = writer.submit_write(b"hello", 1_000_000_000).unwrap();
let idx1 = writer.submit_write(b"world", 2_000_000_000).unwrap();
assert_eq!(idx0, 0);
assert_eq!(idx1, 1);
assert_eq!(writer.next_index(), 2);
assert_eq!(writer.committed_index(), None);
let committed = writer.flush().unwrap();
assert_eq!(committed, 1);
assert_eq!(writer.committed_index(), Some(1));
fs::remove_file(path).unwrap();
}
#[test]
fn test_uring_writer_batch() {
let path = Path::new("/tmp/chr_uring_batch_test.log");
let _ = fs::remove_file(path);
let mut writer = IoUringWriter::create(path, 1, None).unwrap();
let payloads: Vec<Vec<u8>> = (0..10)
.map(|i| format!("entry_{}", i).into_bytes())
.collect();
let last_idx = writer.submit_write_batch(&payloads, 1_000_000_000).unwrap();
assert_eq!(last_idx, 9);
assert_eq!(writer.next_index(), 10);
let committed = writer.flush().unwrap();
assert_eq!(committed, 9);
assert_eq!(writer.fsync_count(), 1);
fs::remove_file(path).unwrap();
}
#[test]
fn test_uring_log_writer_compat() {
let path = Path::new("/tmp/chr_uring_compat_test.log");
let _ = fs::remove_file(path);
let mut writer = IoUringLogWriter::create(path, 1).unwrap();
let idx0 = writer.append(b"entry_0", 0, 0, 1_000_000_000).unwrap();
let idx1 = writer.append(b"entry_1", 0, 0, 2_000_000_000).unwrap();
assert_eq!(idx0, 0);
assert_eq!(idx1, 1);
assert_eq!(writer.committed_index(), Some(1));
fs::remove_file(path).unwrap();
}
#[test]
fn test_uring_writer_direct_io() {
let path = Path::new("/tmp/chr_uring_direct_test.log");
let _ = fs::remove_file(path);
let result = IoUringWriter::create_direct(path, 1, None);
match result {
Ok(mut writer) => {
assert!(writer.is_direct_io());
let idx0 = writer.submit_write(b"hello_direct", 1_000_000_000).unwrap();
let idx1 = writer.submit_write(b"world_direct", 2_000_000_000).unwrap();
assert_eq!(idx0, 0);
assert_eq!(idx1, 1);
assert_eq!(writer.write_offset % DMA_ALIGNMENT as u64, 0);
let committed = writer.flush().unwrap();
assert_eq!(committed, 1);
let (allocs, hits, free) = writer.dma_pool_stats().unwrap();
assert!(allocs >= 2, "Should have allocated at least 2 buffers");
assert!(free >= 2 || hits > 0, "Buffers should be recycled");
fs::remove_file(path).unwrap();
}
Err(e) => {
eprintln!("O_DIRECT not supported: {} - skipping test", e);
}
}
}
#[test]
fn test_uring_writer_direct_io_batch() {
let path = Path::new("/tmp/chr_uring_direct_batch_test.log");
let _ = fs::remove_file(path);
let result = IoUringWriter::create_direct(path, 1, None);
match result {
Ok(mut writer) => {
assert!(writer.is_direct_io());
let payloads: Vec<Vec<u8>> = (0..5)
.map(|i| format!("direct_entry_{}", i).into_bytes())
.collect();
let last_idx = writer.submit_write_batch(&payloads, 1_000_000_000).unwrap();
assert_eq!(last_idx, 4);
assert_eq!(writer.write_offset % DMA_ALIGNMENT as u64, 0);
let committed = writer.flush().unwrap();
assert_eq!(committed, 4);
assert_eq!(writer.fsync_count(), 1);
fs::remove_file(path).unwrap();
}
Err(e) => {
eprintln!("O_DIRECT not supported: {} - skipping test", e);
}
}
}
#[test]
fn test_dma_buffer_alignment() {
let buf = DmaBuffer::new(100).unwrap();
assert_eq!(buf.capacity(), DMA_ALIGNMENT);
assert_eq!(buf.as_ptr() as usize % DMA_ALIGNMENT, 0);
let buf2 = DmaBuffer::new(5000).unwrap();
assert_eq!(buf2.capacity(), 8192); assert_eq!(buf2.as_ptr() as usize % DMA_ALIGNMENT, 0);
}
#[test]
fn test_dma_buffer_pool() {
let mut pool = DmaBufferPool::new(DEFAULT_DMA_BUFFER_SIZE, 4);
let buf1 = pool.acquire(1000).unwrap();
let buf2 = pool.acquire(1000).unwrap();
let (allocs, hits, free) = pool.stats();
assert_eq!(allocs, 2);
assert_eq!(hits, 0);
assert_eq!(free, 0);
pool.release(buf1);
pool.release(buf2);
let (_, _, free) = pool.stats();
assert_eq!(free, 2);
let _buf3 = pool.acquire(1000).unwrap();
let (allocs, hits, _) = pool.stats();
assert_eq!(allocs, 2); assert_eq!(hits, 1); }
}