use std::fs::File;
use std::io;
use std::io::BufRead;
use std::io::BufReader;
use std::io::BufWriter;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::path::Path;
use lasso::Spur;
use tempfile::NamedTempFile;
pub fn partition<T>(v: Vec<T>, n: usize) -> Vec<Vec<T>> {
let n = n.max(1);
let chunk = v.len() / n;
let mut iter = v.into_iter();
(0..n)
.map(|i| {
let take = if i + 1 == n { iter.len() } else { chunk };
iter.by_ref().take(take).collect()
})
.collect()
}
pub fn byte_range_reader(
path: &Path,
index: usize,
peers: usize,
) -> Option<(BufReader<File>, u64)> {
let mut file = File::open(path)
.inspect_err(|e| {
eprintln!(
"[flowlog-runtime::io] failed to open {}: {e}",
path.display()
);
})
.ok()?;
let file_size = file
.metadata()
.inspect_err(|e| {
eprintln!(
"[flowlog-runtime::io] failed to stat {}: {e}",
path.display()
);
})
.ok()?
.len();
let chunk = file_size / peers as u64;
let start = chunk * index as u64;
let end = if index == peers - 1 {
file_size
} else {
chunk * (index + 1) as u64
};
if start >= end {
return Some((BufReader::new(file), 0));
}
if start == 0 {
return Some((BufReader::new(file), end));
}
if file.seek(SeekFrom::Start(start - 1)).is_err() {
return Some((BufReader::new(file), 0));
}
let mut reader = BufReader::new(file);
let mut peek = [0u8; 1];
if reader.read_exact(&mut peek).is_err() {
return Some((reader, 0));
}
if peek[0] == b'\n' {
return Some((reader, end - start));
}
let mut discard = Vec::new();
let skipped = reader.read_until(b'\n', &mut discard).unwrap_or(0);
Some((reader, (end - start).saturating_sub(skipped as u64)))
}
#[inline]
pub fn shard_int(first: i64, peers: usize, index: usize) -> bool {
first.rem_euclid(peers as i64) as usize == index
}
#[inline]
pub fn shard_str(first: &str, peers: usize, index: usize) -> bool {
let mut hash: u32 = 0x811c9dc5;
for &b in first.as_bytes() {
hash ^= b as u32;
hash = hash.wrapping_mul(0x01000193);
}
(hash as usize) % peers == index
}
#[inline]
pub fn shard_spur(first: Spur, peers: usize, index: usize) -> bool {
(first.into_inner().get() as usize) % peers == index
}
pub fn write_atomic(
path: impl AsRef<Path>,
write: impl FnOnce(&mut dyn Write) -> io::Result<()>,
) -> io::Result<()> {
let path = path.as_ref();
let mut tmp = match path.parent().filter(|p| !p.as_os_str().is_empty()) {
Some(dir) => NamedTempFile::new_in(dir)?,
None => NamedTempFile::new()?,
};
{
let mut buf = BufWriter::new(&mut tmp);
write(&mut buf)?;
buf.flush()?;
}
tmp.persist(path).map_err(|e| e.error)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_atomic_persists_content_and_leaves_no_temp() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("out.log");
write_atomic(&path, |w| write!(w, "hello")).expect("write");
assert_eq!(std::fs::read_to_string(&path).expect("read"), "hello");
let names: Vec<_> = std::fs::read_dir(dir.path())
.expect("read dir")
.map(|e| e.expect("entry").file_name())
.collect();
assert_eq!(
names.len(),
1,
"only the persisted file should remain: {names:?}"
);
}
#[test]
fn write_atomic_overwrites_existing() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("out.log");
write_atomic(&path, |w| write!(w, "first")).expect("first");
write_atomic(&path, |w| write!(w, "second")).expect("second");
assert_eq!(std::fs::read_to_string(&path).expect("read"), "second");
}
#[test]
fn write_atomic_failed_write_preserves_existing() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("out.log");
write_atomic(&path, |w| write!(w, "original")).expect("seed");
let err = write_atomic(&path, |w| {
write!(w, "partial")?;
Err(io::Error::other("boom"))
})
.expect_err("closure error must propagate");
assert_eq!(err.to_string(), "boom");
assert_eq!(std::fs::read_to_string(&path).expect("read"), "original");
let names: Vec<_> = std::fs::read_dir(dir.path())
.expect("read dir")
.map(|e| e.expect("entry").file_name())
.collect();
assert_eq!(
names.len(),
1,
"temp sibling should be cleaned up: {names:?}"
);
}
}