git_policy_cli/landing/service_captures.rs
1//! What:
2//! The service's memory of when the bytes of the commits it landed were captured, kept per worktree.
3//! Why:
4//! Two commands that capture a file from one worktree at different moments hold different states of it, and the later
5//! capture holds everything the earlier one did (`doc/decision/cli-git-concurrent-commits.md`, "Serial landing"). When the
6//! service sends a commit back because a landed commit changed the same path, the client needs to know which of the two
7//! captured later, and only the service saw the landed commit's request. The stamp is a reading of `CLOCK_MONOTONIC`, one
8//! clock for every process of the host, so captures made by different wrapper processes compare. The memory lives in the
9//! service process and starts empty with it, so a restart can only make a capture unknown, never misordered: a commit with
10//! no entry is treated as captured by somebody else, and the client then merges instead of preferring either side. A reboot
11//! restarts both the clock and the service together. Entries are kept per worktree, because the rule holds for captures of
12//! one worktree only.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! memory.recordLanded(commonDir, worktree, commit, captured);
17//! const known = memory.remembered(commonDir, worktree); // what a commit sent back is told
18//! ```
19
20use crate::service_protocol::LandedCapture;
21use std::collections::{HashMap, VecDeque};
22use std::path::{Path, PathBuf};
23
24/// How many landed commits are remembered for one worktree; older ones are forgotten, which makes them unknown. Measured: in the
25/// service journal (1,085 landings of the `cli-git@` units, 2026-11-02T09:01:38Z to 2026-11-03T15:11:58Z) the busiest unit landed 15
26/// commits in its busiest ten minutes and 349 in all, so this many cover eight times the busiest ten minutes and over a third
27/// of everything the busiest worktree landed in the window. Kept as the coordinator's pick under the owner's delegation, open to the owner's veto (`doc/planning/cli-git-shipped-limits-audit.md`, "Sizes and ages of the service's own housekeeping").
28pub const REMEMBERED: usize = 128;
29
30type Key = (PathBuf, PathBuf);
31
32/// What:
33/// The memory of landed captures, owned by the serve loop that lands the commits and answers the clients.
34/// Why:
35/// Only that one thread records a landed commit and reads the memory to answer a commit sent back, so the memory is a plain
36/// value it holds and passes down (`&mut` to record, `&` to read), not a process-wide table behind a lock: nothing waits for
37/// it and nothing else can reach it.
38///
39/// In TS you'd write (pseudocode):
40/// ```ts
41/// const memory = new CaptureMemory(); memory.recordLanded(commonDir, worktree, commit, captured);
42/// ```
43#[derive(Debug, Default)]
44pub struct CaptureMemory {
45 /// The landed commits of each worktree, oldest first.
46 by_worktree: HashMap<Key, VecDeque<LandedCapture>>,
47}
48
49/// Methods over the memory.
50impl CaptureMemory {
51 /// What:
52 /// Remembers a landed commit and the stamp of the request that made it, when the stamp is known.
53 /// Why:
54 /// A request with no stamp (zero) cannot be ordered, so it is not remembered and its commit stays unknown.
55 ///
56 /// In TS you'd write (pseudocode):
57 /// ```ts
58 /// memory.recordLanded(commonDir, worktree, commit, captured)
59 /// ```
60 pub fn record_landed(&mut self, common_dir: &Path, worktree: &Path, commit: &str, captured: u64) {
61 if captured == 0 {
62 return;
63 }
64 let entries: &mut VecDeque<LandedCapture> = self.by_worktree.entry((common_dir.to_path_buf(), worktree.to_path_buf())).or_default();
65 entries.push_back(LandedCapture { commit: commit.to_string(), captured });
66 while entries.len() > REMEMBERED {
67 entries.pop_front();
68 }
69 }
70
71 /// What:
72 /// The commits landed for a worktree that the service remembers, oldest first.
73 /// Why:
74 /// The client matches them by name against the commits that changed a path since its base, so older ones do no harm.
75 ///
76 /// In TS you'd write (pseudocode):
77 /// ```ts
78 /// memory.remembered(commonDir, worktree)
79 /// ```
80 pub fn remembered(&self, common_dir: &Path, worktree: &Path) -> Vec<LandedCapture> {
81 return self.by_worktree.get(&(common_dir.to_path_buf(), worktree.to_path_buf())).map(|entries| { return entries.iter().cloned().collect(); }).unwrap_or_default();
82 }
83}
84
85/// Capture memory controls stay out of the release executable.
86#[cfg(test)]
87#[path = "service_captures_tests.rs"]
88mod tests;