Skip to main content

git_policy_cli/
journal_record.rs

1//! What:
2//!  One journal record written whole, followed by exactly the syncs its durability level names.
3//! Why:
4//!  Recovery reads each record of a transaction directory, and the tearing table in
5//!  `doc/planning/cli-git-spawn-and-sync-reduction.md` shows which records a power loss may lose or tear without any
6//!  change of outcome and which must reach the disk before the next step. Naming the level at the call site keeps
7//!  every record's sync decision visible, and the budget tests pin the resulting counts.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! await writeRecord({ directory, filename, bytes, sync: 'nothing' });
12//! ```
13
14/// `Path` is a borrowed filesystem path; `PathBuf` is the owned one.
15use std::path::{Path, PathBuf};
16/// Whole-or-absent record writes, with and without the sync.
17use super::private_storage::{sync_directory, write_private_file, write_private_file_unsynced};
18
19/// What:
20///  Which syncs follow a journal record's write.
21/// Why:
22///   A record published by link is whole or absent after a kill whatever the level; the level only decides what a
23///  power loss may leave.
24///
25/// In TS you'd write (pseudocode):
26/// ```ts
27/// type RecordSync = 'file-and-directory' | 'file' | 'nothing';
28/// ```
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum RecordSync {
31    /// The file's bytes, then its directory entry: the record is a barrier that later steps rely on.
32    FileAndDirectory,
33    /// The file's bytes only: a later record's directory sync makes the entry durable before anything depends on it,
34    ///  and a name a power loss took anyway leaves the state of a record that was never written.
35    FileOnly,
36    /// Neither: recovery completes or discards the transaction whatever a power loss left of this record.
37    Nothing,
38}
39
40/// What:
41///  Write `filename` in `directory` exclusively and whole, then sync as `sync` says.
42///  Returns the failure as a sentence that names the path.
43/// Why:
44///   The callers report a failed write as a landing or transaction failure and differ only in the type that wraps
45///  the sentence.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// await writeRecord({ directory, filename, bytes, sync });
50/// ```
51pub fn write_record(directory: &Path, filename: &str, bytes: &[u8], sync: RecordSync) -> Result<(), String> {
52    let path: PathBuf = directory.join(filename);
53    let written: std::io::Result<()> = match sync {
54        RecordSync::FileAndDirectory | RecordSync::FileOnly => write_private_file(path.as_path(), bytes),
55        RecordSync::Nothing => write_private_file_unsynced(path.as_path(), bytes),
56    };
57    if let Err(error) = written {
58        return Err(format!("writing {} failed: {error}", path.display()));
59    }
60    if sync == RecordSync::FileAndDirectory
61        && let Err(error) = sync_directory(directory)
62    {
63        return Err(format!("syncing {} failed: {error}", directory.display()));
64    }
65    return Ok(());
66}
67
68/// Journal record controls stay out of the release executable.
69#[cfg(test)]
70#[path = "journal_record_tests.rs"]
71mod tests;