diff options
Diffstat (limited to 'src/files.rs')
| -rw-r--r-- | src/files.rs | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/src/files.rs b/src/files.rs new file mode 100644 index 0000000..58fd043 --- /dev/null +++ b/src/files.rs @@ -0,0 +1,213 @@ +//! The files this node reads and writes for itself, and how much it did (`/admin/stats`, +//! `disk`): the state file (`swarm::state_file`), the document file +//! (`document::file`) and a backup to recover from (`swarm::bootstrap`). Not the +//! Mycelium daemon's files, configuration or logs. +//! +//! Writes replace a file atomically: a temporary file next to it, flushed to disk, then +//! renamed over it, so a crash leaves the old content or the new one. Appends add to the +//! end and are flushed to disk before they count as done; a crash may leave the last one +//! cut short, which the file's reader has to expect. + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, MutexGuard}; + +/// What a file is, for the counts. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FileKind { + State, + Document, + Backup, + /// The input of a genesis remake (`--genesis --remake`). + Remake, +} + +impl FileKind { + pub(crate) const ALL: [FileKind; 4] = [ + FileKind::State, + FileKind::Document, + FileKind::Backup, + FileKind::Remake, + ]; + + pub(crate) fn as_str(self) -> &'static str { + match self { + FileKind::State => "state", + FileKind::Document => "document", + FileKind::Backup => "backup", + FileKind::Remake => "remake", + } + } + + fn index(self) -> usize { + self as usize + } +} + +/// Counts for one kind of file, since the node started. +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct Counts { + pub(crate) reads: u64, + pub(crate) read_bytes: u64, + /// Whole files written. + pub(crate) writes: u64, + pub(crate) written_bytes: u64, + pub(crate) appends: u64, + pub(crate) appended_bytes: u64, + /// Reads, writes or appends that failed (a missing file read is no failure). + pub(crate) errors: u64, +} + +/// Reads and writes this node's files, counting per kind. +#[derive(Default)] +pub(crate) struct Files { + // A std mutex: every access is short and never spans an await. + counts: Mutex<[Counts; FileKind::ALL.len()]>, +} + +impl Files { + fn lock(&self) -> MutexGuard<'_, [Counts; FileKind::ALL.len()]> { + self.counts + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } + + fn count(&self, kind: FileKind, f: impl FnOnce(&mut Counts)) { + f(&mut self.lock()[kind.index()]); + } + + /// The file's content; None if it doesn't exist. + pub(crate) fn read(&self, kind: FileKind, path: &Path) -> std::io::Result<Option<Vec<u8>>> { + match std::fs::read(path) { + Ok(bytes) => { + self.count(kind, |counts| { + counts.reads += 1; + counts.read_bytes += bytes.len() as u64; + }); + Ok(Some(bytes)) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => { + self.count(kind, |counts| counts.errors += 1); + Err(error) + } + } + } + + /// Replaces the file's content atomically (see the module docs). + pub(crate) fn write_atomic( + &self, + kind: FileKind, + path: &Path, + bytes: &[u8], + ) -> std::io::Result<()> { + let result = write_atomic(path, bytes); + self.count(kind, |counts| match &result { + Ok(()) => { + counts.writes += 1; + counts.written_bytes += bytes.len() as u64; + } + Err(_) => counts.errors += 1, + }); + result + } + + /// Adds `bytes` to the end of the file (which must exist), flushed to disk. + pub(crate) fn append(&self, kind: FileKind, path: &Path, bytes: &[u8]) -> std::io::Result<()> { + let result = (|| { + let mut file = std::fs::OpenOptions::new().append(true).open(path)?; + file.write_all(bytes)?; + file.sync_data() + })(); + self.count(kind, |counts| match &result { + Ok(()) => { + counts.appends += 1; + counts.appended_bytes += bytes.len() as u64; + } + Err(_) => counts.errors += 1, + }); + result + } + + /// Moves an unusable file aside (`<path><suffix>`), returning where to. + pub(crate) fn move_aside(&self, path: &Path, suffix: &str) -> std::io::Result<PathBuf> { + let aside = with_suffix(path, suffix); + std::fs::rename(path, &aside)?; + Ok(aside) + } + + /// The counts per kind. + pub(crate) fn counts(&self) -> Vec<(FileKind, Counts)> { + let counts = *self.lock(); + FileKind::ALL + .into_iter() + .map(|kind| (kind, counts[kind.index()])) + .collect() + } +} + +fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> { + let temporary = with_suffix(path, ".tmp"); + { + let mut file = std::fs::File::create(&temporary)?; + file.write_all(bytes)?; + file.sync_all()?; + } + std::fs::rename(&temporary, path)?; + sync_parent(path); + Ok(()) +} + +/// Makes a rename in `path`'s directory durable (best effort: not every platform syncs +/// directories). +fn sync_parent(path: &Path) { + if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) { + if let Ok(dir) = std::fs::File::open(dir) { + let _ = dir.sync_all(); + } + } +} + +fn with_suffix(path: &Path, suffix: &str) -> PathBuf { + let mut name = path.as_os_str().to_owned(); + name.push(suffix); + PathBuf::from(name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn writes_reads_and_counts() { + let dir = std::env::temp_dir().join(format!("continuum-files-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("state.json"); + let files = Files::default(); + + assert_eq!(files.read(FileKind::State, &path).unwrap(), None); + files.write_atomic(FileKind::State, &path, b"one").unwrap(); + files + .write_atomic(FileKind::State, &path, b"three") + .unwrap(); + assert_eq!( + files.read(FileKind::State, &path).unwrap().as_deref(), + Some(&b"three"[..]) + ); + assert!(!with_suffix(&path, ".tmp").exists()); + + let (kind, state) = files.counts()[0]; + assert_eq!(kind, FileKind::State); + assert_eq!((state.writes, state.written_bytes), (2, 8)); + assert_eq!((state.reads, state.read_bytes, state.errors), (1, 5, 0)); + files.append(FileKind::State, &path, b"!").unwrap(); + assert_eq!( + files.read(FileKind::State, &path).unwrap().as_deref(), + Some(&b"three!"[..]) + ); + assert_eq!(files.counts()[0].1.appended_bytes, 1); + assert_eq!(files.counts()[1].1.reads, 0); + + std::fs::remove_dir_all(&dir).unwrap(); + } +} |
