use std::collections::VecDeque;
use std::path::PathBuf;
use std::fs::File;
use wireshift::ops::{OpenAt, Read, Statx};
use wireshift::{Request, Ring, RingConfig};
use crate::error::{Error, Result};
use crate::sample::{Sample, Tensor};
use crate::source::{Source, SourceIterator};
pub struct RingSource {
paths: Vec<PathBuf>,
pattern: String,
batch_size: usize,
ring_config: RingConfig,
}
impl RingSource {
pub fn from_glob(pattern: &str, batch_size: usize) -> Result<Self> {
let paths = super::collect_glob_files(pattern)?;
if paths.is_empty() {
return Err(Error::EmptySource {
pattern: pattern.to_string(),
});
}
tracing::info!(
"RingSource: found {} files matching '{}' (batch_size={})",
paths.len(),
pattern,
batch_size
);
Ok(Self {
paths,
pattern: pattern.to_string(),
batch_size: batch_size.max(1),
ring_config: RingConfig::default(),
})
}
pub fn from_paths(paths: Vec<PathBuf>, batch_size: usize) -> Result<Self> {
if paths.is_empty() {
return Err(Error::EmptySource {
pattern: "<explicit paths>".to_string(),
});
}
Ok(Self {
pattern: format!("{} explicit paths", paths.len()),
paths,
batch_size: batch_size.max(1),
ring_config: RingConfig::default(),
})
}
pub fn with_ring_config(mut self, config: RingConfig) -> Self {
self.ring_config = config;
self
}
}
impl Source for RingSource {
fn open(&self) -> Result<Box<dyn SourceIterator>> {
let ring = Ring::new(self.ring_config.clone()).map_err(|error| Error::SourceFailed {
source_name: self.pattern.clone(),
reason: format!("failed to create santh-io ring: {error}"),
})?;
tracing::debug!("RingSource opened with backend {:?}", ring.backend_kind());
Ok(Box::new(RingIterator {
paths: self.paths.clone(),
ring,
index: 0,
batch_size: self.batch_size,
pending: VecDeque::new(),
}))
}
fn len_hint(&self) -> Option<u64> {
Some(self.paths.len() as u64)
}
fn name(&self) -> &str {
&self.pattern
}
}
enum ReadState {
Submitted(Request<wireshift::ops::ReadResult>),
Error(Error),
}
struct PendingOpen {
request: Request<File>,
path: PathBuf,
index: u64,
read_len: usize,
}
struct PendingStat {
request: Request<wireshift::FileStat>,
path: PathBuf,
index: u64,
}
struct PendingRead {
state: ReadState,
path: PathBuf,
index: u64,
}
struct RingIterator {
paths: Vec<PathBuf>,
ring: Ring,
index: u64,
batch_size: usize,
pending: VecDeque<PendingRead>,
}
impl RingIterator {
fn submit_batch(&mut self) -> Result<()> {
let start = self.index as usize;
let end = (start + self.batch_size).min(self.paths.len());
let mut pending_stats = Vec::with_capacity(end.saturating_sub(start));
let mut pending_opens = Vec::with_capacity(end.saturating_sub(start));
for idx in start..end {
let path = &self.paths[idx];
match self.ring.submit(Statx::new(path)) {
Ok(request) => pending_stats.push(PendingStat {
request,
path: path.clone(),
index: idx as u64,
}),
Err(error) => self.pending.push_back(PendingRead {
state: ReadState::Error(Error::SourceFailed {
source_name: path.to_string_lossy().to_string(),
reason: format!("failed to submit statx: {error}"),
}),
path: path.clone(),
index: idx as u64,
}),
}
}
for pending_stat in pending_stats {
let read_len = match pending_stat
.request
.wait(Some(std::time::Duration::from_secs(30)))
{
Ok(stat) => match usize::try_from(stat.size) {
Ok(size) => size,
Err(_) => {
self.pending.push_back(PendingRead {
state: ReadState::Error(Error::SourceFailed {
source_name: pending_stat.path.to_string_lossy().to_string(),
reason: "file is too large to fit in an in-memory read buffer. Fix: shard the file or stream it in smaller ranges.".to_string(),
}),
path: pending_stat.path,
index: pending_stat.index,
});
continue;
}
},
Err(error) => {
self.pending.push_back(PendingRead {
state: ReadState::Error(Error::ReadFailed {
path: pending_stat.path.clone(),
reason: error.to_string(),
}),
path: pending_stat.path,
index: pending_stat.index,
});
continue;
}
};
match self.ring.submit(OpenAt::read_only(&pending_stat.path)) {
Ok(request) => pending_opens.push(PendingOpen {
request,
path: pending_stat.path,
index: pending_stat.index,
read_len,
}),
Err(error) => self.pending.push_back(PendingRead {
state: ReadState::Error(Error::SourceFailed {
source_name: pending_stat.path.to_string_lossy().to_string(),
reason: format!("failed to submit openat: {error}"),
}),
path: pending_stat.path,
index: pending_stat.index,
}),
}
}
for pending_open in pending_opens {
let state = match pending_open
.request
.wait(Some(std::time::Duration::from_secs(30)))
{
Ok(file) => match Read::new(file, 0, pending_open.read_len) {
Ok(op) => match self.ring.submit(op) {
Ok(request) => ReadState::Submitted(request),
Err(error) => ReadState::Error(Error::SourceFailed {
source_name: pending_open.path.to_string_lossy().to_string(),
reason: format!("failed to submit read after openat: {error}"),
}),
},
Err(error) => ReadState::Error(Error::SourceFailed {
source_name: pending_open.path.to_string_lossy().to_string(),
reason: format!("failed to create read operation after openat: {error}"),
}),
},
Err(error) => ReadState::Error(Error::ReadFailed {
path: pending_open.path.clone(),
reason: error.to_string(),
}),
};
self.pending.push_back(PendingRead {
state,
path: pending_open.path,
index: pending_open.index,
});
}
self.index = end as u64;
Ok(())
}
fn next_completion(&mut self) -> Option<Result<Sample>> {
if self.pending.is_empty() {
return None;
}
let pending = self.pending.pop_front()?;
let result = match pending.state {
ReadState::Submitted(request) => request
.wait(Some(std::time::Duration::from_secs(30)))
.map_err(|error| Error::ReadFailed {
path: pending.path.clone(),
reason: error.to_string(),
}),
ReadState::Error(error) => Err(error),
};
match result {
Ok(read_result) => {
let filename = match pending.path.file_name() {
Some(n) => n.to_string_lossy().to_string(),
None => String::new(),
};
Some(Ok(Sample::new()
.with("data", Tensor::bytes_from_completed(read_result.buffer))
.with("filename", Tensor::bytes(filename.into_bytes()))
.with_metadata(
pending.path.to_string_lossy(),
pending.index,
)))
}
Err(error) => Some(Err(error)),
}
}
}
impl SourceIterator for RingIterator {
fn next_sample(&mut self) -> Option<Result<Sample>> {
if self.pending.is_empty() {
if (self.index as usize) >= self.paths.len() {
return None;
}
if let Err(error) = self.submit_batch() {
return Some(Err(error));
}
}
self.next_completion()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "uring")]
#[test]
fn ring_source_direct_iteration_reads_all_paths() {
let dir = tempfile::tempdir().expect("tempdir");
for idx in 0..10 {
std::fs::write(dir.path().join(format!("f{idx}.bin")), vec![42_u8; 4096])
.expect("write test file");
}
let pattern = format!("{}/*.bin", dir.path().display());
let source = RingSource::from_glob(&pattern, 1).expect("source");
let mut iter = source.open().expect("open");
let mut total = 0usize;
while let Some(sample) = iter.next_sample() {
let sample = sample.expect("sample");
assert_eq!(
sample.get("data").expect("data field").as_bytes().len(),
4096
);
total += 1;
}
assert_eq!(total, 10);
}
}