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 service's push 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    deadline: Duration,
97    grace: Duration,
98    environment: &[(OsString, OsString)],
99) -> Result<QueryOutput, SingleFlightError> {
100    let mut stdout: File = scratch_file(environment).map_err(SingleFlightError::Start)?;
101    let mut stderr: File = scratch_file(environment).map_err(SingleFlightError::Start)?;
102    let for_stdout: File = stdout.try_clone().map_err(SingleFlightError::Start)?;
103    let for_stderr: File = stderr.try_clone().map_err(SingleFlightError::Start)?;
104    command
105        .stdin(Stdio::null())
106        .stdout(Stdio::from(for_stdout))
107        .stderr(Stdio::from(for_stderr));
108    let mut child = command.spawn_retrying().map_err(SingleFlightError::Start)?;
109    drop(command);
110    let ended: Result<ExitStatus, StopCause> =
111        wait_until_done(&mut child, &[&stdout, &stderr], deadline)?;
112    return match ended {
113        Ok(status) => Ok(QueryOutput {
114            code: status.code(),
115            stdout: read_back(&mut stdout)?,
116            stderr: read_back(&mut stderr)?,
117        }),
118        Err(cause) => {
119            stop_push(&mut child, grace);
120            Err(match cause {
121                StopCause::Deadline => SingleFlightError::TimedOut {
122                    arguments: arguments.to_owned(),
123                    seconds: deadline.as_secs(),
124                },
125                StopCause::Flood => SingleFlightError::Git {
126                    arguments: arguments.to_owned(),
127                    code: None,
128                    stderr: format!(
129                        "the query wrote more than {MAX_PUSH_OUTPUT} bytes and was stopped"
130                    ),
131                },
132            })
133        }
134    };
135}
136
137/// Query deadlines are controlled in the test module.
138#[cfg(test)]
139#[path = "bounded_query_tests.rs"]
140mod tests;