Skip to main content

git_policy_cli/
child_class.rs

1//! What:
2//!  The two kinds of Git step a commit starts for itself, and the bound and the notice grace of each.
3//! Why:
4//!  One bound for every step had to be long enough for the slowest, so a stalled `rev-parse` held a commit for as long
5//!  as a stalled `pack-objects`, and the first notice came after one second for steps that only needed a moment.
6//!  The classes follow what a step touches, measured on the repository this crate lives in
7//!  (`doc/planning/cli-git-commit-timeouts.md`):
8//!  a query reads refs, commits and trees and ends in milliseconds whatever the worktree holds, while a step that
9//!  writes objects, or that compares the index with the worktree, costs time in proportion to the bytes it reads
10//!  (`reset`, `update-index --refresh` and `ls-files --modified` took 2.9 s on a stat-dirty 20 000-file index against
11//!  0.03 s on a clean one, and a cold disk multiplies that).
12//!  The user's own `git commit` with its hooks is not a class: its runtime belongs to the user and it is never bounded.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const limit = QUERY_SUBCOMMANDS.includes(subcommandOf(args)) ? QUERY_LIMIT_MS : BULK_LIMIT_MS;
17//! ```
18
19/// Positive-seconds parsing shared with the lock wait limits.
20use super::bounded_push::positive_seconds;
21/// The name of the subcommand a request runs.
22use super::transaction_git::subcommand_of;
23/// Operating-system text of the arguments and of the environment value.
24use std::ffi::{OsStr, OsString};
25/// Time spans.
26use std::time::Duration;
27
28/// Environment variable that sets, in whole seconds, how long any Git step the wrapper starts for itself may run
29///  before it is stopped, whatever its class.
30pub const CHILD_WAIT_VARIABLE: &str = "CLI_GIT_CHILD_WAIT_SECONDS";
31
32/// How long a query step may run:
33///  a hundred times the slowest such step measured on a host at several times its core count in load.
34pub const PLUMBING_CHILD_WAIT: Duration = Duration::from_secs(60);
35
36/// How long a bulk step may run:
37///  its time grows with the bytes it moves or compares, so the bound leaves room for a large or stat-dirty worktree
38///  on a loaded host.
39pub const OBJECT_CHILD_WAIT: Duration = Duration::from_secs(300);
40
41/// How long a query step runs before the first notice.
42pub const PLUMBING_NOTICE_GRACE: Duration = Duration::from_secs(5);
43
44/// How long a bulk step runs before the first notice.
45pub const OBJECT_NOTICE_GRACE: Duration = Duration::from_secs(10);
46
47/// Subcommands that only read refs, commits and trees, or write one ref or one tree from the index as it is:
48///  they end in milliseconds whatever the worktree holds.
49///  Every other subcommand, named or not, is a bulk step and gets the longer bound instead of an early stop:
50///  `pack-objects`, `index-pack`, `add`, `hash-object` and `maintenance` move bytes, and `reset`, `read-tree`,
51///  `update-index`, `ls-files`, `diff`, `diff-index`, `show` and `ls-remote` refresh the index, compare it with the
52///  worktree or talk to a remote.
53pub const PLUMBING_SUBCOMMANDS: [&str; 13] = [
54    "rev-parse",
55    "rev-list",
56    "symbolic-ref",
57    "for-each-ref",
58    "ls-tree",
59    "cat-file",
60    "diff-tree",
61    "merge-base",
62    "write-tree",
63    "update-ref",
64    "config",
65    "remote",
66    "version",
67];
68
69/// What:
70///  The kind of a Git step, which decides its bound and when its first notice comes.
71/// Why:
72///   The two kinds differ by orders of magnitude in how long a healthy step takes.
73///
74/// In TS you'd write (pseudocode):
75/// ```ts
76/// type ChildClass = 'plumbing' | 'object';
77/// ```
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum ChildClass {
80    /// A query of refs, commits and trees.
81    Plumbing,
82    /// A step that writes objects, compares the index with the worktree, or is not known to be a query.
83    Object,
84}
85
86impl ChildClass {
87    /// How a notice or a timeout sentence names the kind of step.
88    pub fn label(self) -> &'static str {
89        return match self {
90            Self::Plumbing => "a quick query step",
91            Self::Object => "a bulk step",
92        };
93    }
94
95    /// How long a step of this class may run when the variable is absent or unusable.
96    pub fn default_limit(self) -> Duration {
97        return match self {
98            Self::Plumbing => PLUMBING_CHILD_WAIT,
99            Self::Object => OBJECT_CHILD_WAIT,
100        };
101    }
102
103    /// How long a step of this class runs before its first notice.
104    pub fn notice_grace(self) -> Duration {
105        return match self {
106            Self::Plumbing => PLUMBING_NOTICE_GRACE,
107            Self::Object => OBJECT_NOTICE_GRACE,
108        };
109    }
110
111    /// The bound for a value of the variable: the person's whole seconds, or this class's default for a missing,
112    ///  zero or malformed value, never "no limit".
113    pub fn limit_from(self, value: Option<&OsStr>) -> Duration {
114        let Some(text) = value else {
115            return self.default_limit();
116        };
117        return positive_seconds(CHILD_WAIT_VARIABLE, text).unwrap_or_else(|| { return self.default_limit(); });
118    }
119
120    /// The bound this process runs this class under: the variable in its environment, or the class default.
121    pub fn limit(self) -> Duration {
122        return self.limit_from(std::env::var_os(CHILD_WAIT_VARIABLE).as_deref());
123    }
124}
125
126/// What:
127///  The class of the Git step a request runs.
128/// Why:
129///   A step is a query only when its subcommand is named in `PLUMBING_SUBCOMMANDS`; every other step, a request with
130///  no subcommand included, gets the longer bound.
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// QUERIES.includes(subcommandOf(args)) ? 'plumbing' : 'object'
135/// ```
136pub fn class_of(arguments: &[OsString]) -> ChildClass {
137    let Some((_, name)) = subcommand_of(arguments) else {
138        return ChildClass::Object;
139    };
140    if PLUMBING_SUBCOMMANDS.contains(&name.as_str()) {
141        return ChildClass::Plumbing;
142    }
143    return ChildClass::Object;
144}
145
146/// Class controls stay out of the release executable.
147#[cfg(test)]
148#[path = "child_class_tests.rs"]
149mod tests;