use super::requests::{IoRequest, UringOpType};
use io_uring::{opcode, types, IoUring};
use std::cell::Cell;
use std::collections::HashMap;
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
use std::sync::{Arc, LazyLock, OnceLock};
use std::time::Duration;
static URING_CONFIG: OnceLock<UringConfig> = OnceLock::new();
struct UringConfig {
queue_depth: usize,
thread_count: usize,
}
pub fn init_uring_config(queue_depth: usize, thread_count: usize) {
let _ = URING_CONFIG.set(UringConfig {
queue_depth: queue_depth.max(1),
thread_count: thread_count.max(1),
});
}
struct UringThreadHandle {
request_tx: SyncSender<Arc<IoRequest>>,
}
pub static URING_THREADS: LazyLock<Vec<UringThreadHandle>> = LazyLock::new(|| {
let queue_depth = get_queue_depth();
let thread_count = get_thread_count();
let mut threads = Vec::with_capacity(thread_count);
for i in 0..thread_count {
let (tx, rx) = std::sync::mpsc::sync_channel(queue_depth);
std::thread::Builder::new()
.name(format!("gfs-uring-{i}"))
.spawn(move || run_uring_thread(rx, queue_depth as u32, i))
.expect("Failed to spawn io_uring thread");
threads.push(UringThreadHandle { request_tx: tx });
}
tracing::info!(
thread_count,
queue_depth,
"io_uring thread pool initialised for page cache"
);
threads
});
static THREAD_SELECTOR: AtomicU64 = AtomicU64::new(0);
static USER_DATA_COUNTER: AtomicU64 = AtomicU64::new(1);
const DEFAULT_SUBMIT_BATCH_SIZE: usize = 128;
const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_millis(1);
pub fn try_submit_request(request: Arc<IoRequest>) -> bool {
let thread_idx =
(THREAD_SELECTOR.fetch_add(1, Ordering::Relaxed) as usize) % URING_THREADS.len();
URING_THREADS[thread_idx]
.request_tx
.try_send(request)
.is_ok()
}
pub fn submit_request(request: Arc<IoRequest>) {
let thread_idx =
(THREAD_SELECTOR.fetch_add(1, Ordering::Relaxed) as usize) % URING_THREADS.len();
match URING_THREADS[thread_idx]
.request_tx
.try_send(Arc::clone(&request))
{
Ok(()) => {}
Err(std::sync::mpsc::TrySendError::Full(_)) => {
request.fail(io::Error::new(
io::ErrorKind::WouldBlock,
"io_uring submission channel full",
));
}
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
request.fail(io::Error::new(
io::ErrorKind::BrokenPipe,
"io_uring thread died",
));
}
}
}
fn run_uring_thread(request_rx: Receiver<Arc<IoRequest>>, queue_depth: u32, thread_id: usize) {
let mut ring = match IoUring::builder().build(queue_depth) {
Ok(r) => r,
Err(e) => {
tracing::error!(error = %e, thread_id, "failed to create io_uring; thread exiting");
return;
}
};
let mut pending: HashMap<u64, Arc<IoRequest>> = HashMap::with_capacity(queue_depth as usize);
let submit_batch_size = DEFAULT_SUBMIT_BATCH_SIZE;
thread_local! {
static SPIN_COUNT: Cell<u32> = Cell::new(0);
}
loop {
let retries = process_completions(&mut ring, &mut pending);
let needs_submit = !retries.is_empty();
if !pending.is_empty() {
if needs_submit {
SPIN_COUNT.with(|c| c.set(0));
}
}
let mut batch_count = 0usize;
let mut should_exit = false;
loop {
let request = if pending.is_empty() && batch_count == 0 {
match request_rx.recv_timeout(DEFAULT_POLL_TIMEOUT) {
Ok(req) => Some(req),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => None,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
should_exit = true;
None
}
}
} else {
match request_rx.try_recv() {
Ok(req) => Some(req),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
should_exit = true;
None
}
}
};
match request {
Some(request) => {
if let Err(e) = push_to_sq(&mut ring, &mut pending, request) {
tracing::error!(error = %e, "failed to push to io_uring SQ");
} else {
batch_count += 1;
}
if batch_count >= submit_batch_size {
break;
}
}
None => break,
}
}
if should_exit {
if batch_count > 0 {
let _ = ring.submit();
}
tracing::info!(thread_id, "io_uring thread shutting down");
return;
}
if batch_count > 0 || needs_submit {
if let Err(e) = ring.submit() {
tracing::error!(error = %e, batch_count, "failed to submit io_uring batch");
}
}
if !pending.is_empty() && batch_count == 0 {
let should_yield = SPIN_COUNT.with(|c| {
let n = c.get().saturating_add(1);
c.set(n);
n % 32 == 0
});
if should_yield {
std::thread::yield_now();
} else {
std::hint::spin_loop();
}
continue;
}
}
}
fn push_to_sq(
ring: &mut IoUring,
pending: &mut HashMap<u64, Arc<IoRequest>>,
request: Arc<IoRequest>,
) -> io::Result<()> {
let user_data = USER_DATA_COUNTER.fetch_add(1, Ordering::Relaxed);
let sqe = match request.op_type {
UringOpType::Read => {
let (buf_ptr, read_offset, read_len) = {
let state = request.state.lock().unwrap();
let br = state.bytes_transferred;
(
unsafe { state.buffer.as_ptr().add(br) as *mut u8 },
request.offset + br as u64,
(request.length - br) as u32,
)
};
opcode::Read::new(types::Fd(request.fd), buf_ptr, read_len)
.offset(read_offset)
.build()
}
UringOpType::Write => {
let (buf_ptr, write_offset, write_len) = {
let state = request.state.lock().unwrap();
let bt = state.bytes_transferred;
(
unsafe { state.buffer.as_ptr().add(bt) as *const u8 },
request.offset + bt as u64,
(request.length - bt) as u32,
)
};
opcode::Write::new(types::Fd(request.fd), buf_ptr, write_len)
.offset(write_offset)
.build()
}
UringOpType::OpenAt => {
let state = request.state.lock().unwrap();
let path_ptr = state.buffer.as_ptr() as *const libc::c_char;
opcode::OpenAt::new(types::Fd(request.fd), path_ptr)
.flags(request.open_flags | libc::O_CLOEXEC)
.mode(0o644)
.build()
}
UringOpType::Close => opcode::Close::new(types::Fd(request.fd)).build(),
UringOpType::UnlinkAt => {
let state = request.state.lock().unwrap();
let path_ptr = state.buffer.as_ptr() as *const libc::c_char;
opcode::UnlinkAt::new(types::Fd(request.fd), path_ptr).build()
}
}
.user_data(user_data);
let mut sq = ring.submission();
if sq.is_full() {
drop(sq);
request.fail(io::Error::new(
io::ErrorKind::WouldBlock,
"io_uring submission queue full",
));
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"io_uring submission queue full",
));
}
unsafe {
if sq.push(&sqe).is_err() {
drop(sq);
request.fail(io::Error::other("Failed to push to SQ"));
return Err(io::Error::other("Failed to push to SQ"));
}
}
drop(sq);
pending.insert(user_data, request);
Ok(())
}
fn process_completions(
ring: &mut IoUring,
pending: &mut HashMap<u64, Arc<IoRequest>>,
) -> Vec<Arc<IoRequest>> {
let mut retries = Vec::new();
for cqe in ring.completion() {
let user_data = cqe.user_data();
let result = cqe.result();
let Some(request) = pending.remove(&user_data) else {
tracing::warn!(user_data, "CQE for unknown user_data");
continue;
};
let mut state = request.state.lock().unwrap();
if result < 0 {
state.err = Some(io::Error::from_raw_os_error(-result));
state.completed = true;
} else {
match request.op_type {
UringOpType::Read => {
let n = result as usize;
if n == 0 {
let bytes_transferred = state.bytes_transferred;
state.buffer.truncate(bytes_transferred);
state.result_code = bytes_transferred as i32;
state.completed = true;
} else {
state.bytes_transferred += n;
if state.bytes_transferred >= request.length {
let bytes_transferred = state.bytes_transferred;
state.buffer.truncate(bytes_transferred);
state.result_code = bytes_transferred as i32;
state.completed = true;
} else {
drop(state);
retries.push(request);
continue;
}
}
}
UringOpType::Write => {
let n = result as usize;
state.bytes_transferred += n;
if state.bytes_transferred >= request.length {
state.result_code = 0;
state.completed = true;
} else {
drop(state);
retries.push(request);
continue;
}
}
UringOpType::OpenAt => {
state.result_code = result;
state.completed = true;
}
UringOpType::Close | UringOpType::UnlinkAt => {
state.result_code = 0;
state.completed = true;
}
}
}
if let Some(waker) = state.waker.take() {
drop(state);
waker.wake();
}
}
retries
}
fn get_queue_depth() -> usize {
if let Some(config) = URING_CONFIG.get() {
return config.queue_depth;
}
std::env::var("GOOSEFS_USER_CLIENT_CACHE_URING_QUEUE_DEPTH")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(16384)
}
fn get_thread_count() -> usize {
if let Some(config) = URING_CONFIG.get() {
return config.thread_count;
}
std::env::var("GOOSEFS_USER_CLIENT_CACHE_URING_THREAD_COUNT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8)
}