Skip to main content

git_policy_cli/landing/
service_round.rs

1//! What:
2//!  The drain of several repositories in one round of the service's after-commit work, side by side.
3//! Why:
4//!  Repositories share nothing: each has its own work directory, its own claims and its own pushes, so one repository's slow
5//!  push or review never needs to hold up another's. The round's thread hands each repository to a worker and takes the answers
6//!  back in the order the repositories were named (`parallel_runner.rs`), so the retry list is the one a drain of one after
7//!  another gives. No lock is taken (`service_lock_inventory_tests.rs`): the handlers are shared read-only, every claim is made
8//!  by an atomic rename as before, and the directories of one repository (a main checkout and its linked worktrees) share a lane
9//!  and are drained one after another there, because they share a work directory and its upkeep.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const retries = await drainDirectories({ directories, handlers, now, host });
14//! ```
15
16use crate::claim_time::ClaimTime;
17use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
18use crate::parallel_runner::{Event, OrderedRun, run_ordered};
19use crate::push_retry::{RetryChange, Retrying};
20use crate::service_work::{CLAIM_LEASE, Drained, WorkHandlers, common_directory_of, drain_common_records_planned, drain_planned};
21use std::path::{Path, PathBuf};
22use std::time::Duration;
23
24/// The 90th percentile of `MemoryPeak` over the 28 commit-service units of this host that `systemctl --user show 'cli-git@*.service'
25///  --property=MemoryPeak` reported on 2026-11-03 (the units' peaks since each one's last start; the Git processes a service
26///  starts are inside the unit): 130,482,176 bytes. The median was 33,000,000 bytes, the 75th percentile 49,000,000 bytes, and the
27///  largest 1,073,766,400 bytes, a unit that had run under the one-gibibyte cap this build no longer sets. A unit serves one
28///  worktree for its whole life, so a unit's peak is an upper bound of what one repository's drain holds at once.
29pub const MEASURED_REPOSITORY_PEAK: u64 = 130_482_176;
30
31/// The bytes the drain of one repository holds at its peak: `MEASURED_REPOSITORY_PEAK` rounded up to a power of two, as
32///  `CONVERSION_MEMORY` rounds its measurement. The coordinator's pick under the owner's delegation, open to the owner's veto
33///  (`doc/planning/cli-git-shipped-limits-audit.md`, "Parallelism caps and memory estimates").
34pub const REPOSITORY_MEMORY: u64 = MEASURED_REPOSITORY_PEAK.next_power_of_two();
35
36/// What:
37///  One directory of a round, with the pushes of it that are due again.
38/// Why:
39///   The service loop owns the retry plan; it takes what is due for the directory out of the plan before the round and hands it to
40///  the directory's worker inside the request, as an owned value, so no worker reads the plan.
41///
42/// In TS you'd write (pseudocode):
43/// ```ts
44/// type Assignment = { directory: string; retrying: Retrying };
45/// ```
46#[derive(Clone, Debug)]
47pub struct Assignment {
48    /// The Git directory to drain.
49    pub directory: PathBuf,
50    /// The first pause and the pushes due again for this directory.
51    pub retrying: Retrying,
52}
53
54/// Methods over an assignment.
55impl Assignment {
56    /// A directory with no plan: a push that could pass is reported as failed.
57    pub fn plain(directory: PathBuf) -> Self {
58        return Self { directory, retrying: Retrying::none() };
59    }
60}
61
62/// What:
63///  What a round gives back: when to look again at the directories that have a claim another instance holds, and the changes the
64///  workers ask of the retry plan.
65/// Why:
66///   The waits keep the order the directories were named; the changes are in that order too, so applying them in sequence gives
67///  the plan a drain of one directory after another would.
68///
69/// In TS you'd write (pseudocode):
70/// ```ts
71/// type RoundAnswer = { waits: [string, number][]; changes: RetryChange[] };
72/// ```
73#[derive(Debug, Default)]
74pub struct RoundAnswer {
75    /// For each directory that has such a claim, how long until its lease runs out, in the order the directories were named.
76    pub waits: Vec<(PathBuf, Duration)>,
77    /// What the workers ask of the retry plan.
78    pub changes: Vec<RetryChange>,
79}
80
81/// What:
82///  What a round needs to drain its repositories.
83/// Why:
84///   One value names the inputs, so the call reads as a list of facts.
85///
86/// In TS you'd write (pseudocode):
87/// ```ts
88/// type RoundDrain<H> = { directories: string[]; handlers: H; now: ClaimTime; host: HostResources };
89/// ```
90pub struct RoundDrain<'a, H: WorkHandlers + Sync> {
91    /// The Git directories named by the round's wakes, each once, in the order they were first named, each with what is due again.
92    pub directories: &'a [Assignment],
93    /// What runs each kind of work; shared read-only by every worker.
94    pub handlers: &'a H,
95    /// The reading of the clock the round started at.
96    pub now: &'a ClaimTime,
97    /// The CPUs and memory the budget is derived from.
98    pub host: &'a HostResources,
99}
100
101/// What:
102///  The lane of a directory: the common directory of its repository when it names one, else the directory itself.
103/// Why:
104///   The directories of one repository share a work directory and its upkeep, so they are drained one after another.
105///
106/// In TS you'd write (pseudocode):
107/// ```ts
108/// const lane = commonDirectoryOf(gitDir) ?? gitDir;
109/// ```
110fn lane_key(directory: &Path) -> PathBuf {
111    return common_directory_of(directory).unwrap_or_else(|| { return directory.to_path_buf(); });
112}
113
114/// What:
115///  Drain every directory, repositories side by side, and give, for each directory that has a claim another instance holds, how
116///  long until that claim's lease runs out, in the order the directories were named.
117/// Why:
118///   See the module description.
119///
120/// In TS you'd write (pseudocode):
121/// ```ts
122/// const answer = await drainDirectories({ directories, handlers, now, host });
123/// ```
124pub fn drain_directories<H: WorkHandlers + Sync>(request: &RoundDrain<'_, H>) -> RoundAnswer {
125    let RoundDrain { directories, handlers, now, host } = *request;
126    let mut lanes: Vec<Vec<usize>> = Vec::new();
127    let mut lane_of: std::collections::HashMap<PathBuf, usize> = std::collections::HashMap::new();
128    for (index, assignment) in directories.iter().enumerate() {
129        match lane_of.get(&lane_key(assignment.directory.as_path())) {
130            Some(&lane) => lanes[lane].push(index),
131            None => {
132                lane_of.insert(lane_key(assignment.directory.as_path()), lanes.len());
133                lanes.push(vec![index]);
134            }
135        }
136    }
137    let workers: usize = worker_budget(&BudgetRequest { items: lanes.len(), per_item_memory: REPOSITORY_MEMORY, ceiling: None, cap: None, host });
138    let mut answer: RoundAnswer = RoundAnswer::default();
139    run_ordered(OrderedRun {
140        items: directories,
141        lanes: lanes.as_slice(),
142        workers,
143        work: &|_index: usize, assignment: &Assignment, _say: &mut dyn FnMut(&str)| {
144            let own: Drained = drain_planned(assignment.directory.as_path(), handlers, now, &assignment.retrying);
145            let common: Drained = drain_common_records_planned(assignment.directory.as_path(), handlers, now, CLAIM_LEASE, assignment.retrying.first);
146            let wait: Option<Duration> = [own.wait, common.wait].into_iter().flatten().min();
147            return (wait, own.changes.into_iter().chain(common.changes).collect::<Vec<RetryChange>>());
148        },
149        on_event: &mut |event: Event<'_, (Option<Duration>, Vec<RetryChange>)>| {
150            if let Event::End(index, (wait, changes)) = event {
151                if let Some(lease_wait) = wait {
152                    answer.waits.push((directories[index].directory.clone(), lease_wait));
153                }
154                answer.changes.extend(changes);
155            }
156        },
157    });
158    return answer;
159}
160
161/// Round controls stay out of the release executable.
162#[cfg(test)]
163#[path = "service_round_tests.rs"]
164mod tests;