Skip to main content

git_policy_cli/landing/
service_durable.rs

1//! What:
2//!  Where the landing's own syncs wait: not on the path the person waits on, unless the landing publishes nothing.
3//! Why:
4//!  The owner's rule for the commit protocol, 2026-11-03: "Force at leaving, else background": no forced write while the person
5//!  waits, so a power cut loses that attempt exactly as it does with native Git (staged changes and files are intact, the
6//!  commit is run again), and whatever the command already promised when it left (an error, a withdrawal, a hand-over) was
7//!  forced to disk first. A landing that publishes a commit therefore only queues the syncs of its records (the intent, the
8//!  lock marker, the completion record, the done mark and the directory syncs); one background thread makes them in batches,
9//!  each batch overlapped. A landing that publishes nothing syncs what it wrote before it returns. Git's own object, reference
10//!  and index writes follow `core.fsync` as they do for native Git: the in-process index writer flushes only when
11//!  `core.fsync` names the index (`ConfigSnapshot::hardens_index`).
12//!  Nothing here changes what a killed process leaves: the page cache keeps every write whether or not it was synced.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const landing = beginDeferral({ hardensIndex });
17//! // ... flushFile(file, 'intent-file') queues; flushDir(dir, 'intent-dir') queues ...
18//! landing.settle(published); // true: hand the queue to the background thread; false: sync it now
19//! ```
20
21use std::cell::{Cell, RefCell};
22use std::fs::File;
23use std::sync::OnceLock;
24use std::sync::mpsc::{Receiver, Sender, channel};
25
26/// One sync waiting for its turn: a handle on the file or directory, and what it is for.
27struct Pending {
28    /// A handle on the same file or directory the record lives in; a flush through it flushes the file itself.
29    handle: File,
30    /// What the sync is for, as the controls and the log name it.
31    what: &'static str,
32}
33
34/// What the background thread is asked to do.
35enum Job {
36    /// Sync every handle of a landing, all at once.
37    Batch(Vec<Pending>),
38    /// Answer on the channel when every earlier job is done.
39    Barrier(Sender<()>),
40}
41
42thread_local! {
43    /// Whether the landing on this thread queues its syncs (`true`) or makes them where it asks (`false`).
44    static DEFERRING: Cell<bool> = const { Cell::new(false) };
45    /// Whether the index the landing writes is flushed before it is installed: `core.fsync` names the index, or nothing said.
46    static INDEX_HARDENED: Cell<bool> = const { Cell::new(true) };
47    /// The syncs this landing has queued so far.
48    static QUEUED: RefCell<Vec<Pending>> = const { RefCell::new(Vec::new()) };
49}
50
51/// The sender of the one background thread, started by the first landing that publishes.
52static BACKGROUND: OnceLock<Sender<Job>> = OnceLock::new();
53
54/// How many syncs the background thread has made, for a control that waits for them.
55pub static BACKGROUND_SYNCED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
56
57/// Sync every handle of a batch at once, one thread each, and log what failed: nobody waits for this, so a failure cannot
58///  fail a commit that was already answered, and the next landing or restart handles the record as it would after a power cut.
59fn sync_batch(batch: Vec<Pending>) {
60    std::thread::scope(|scope| {
61        for pending in &batch {
62            scope.spawn(move || {
63                if let Err(error) = pending.handle.sync_all() {
64                    crate::service_warn!("the background sync of the {} failed: {error}", pending.what);
65                }
66                BACKGROUND_SYNCED.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
67            });
68        }
69    });
70}
71
72/// The background thread: takes the jobs in order until the sender is gone.
73fn background(jobs: &Receiver<Job>) {
74    while let Ok(job) = jobs.recv() {
75        match job {
76            Job::Batch(batch) => sync_batch(batch),
77            Job::Barrier(done) => {
78                let _answered = done.send(());
79            }
80        }
81    }
82}
83
84/// The sender of the background thread, started when first asked for.
85fn sender() -> &'static Sender<Job> {
86    return BACKGROUND.get_or_init(|| {
87        let (sender, jobs) = channel::<Job>();
88        let started = std::thread::Builder::new().name(String::from("cli-git-durable")).spawn(move || { background(&jobs); });
89        if let Err(error) = started {
90            crate::service_warn!("the thread that makes the landing's syncs could not start ({error}); they are made where they are asked for");
91        }
92        return sender;
93    });
94}
95
96/// Waits until every sync handed to the background thread so far is done.
97///  A control uses it to see the queue empty; the service does not wait for it on a commit's path.
98pub fn drain() {
99    let (done, answered) = channel::<()>();
100    if sender().send(Job::Barrier(done)).is_ok() {
101        let _waited = answered.recv();
102    }
103}
104
105/// What a landing holds while it runs: its syncs are queued, and it settles them when it ends.
106pub struct Deferral {
107    /// Whether `settle` has run; a deferral dropped without it (a panic) syncs what it queued.
108    settled: bool,
109}
110
111/// Starts queueing the syncs of the landing on this thread.
112///  `hardens_index` says whether the index file the landing installs is flushed before the rename (`core.fsync` names it).
113pub fn begin(hardens_index: bool) -> Deferral {
114    DEFERRING.with(|flag| { flag.set(true); });
115    INDEX_HARDENED.with(|flag| { flag.set(hardens_index); });
116    QUEUED.with(|queue| { queue.borrow_mut().clear(); });
117    return Deferral { settled: false };
118}
119
120/// Ends the deferral on this thread and returns what it queued.
121fn end() -> Vec<Pending> {
122    DEFERRING.with(|flag| { flag.set(false); });
123    INDEX_HARDENED.with(|flag| { flag.set(true); });
124    return QUEUED.with(|queue| { return std::mem::take(&mut *queue.borrow_mut()); });
125}
126
127/// Syncs every handle now, all at once, and names each as waited for.
128fn force(batch: Vec<Pending>) {
129    for pending in &batch {
130        crate::service_fault::note(format!("sync-waited:{}", pending.what).as_str());
131    }
132    std::thread::scope(|scope| {
133        for pending in &batch {
134            scope.spawn(move || {
135                if let Err(error) = pending.handle.sync_all() {
136                    crate::service_warn!("the sync of the {} failed: {error}", pending.what);
137                }
138            });
139        }
140    });
141}
142
143impl Deferral {
144    /// Ends the deferral. A landing that published a commit hands its queue to the background thread and returns at once; a
145    ///  landing that published nothing (a withdrawal, an error, a commit sent back) syncs its records before it returns.
146    pub fn settle(mut self, published: bool) {
147        self.settled = true;
148        let batch: Vec<Pending> = end();
149        if batch.is_empty() {
150            return;
151        }
152        if published {
153            if let Err(rejected) = sender().send(Job::Batch(batch)) {
154                // The thread is gone, so the queue is made here rather than lost.
155                if let Job::Batch(returned) = rejected.0 {
156                    force(returned);
157                }
158            }
159        } else {
160            force(batch);
161        }
162    }
163}
164
165impl Drop for Deferral {
166    fn drop(&mut self) {
167        if !self.settled {
168            force(end());
169        }
170    }
171}
172
173/// Flushes a file that is a landing's record: queued while a landing defers, made now otherwise.
174///
175/// # Errors
176///
177/// The error of the flush when it is made now.
178pub fn flush_file(file: &File, what: &'static str) -> std::io::Result<()> {
179    if DEFERRING.with(Cell::get) {
180        crate::service_fault::note(format!("sync-queued:{what}").as_str());
181        QUEUED.with(|queue| -> std::io::Result<()> {
182            queue.borrow_mut().push(Pending { handle: file.try_clone()?, what });
183            return Ok(());
184        })?;
185        return Ok(());
186    }
187    let synced: std::io::Result<()> = file.sync_all();
188    crate::service_fault::note(format!("sync-waited:{what}").as_str());
189    return synced;
190}
191
192/// Flushes a directory whose entries are a landing's records, as `flush_file` does for a file.
193///
194/// # Errors
195///
196/// The error of opening the directory, or of the flush when it is made now.
197pub fn flush_directory(directory: &std::path::Path, what: &'static str) -> std::io::Result<()> {
198    return flush_file(&File::open(directory)?, what);
199}
200
201/// Flushes the index file the landing writes, when the index is hardened: `core.fsync` names it, as native Git's index write
202///  is hardened then. Otherwise the index is written as native Git writes it, unflushed. It is made now, on the landing's path,
203///  as Git's own write is.
204///
205/// # Errors
206///
207/// The error of the flush.
208pub fn flush_index(file: &File, what: &'static str) -> std::io::Result<()> {
209    if INDEX_HARDENED.with(Cell::get) {
210        let synced: std::io::Result<()> = file.sync_all();
211        crate::service_fault::note(format!("sync-waited:{what}").as_str());
212        return synced;
213    }
214    return Ok(());
215}
216
217/// Whether the index file the landing writes is flushed before it is installed.
218pub fn index_is_hardened() -> bool {
219    return INDEX_HARDENED.with(Cell::get);
220}
221
222/// Controls for the queue stay out of the release executable.
223#[cfg(test)]
224#[path = "service_durable_tests.rs"]
225mod tests;