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;