use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
pub const DIR_NAME: &str = ".nordisk-disktest";
pub const FILE_BYTES: u64 = 64 << 20;
pub const HOLD_HELLO: Duration = Duration::from_secs(300);
const CHUNK: usize = 4 << 20;
pub fn measure(data_root: &Path) -> Result<u32, String> {
let dir = data_root.join(DIR_NAME);
let file = dir.join("seq-write");
let run = || -> std::io::Result<Duration> {
std::fs::create_dir_all(&dir)?;
let mut f = std::fs::File::create(&file)?;
let chunk = vec![0xA5u8; CHUNK];
let t0 = Instant::now();
let mut left = FILE_BYTES;
while left > 0 {
let n = left.min(CHUNK as u64) as usize;
f.write_all(&chunk[..n])?;
left -= n as u64;
}
f.sync_all()?;
Ok(t0.elapsed())
};
let took = run();
let _ = std::fs::remove_file(&file);
let _ = std::fs::remove_dir(&dir);
let took = took.map_err(|e| format!("the start-up disk test on {} failed: {e}", data_root.display()))?;
let mib = (FILE_BYTES >> 20) as f64;
Ok(((mib / took.as_secs_f64().max(1e-6)) as u64).clamp(1, u32::MAX as u64) as u32)
}
#[derive(Debug)]
pub struct DiskTest {
started: Instant,
result: OnceLock<Result<u32, String>>,
}
impl DiskTest {
pub fn pending(started: Instant) -> DiskTest {
DiskTest { started, result: OnceLock::new() }
}
pub fn start(data_root: PathBuf, say: impl Fn(&str) + Send + 'static) -> Arc<DiskTest> {
let t = Arc::new(DiskTest::pending(Instant::now()));
let done = Arc::clone(&t);
let spawned = std::thread::Builder::new().name("disktest".into()).spawn(move || {
let r = measure(&data_root);
say(&match &r {
Ok(v) => format!("disktest {}: seq write {v} MiB/s ({} MiB, fsync'd) — the election reads it", data_root.display(), FILE_BYTES >> 20),
Err(e) => format!("disktest: {e} — the election reads 0 MiB/s"),
});
done.finish(r);
});
if let Err(e) = spawned {
t.finish(Err(format!("the start-up disk test could not start its thread: {e}")));
}
t
}
pub fn finish(&self, r: Result<u32, String>) {
let _ = self.result.set(r);
}
pub fn result(&self) -> Option<&Result<u32, String>> {
self.result.get()
}
pub fn disk_mib_s(&self) -> u32 {
match self.result.get() {
Some(Ok(v)) => *v,
_ => 0,
}
}
pub fn hold_hello(&self, now: Instant) -> Result<(), String> {
if self.result.get().is_none() && now.saturating_duration_since(self.started) < HOLD_HELLO {
return Err("the start-up disk test has not finished; the election compares its numbers — ask again".into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_disk_test_feeds_the_election_and_holds_hello_until_done() {
let root = std::env::temp_dir().join(format!("disktest-{}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
let v = measure(&root).unwrap();
assert!(v >= 1, "{v}");
assert!(!root.join(DIR_NAME).exists(), "nothing left behind");
let t0 = Instant::now();
let t = DiskTest::pending(t0);
assert!(t.hold_hello(t0 + Duration::from_secs(1)).is_err(), "running: ask again");
assert!(t.hold_hello(t0 + HOLD_HELLO).is_ok(), "the hold ran out");
assert_eq!(t.disk_mib_s(), 0);
t.finish(Ok(812));
assert!(t.hold_hello(t0).is_ok());
assert_eq!(t.disk_mib_s(), 812);
assert!(measure(&root.join("absent/deeper\0bad")).is_err(), "a failure says why");
let started = DiskTest::start(root.clone(), |_| {});
for _ in 0..200 {
if started.result().is_some() {
break;
}
std::thread::sleep(Duration::from_millis(20));
}
assert!(started.disk_mib_s() >= 1);
let _ = std::fs::remove_dir_all(&root);
}
}