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