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;