use std::path::Path;
use crate::Result;
use super::iouring::{IouringFile, IouringVfs};
use super::oslock::NativeLockHandle;
use super::tokio_backend::{TokioFile, TokioVfs};
use super::traits::{Vfs, VfsFile};
use super::types::{OpenMode, ReadReq, WriteReq};
#[derive(Clone)]
pub enum NativeVfs {
Iouring(IouringVfs),
Tokio(TokioVfs),
}
pub enum NativeFile {
Iouring(IouringFile),
Tokio(TokioFile),
}
#[must_use]
pub fn open_native(root: &Path) -> NativeVfs {
match IouringVfs::new(root) {
Ok(vfs) => NativeVfs::Iouring(vfs),
Err(error) => {
tracing::warn!(
root = %root.display(),
error = %error,
"io_uring is unavailable, so pagedb is continuing on the thread-pool \
backend: identical behaviour and durability, reduced I/O performance. \
Worth checking: RLIMIT_MEMLOCK (every ring is charged against it, and \
the default 8 MB is exhausted by a handful of concurrent stores), a \
seccomp profile blocking io_uring_setup (Docker's default profile \
does), or a kernel older than 5.1."
);
NativeVfs::Tokio(TokioVfs::new(root))
}
}
}
impl NativeVfs {
#[must_use]
pub fn uses_iouring(&self) -> bool {
matches!(self, Self::Iouring(_))
}
}
impl Vfs for NativeVfs {
type File = NativeFile;
type LockHandle = NativeLockHandle;
async fn open(&self, path: &str, mode: OpenMode) -> Result<Self::File> {
match self {
Self::Iouring(vfs) => Ok(NativeFile::Iouring(vfs.open(path, mode).await?)),
Self::Tokio(vfs) => Ok(NativeFile::Tokio(vfs.open(path, mode).await?)),
}
}
async fn remove(&self, path: &str) -> Result<()> {
match self {
Self::Iouring(vfs) => vfs.remove(path).await,
Self::Tokio(vfs) => vfs.remove(path).await,
}
}
async fn rename(&self, from: &str, to: &str) -> Result<()> {
match self {
Self::Iouring(vfs) => vfs.rename(from, to).await,
Self::Tokio(vfs) => vfs.rename(from, to).await,
}
}
async fn list_dir(&self, path: &str) -> Result<Vec<String>> {
match self {
Self::Iouring(vfs) => vfs.list_dir(path).await,
Self::Tokio(vfs) => vfs.list_dir(path).await,
}
}
async fn mkdir_all(&self, path: &str) -> Result<()> {
match self {
Self::Iouring(vfs) => vfs.mkdir_all(path).await,
Self::Tokio(vfs) => vfs.mkdir_all(path).await,
}
}
async fn sync_dir(&self, path: &str) -> Result<()> {
match self {
Self::Iouring(vfs) => vfs.sync_dir(path).await,
Self::Tokio(vfs) => vfs.sync_dir(path).await,
}
}
async fn lock_exclusive(&self, path: &str) -> Result<Self::LockHandle> {
match self {
Self::Iouring(vfs) => vfs.lock_exclusive(path).await,
Self::Tokio(vfs) => vfs.lock_exclusive(path).await,
}
}
async fn lock_shared(&self, path: &str) -> Result<Self::LockHandle> {
match self {
Self::Iouring(vfs) => vfs.lock_shared(path).await,
Self::Tokio(vfs) => vfs.lock_shared(path).await,
}
}
fn root_path(&self) -> Option<&Path> {
match self {
Self::Iouring(vfs) => vfs.root_path(),
Self::Tokio(vfs) => Vfs::root_path(vfs),
}
}
}
impl VfsFile for NativeFile {
async fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
match self {
Self::Iouring(file) => file.read_at(offset, buf).await,
Self::Tokio(file) => file.read_at(offset, buf).await,
}
}
async fn read_at_vectored(&self, reqs: &mut [ReadReq<'_>]) -> Result<()> {
match self {
Self::Iouring(file) => file.read_at_vectored(reqs).await,
Self::Tokio(file) => file.read_at_vectored(reqs).await,
}
}
async fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize> {
match self {
Self::Iouring(file) => file.write_at(offset, buf).await,
Self::Tokio(file) => file.write_at(offset, buf).await,
}
}
async fn write_at_vectored(&mut self, reqs: &[WriteReq<'_>]) -> Result<()> {
match self {
Self::Iouring(file) => file.write_at_vectored(reqs).await,
Self::Tokio(file) => file.write_at_vectored(reqs).await,
}
}
async fn sync(&mut self) -> Result<()> {
match self {
Self::Iouring(file) => file.sync().await,
Self::Tokio(file) => file.sync().await,
}
}
async fn truncate(&mut self, len: u64) -> Result<()> {
match self {
Self::Iouring(file) => file.truncate(len).await,
Self::Tokio(file) => file.truncate(len).await,
}
}
async fn set_len(&mut self, len: u64) -> Result<()> {
match self {
Self::Iouring(file) => file.set_len(len).await,
Self::Tokio(file) => file.set_len(len).await,
}
}
async fn len(&self) -> Result<u64> {
match self {
Self::Iouring(file) => file.len().await,
Self::Tokio(file) => file.len().await,
}
}
async fn is_empty(&self) -> Result<bool> {
match self {
Self::Iouring(file) => file.is_empty().await,
Self::Tokio(file) => file.is_empty().await,
}
}
fn supports_direct_io(&self) -> bool {
match self {
Self::Iouring(file) => file.supports_direct_io(),
Self::Tokio(file) => file.supports_direct_io(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tempdir() -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
let mut path = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let sequence = NEXT.fetch_add(1, Ordering::Relaxed);
path.push(format!(
"pagedb-native-unit-{}-{nanos}-{sequence}",
std::process::id()
));
std::fs::create_dir(&path).unwrap();
path
}
#[tokio::test(flavor = "current_thread")]
async fn the_fallback_arm_delegates_every_file_operation() {
let dir = tempdir();
let vfs = NativeVfs::Tokio(TokioVfs::new(&dir));
assert!(!vfs.uses_iouring());
assert_eq!(vfs.root_path(), Some(dir.as_path()));
vfs.mkdir_all("/seg").await.unwrap();
let mut file = vfs.open("/seg/page", OpenMode::CreateNew).await.unwrap();
file.write_at(0, b"pagedb").await.unwrap();
file.sync().await.unwrap();
assert_eq!(file.len().await.unwrap(), 6);
assert!(!file.is_empty().await.unwrap());
file.write_at_vectored(&[WriteReq {
offset: 8,
buf: b"tail",
}])
.await
.unwrap();
let mut head = [0u8; 6];
let mut tail = [0u8; 4];
file.read_at_vectored(&mut [
ReadReq {
offset: 0,
buf: &mut head,
},
ReadReq {
offset: 8,
buf: &mut tail,
},
])
.await
.unwrap();
assert_eq!(&head, b"pagedb");
assert_eq!(&tail, b"tail");
file.set_len(6).await.unwrap();
assert_eq!(file.len().await.unwrap(), 6);
file.truncate(0).await.unwrap();
assert!(file.is_empty().await.unwrap());
assert!(!file.supports_direct_io());
drop(file);
vfs.rename("/seg/page", "/seg/renamed").await.unwrap();
assert_eq!(vfs.list_dir("/seg").await.unwrap(), vec!["renamed"]);
vfs.sync_dir("/seg").await.unwrap();
vfs.remove("/seg/renamed").await.unwrap();
assert!(vfs.list_dir("/seg").await.unwrap().is_empty());
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test(flavor = "current_thread")]
async fn the_fallback_arm_locks_through_the_shared_handle() {
let dir = tempdir();
let vfs = NativeVfs::Tokio(TokioVfs::new(&dir));
let held = vfs.lock_exclusive("/db").await.unwrap();
assert!(vfs.lock_shared("/db").await.is_err());
drop(held);
let shared_first = vfs.lock_shared("/db").await.unwrap();
let shared_second = vfs.lock_shared("/db").await.unwrap();
assert!(vfs.lock_exclusive("/db").await.is_err());
drop(shared_first);
drop(shared_second);
vfs.lock_exclusive("/db").await.unwrap();
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test(flavor = "current_thread")]
async fn open_native_always_yields_a_working_vfs() {
let dir = tempdir();
let vfs = open_native(&dir);
let mut file = vfs.open("/probe", OpenMode::CreateNew).await.unwrap();
file.write_at(0, b"ok").await.unwrap();
file.sync().await.unwrap();
drop(file);
let reopened = vfs.open("/probe", OpenMode::Read).await.unwrap();
let mut buf = [0u8; 2];
assert_eq!(reopened.read_at(0, &mut buf).await.unwrap(), 2);
assert_eq!(&buf, b"ok");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test(flavor = "current_thread")]
async fn the_two_backends_exclude_each_other_on_the_same_path() {
let dir = tempdir();
let Ok(iouring) = IouringVfs::new(&dir) else {
std::fs::remove_dir_all(&dir).ok();
return;
};
let iouring = NativeVfs::Iouring(iouring);
let tokio = NativeVfs::Tokio(TokioVfs::new(&dir));
let held = iouring.lock_exclusive("/db").await.unwrap();
assert!(
matches!(
tokio.lock_exclusive("/db").await,
Err(crate::errors::PagedbError::AlreadyLocked)
),
"the thread-pool backend must not be able to take a lock the \
io_uring backend already holds"
);
drop(held);
let held = tokio.lock_exclusive("/db").await.unwrap();
assert!(matches!(
iouring.lock_exclusive("/db").await,
Err(crate::errors::PagedbError::AlreadyLocked)
));
drop(held);
std::fs::remove_dir_all(&dir).ok();
}
}