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