Skip to main content

git_policy_cli/
bounded_query.rs

1//! What:
2//!  Run one read-only Git query with a deadline and separate output streams.
3//! Why:
4//!  The auto-push asks Git a handful of questions before it pushes,
5//!  and a question that never returns would hold the commit and the branch's push lock for good.
6//!  A query that outlives its deadline is stopped and reported, so the commit stays saved.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! const { exitCode, stdout, stderr } = await nanoSpawn(gitPath, args, { cwd, signal: AbortSignal.timeout(deadlineMs) });
11//! ```
12
13/// The failure type of the auto-push coordination.
14use super::auto_push_single_flight::SingleFlightError;
15/// The deadline runner shared with the push, and its stop causes.
16use super::bounded_push::{MAX_PUSH_OUTPUT, StopCause, push_deadline, stop_push, wait_until_done};
17/// Starts that ride out a short shortage of processes.
18/// The anonymous output files of the query, made in the per-user scratch directory.
19use super::scratch_location::scratch_file;
20use super::start_retry::RetryStart;
21/// Owned OS strings of the child environment.
22use std::ffi::OsString;
23/// The anonymous output files and their reading back.
24use std::fs::File;
25use std::io::{Read, Seek, SeekFrom};
26/// `Command` describes the query; `Stdio` connects its streams.
27use std::process::{Command, ExitStatus, Stdio};
28/// Time spans.
29use std::time::Duration;
30
31/// The longest a query may run whatever the push deadline is:
32///  queries read local repository state and finish in milliseconds.
33pub const QUERY_CEILING: Duration = Duration::from_secs(120);
34
35/// What:
36///  What a finished query printed and how it ended.
37/// Why:
38///   Callers read an exit code as an answer ("not an ancestor") or a failure, never both.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// type QueryOutput = { code: number | undefined; stdout: Uint8Array; stderr: Uint8Array };
43/// ```
44#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct QueryOutput {
46    /// Exit code, or nothing when a signal ended the process.
47    pub code: Option<i32>,
48    /// Complete standard output, up to the output cap.
49    pub stdout: Vec<u8>,
50    /// Complete standard error, up to the output cap.
51    pub stderr: Vec<u8>,
52}
53
54/// What:
55///  The deadline of a query: the push deadline, but never more than the ceiling.
56/// Why:
57///   One setting bounds every Git child of the auto-push, and a long push deadline must not stretch queries.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// Math.min(pushDeadlineMs(env), QUERY_CEILING_MS)
62/// ```
63pub fn query_deadline(environment: &[(OsString, OsString)]) -> Duration {
64    return push_deadline(environment).min(QUERY_CEILING);
65}
66
67/// Read one output file back from its start, up to the output cap.
68///
69/// In TS you'd write (pseudocode):
70/// ```ts
71/// await readFile(path)
72/// ```
73fn read_back(file: &mut File) -> Result<Vec<u8>, SingleFlightError> {
74    file.seek(SeekFrom::Start(0))
75        .map_err(SingleFlightError::Start)?;
76    let mut bytes: Vec<u8> = Vec::new();
77    file.take(MAX_PUSH_OUTPUT)
78        .read_to_end(&mut bytes)
79        .map_err(SingleFlightError::Start)?;
80    return Ok(bytes);
81}
82
83/// What:
84///  Run a query command to its end or its deadline.
85/// Why:
86///   Both streams go to anonymous files, so no reader thread can outlive a stopped query.
87///  A query past its deadline is a coordination failure, not an answer.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// async function runBoundedQuery({ command, arguments, deadlineMs, graceMs }): Promise<QueryOutput>;
92/// ```
93pub fn run_bounded_query(
94    mut command: Command,
95    arguments: &str,
96    requested_deadline: Duration,
97    grace: Duration,
98    environment: &[(OsString, OsString)],
99) -> Result<QueryOutput, SingleFlightError> {
100    if super::holder_budget::exhausted() {
101        return Err(SingleFlightError::BudgetSpent { arguments: arguments.to_owned() });
102    }
103    let deadline: Duration = super::holder_budget::clamp(requested_deadline);
104    let mut stdout: File = scratch_file(environment).map_err(SingleFlightError::Start)?;
105    let mut stderr: File = scratch_file(environment).map_err(SingleFlightError::Start)?;
106    let for_stdout: File = stdout.try_clone().map_err(SingleFlightError::Start)?;
107    let for_stderr: File = stderr.try_clone().map_err(SingleFlightError::Start)?;
108    command
109        .stdin(Stdio::null())
110        .stdout(Stdio::from(for_stdout))
111        .stderr(Stdio::from(for_stderr));
112    let mut child = command.spawn_retrying().map_err(SingleFlightError::Start)?;
113    drop(command);
114    let ended: Result<ExitStatus, StopCause> =
115        wait_until_done(&mut child, &[&stdout, &stderr], deadline)?;
116    return match ended {
117        Ok(status) => Ok(QueryOutput {
118            code: status.code(),
119            stdout: read_back(&mut stdout)?,
120            stderr: read_back(&mut stderr)?,
121        }),
122        Err(cause) => {
123            stop_push(&mut child, grace);
124            Err(match cause {
125                StopCause::Deadline => SingleFlightError::TimedOut {
126                    arguments: arguments.to_owned(),
127                    seconds: deadline.as_secs(),
128                },
129                StopCause::Flood => SingleFlightError::Git {
130                    arguments: arguments.to_owned(),
131                    code: None,
132                    stderr: format!(
133                        "the query wrote more than {MAX_PUSH_OUTPUT} bytes and was stopped"
134                    ),
135                },
136            })
137        }
138    };
139}
140
141/// Query deadlines are controlled in the test module.
142#[cfg(test)]
143#[path = "bounded_query_tests.rs"]
144mod tests;