use crate::error::{Result, TdbError};
use bytes::Bytes;
use futures::future::BoxFuture;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AsyncIoBackend {
#[default]
Auto,
Tokio,
#[cfg(target_os = "linux")]
IoUring,
}
impl AsyncIoBackend {
pub fn select_best() -> Self {
#[cfg(all(target_os = "linux", feature = "tokio-uring"))]
{
Self::IoUring
}
#[cfg(not(all(target_os = "linux", feature = "tokio-uring")))]
{
Self::Tokio
}
}
}
pub struct AsyncFileHandle {
path: PathBuf,
backend: AsyncIoBackend,
tokio_file: Option<Arc<tokio::sync::Mutex<File>>>,
stats: Arc<parking_lot::Mutex<AsyncIoStats>>,
}
#[derive(Debug, Clone, Default)]
pub struct AsyncIoStats {
pub total_reads: u64,
pub total_writes: u64,
pub bytes_read: u64,
pub bytes_written: u64,
pub total_syncs: u64,
pub backend: Option<String>,
}
impl AsyncFileHandle {
pub async fn open<P: AsRef<Path>>(path: P, backend: AsyncIoBackend) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let backend = if backend == AsyncIoBackend::Auto {
AsyncIoBackend::select_best()
} else {
backend
};
let stats = AsyncIoStats {
backend: Some(format!("{:?}", backend)),
..Default::default()
};
match backend {
AsyncIoBackend::Tokio => {
let file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false) .open(&path)
.await
.map_err(TdbError::Io)?;
Ok(Self {
path,
backend,
tokio_file: Some(Arc::new(tokio::sync::Mutex::new(file))),
stats: Arc::new(parking_lot::Mutex::new(stats)),
})
}
#[cfg(target_os = "linux")]
AsyncIoBackend::IoUring => {
Self::open(path, AsyncIoBackend::Tokio).await
}
AsyncIoBackend::Auto => unreachable!("Auto should have been resolved"),
}
}
pub async fn create<P: AsRef<Path>>(path: P, backend: AsyncIoBackend) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let backend = if backend == AsyncIoBackend::Auto {
AsyncIoBackend::select_best()
} else {
backend
};
let stats = AsyncIoStats {
backend: Some(format!("{:?}", backend)),
..Default::default()
};
match backend {
AsyncIoBackend::Tokio => {
let file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true) .open(&path)
.await
.map_err(TdbError::Io)?;
Ok(Self {
path,
backend,
tokio_file: Some(Arc::new(tokio::sync::Mutex::new(file))),
stats: Arc::new(parking_lot::Mutex::new(stats)),
})
}
#[cfg(target_os = "linux")]
AsyncIoBackend::IoUring => {
Box::pin(Self::create(path, AsyncIoBackend::Tokio)).await
}
AsyncIoBackend::Auto => unreachable!(),
}
}
pub async fn read_at(&self, buffer: &mut [u8], offset: u64) -> Result<usize> {
match self.backend {
AsyncIoBackend::Tokio => {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let mut file_guard = file.lock().await;
file_guard
.seek(std::io::SeekFrom::Start(offset))
.await
.map_err(TdbError::Io)?;
let bytes_read = file_guard.read(buffer).await.map_err(TdbError::Io)?;
let mut stats = self.stats.lock();
stats.total_reads += 1;
stats.bytes_read += bytes_read as u64;
Ok(bytes_read)
}
#[cfg(target_os = "linux")]
AsyncIoBackend::IoUring => {
self.read_at_tokio(buffer, offset).await
}
AsyncIoBackend::Auto => unreachable!(),
}
}
async fn read_at_tokio(&self, buffer: &mut [u8], offset: u64) -> Result<usize> {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let mut file_guard = file.lock().await;
file_guard
.seek(std::io::SeekFrom::Start(offset))
.await
.map_err(TdbError::Io)?;
let bytes_read = file_guard.read(buffer).await.map_err(TdbError::Io)?;
let mut stats = self.stats.lock();
stats.total_reads += 1;
stats.bytes_read += bytes_read as u64;
Ok(bytes_read)
}
pub async fn write_at(&self, data: &[u8], offset: u64) -> Result<usize> {
match self.backend {
AsyncIoBackend::Tokio => {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let mut file_guard = file.lock().await;
file_guard
.seek(std::io::SeekFrom::Start(offset))
.await
.map_err(TdbError::Io)?;
let bytes_written = file_guard.write(data).await.map_err(TdbError::Io)?;
let mut stats = self.stats.lock();
stats.total_writes += 1;
stats.bytes_written += bytes_written as u64;
Ok(bytes_written)
}
#[cfg(target_os = "linux")]
AsyncIoBackend::IoUring => {
self.write_at_tokio(data, offset).await
}
AsyncIoBackend::Auto => unreachable!(),
}
}
async fn write_at_tokio(&self, data: &[u8], offset: u64) -> Result<usize> {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let mut file_guard = file.lock().await;
file_guard
.seek(std::io::SeekFrom::Start(offset))
.await
.map_err(TdbError::Io)?;
let bytes_written = file_guard.write(data).await.map_err(TdbError::Io)?;
let mut stats = self.stats.lock();
stats.total_writes += 1;
stats.bytes_written += bytes_written as u64;
Ok(bytes_written)
}
pub async fn sync_all(&self) -> Result<()> {
match self.backend {
AsyncIoBackend::Tokio => {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let file_guard = file.lock().await;
file_guard.sync_all().await.map_err(TdbError::Io)?;
let mut stats = self.stats.lock();
stats.total_syncs += 1;
Ok(())
}
#[cfg(target_os = "linux")]
AsyncIoBackend::IoUring => {
self.sync_all_tokio().await
}
AsyncIoBackend::Auto => unreachable!(),
}
}
async fn sync_all_tokio(&self) -> Result<()> {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let file_guard = file.lock().await;
file_guard.sync_all().await.map_err(TdbError::Io)?;
let mut stats = self.stats.lock();
stats.total_syncs += 1;
Ok(())
}
pub async fn len(&self) -> Result<u64> {
match self.backend {
AsyncIoBackend::Tokio => {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let file_guard = file.lock().await;
let metadata = file_guard.metadata().await.map_err(TdbError::Io)?;
Ok(metadata.len())
}
#[cfg(target_os = "linux")]
AsyncIoBackend::IoUring => {
self.len_tokio().await
}
AsyncIoBackend::Auto => unreachable!(),
}
}
async fn len_tokio(&self) -> Result<u64> {
let file = self
.tokio_file
.as_ref()
.ok_or_else(|| TdbError::Other("No tokio file handle".to_string()))?;
let file_guard = file.lock().await;
let metadata = file_guard.metadata().await.map_err(TdbError::Io)?;
Ok(metadata.len())
}
pub async fn is_empty(&self) -> Result<bool> {
Ok(self.len().await? == 0)
}
pub fn stats(&self) -> AsyncIoStats {
self.stats.lock().clone()
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn backend(&self) -> AsyncIoBackend {
self.backend
}
}
pub struct AsyncIoBatch {
operations: Vec<AsyncIoOperation>,
file: Arc<AsyncFileHandle>,
}
pub enum AsyncIoOperation {
Read {
offset: u64,
length: usize,
},
Write {
offset: u64,
data: Bytes,
},
}
impl AsyncIoBatch {
pub fn new(file: Arc<AsyncFileHandle>) -> Self {
Self {
operations: Vec::new(),
file,
}
}
pub fn add_read(&mut self, offset: u64, length: usize) {
self.operations
.push(AsyncIoOperation::Read { offset, length });
}
pub fn add_write(&mut self, offset: u64, data: Bytes) {
self.operations
.push(AsyncIoOperation::Write { offset, data });
}
pub async fn execute(self) -> Result<Vec<Result<Vec<u8>>>> {
let mut results = Vec::new();
for op in self.operations {
match op {
AsyncIoOperation::Read { offset, length } => {
let mut buffer = vec![0u8; length];
match self.file.read_at(&mut buffer, offset).await {
Ok(bytes_read) => {
buffer.truncate(bytes_read);
results.push(Ok(buffer));
}
Err(e) => results.push(Err(e)),
}
}
AsyncIoOperation::Write { offset, data } => {
match self.file.write_at(&data, offset).await {
Ok(_) => results.push(Ok(Vec::new())),
Err(e) => results.push(Err(e)),
}
}
}
}
Ok(results)
}
pub fn len(&self) -> usize {
self.operations.len()
}
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[tokio::test]
async fn test_async_file_create() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("test_async_create.dat");
let file = AsyncFileHandle::create(&test_file, AsyncIoBackend::Tokio)
.await
.unwrap();
assert_eq!(file.backend(), AsyncIoBackend::Tokio);
assert!(file.is_empty().await.unwrap());
let _ = tokio::fs::remove_file(&test_file).await;
}
#[tokio::test]
async fn test_async_write_and_read() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("test_async_rw.dat");
let file = AsyncFileHandle::create(&test_file, AsyncIoBackend::Tokio)
.await
.unwrap();
let data = b"Hello, async world!";
let bytes_written = file.write_at(data, 0).await.unwrap();
assert_eq!(bytes_written, data.len());
file.sync_all().await.unwrap();
let mut buffer = vec![0u8; data.len()];
let bytes_read = file.read_at(&mut buffer, 0).await.unwrap();
assert_eq!(bytes_read, data.len());
assert_eq!(&buffer[..], data);
let stats = file.stats();
assert_eq!(stats.total_writes, 1);
assert_eq!(stats.total_reads, 1);
assert_eq!(stats.bytes_written, data.len() as u64);
assert_eq!(stats.bytes_read, data.len() as u64);
let _ = tokio::fs::remove_file(&test_file).await;
}
#[tokio::test]
async fn test_async_file_len() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("test_async_len.dat");
let file = AsyncFileHandle::create(&test_file, AsyncIoBackend::Tokio)
.await
.unwrap();
assert_eq!(file.len().await.unwrap(), 0);
file.write_at(b"test", 0).await.unwrap();
file.sync_all().await.unwrap();
assert_eq!(file.len().await.unwrap(), 4);
let _ = tokio::fs::remove_file(&test_file).await;
}
#[tokio::test]
async fn test_async_io_batch() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("test_async_batch.dat");
let file = Arc::new(
AsyncFileHandle::create(&test_file, AsyncIoBackend::Tokio)
.await
.unwrap(),
);
let mut batch = AsyncIoBatch::new(file.clone());
batch.add_write(0, Bytes::from("Hello"));
batch.add_write(5, Bytes::from("World"));
assert_eq!(batch.len(), 2);
let results = batch.execute().await.unwrap();
assert_eq!(results.len(), 2);
file.sync_all().await.unwrap();
let mut read_batch = AsyncIoBatch::new(file.clone());
read_batch.add_read(0, 5);
read_batch.add_read(5, 5);
let results = read_batch.execute().await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].as_ref().unwrap(), b"Hello");
assert_eq!(results[1].as_ref().unwrap(), b"World");
let _ = tokio::fs::remove_file(&test_file).await;
}
#[tokio::test]
async fn test_backend_selection() {
let backend = AsyncIoBackend::select_best();
#[cfg(all(target_os = "linux", feature = "tokio-uring"))]
assert_eq!(backend, AsyncIoBackend::IoUring);
#[cfg(not(all(target_os = "linux", feature = "tokio-uring")))]
assert_eq!(backend, AsyncIoBackend::Tokio);
}
#[tokio::test]
async fn test_concurrent_operations() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("test_async_concurrent.dat");
let file = Arc::new(
AsyncFileHandle::create(&test_file, AsyncIoBackend::Tokio)
.await
.unwrap(),
);
let mut handles = Vec::new();
for i in 0..10 {
let file_clone = file.clone();
let handle = tokio::spawn(async move {
let data = format!("Data{}", i);
file_clone.write_at(data.as_bytes(), (i * 10) as u64).await
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap().unwrap();
}
let stats = file.stats();
assert_eq!(stats.total_writes, 10);
let _ = tokio::fs::remove_file(&test_file).await;
}
#[tokio::test]
async fn test_multiple_reads_writes() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("test_async_multiple.dat");
let file = AsyncFileHandle::create(&test_file, AsyncIoBackend::Tokio)
.await
.unwrap();
file.write_at(b"AAA", 0).await.unwrap();
file.write_at(b"BBB", 100).await.unwrap();
file.write_at(b"CCC", 200).await.unwrap();
file.sync_all().await.unwrap();
let mut buf1 = vec![0u8; 3];
let mut buf2 = vec![0u8; 3];
let mut buf3 = vec![0u8; 3];
file.read_at(&mut buf1, 0).await.unwrap();
file.read_at(&mut buf2, 100).await.unwrap();
file.read_at(&mut buf3, 200).await.unwrap();
assert_eq!(&buf1, b"AAA");
assert_eq!(&buf2, b"BBB");
assert_eq!(&buf3, b"CCC");
let _ = tokio::fs::remove_file(&test_file).await;
}
}