Skip to main content

git_policy_cli/
service_timing.rs

1//! What:
2//!  The lines the resident commit service writes to its log (standard error,
3//! which the service manager keeps in the journal)
4//!  about its own life and about the time each request took:
5//! when it started,
6//! one line for each request it answered with the
7//!  phases of the wait,
8//! a retire request,
9//! and its exit with the reason and the uptime.
10//! Why:
11//!  After a slow commit the journal held `ready` and nothing else,
12//! so nothing said whether the time went to the queue,
13//! to
14//!  another program's hold on the index,
15//! to the landing or to a service being replaced.
16//! These lines cost one clock read per
17//!  phase edge and one line per request;
18//! they name no person (the commit's message and identities never appear) and are not
19//!  shown to the person at the terminal.
20//!
21//! In TS you'd write (pseudocode):
22//! ```ts
23//! console.error(`cli-git-service: answered request=${id} queued_ms=${queued} lock_wait_ms=${lockWait} total_ms=${total}`);
24//! ```
25
26use std::io::Write;
27use std::time::{Duration, Instant};
28
29/// The prefix of every line the service writes,
30/// which `journalctl` shows after the unit name.
31pub const LOG_PREFIX: &str = "cli-git-service: ";
32
33/// When a connection was accepted and how long its request line took to arrive.
34#[derive(Clone, Copy, Debug)]
35pub struct Arrival {
36    /// The moment the connection was accepted.
37    pub accepted: Instant,
38    /// Time from the acceptance to the whole request line being read;
39    /// a client that is slow to send shows up here and not as a
40    ///  wait in the queue.
41    pub read: Duration,
42}
43
44/// What:
45///  The phases of one landing the service itself spent time in,
46/// summed over the attempts a moving branch made it repeat.
47/// Why:
48///   The wait for another program's hold on the index,
49/// the objects,
50/// the journal write,
51/// the branch move and the index update are
52///  the stretches a slow landing can lose time in;
53/// whatever else a landing did is reported as the reads that remain.
54///
55/// In TS you'd write (pseudocode):
56/// ```ts
57/// type LandTiming = { lockWait: number; lockHolder?: string; objects: number; journal: number; refUpdate: number; sync: number; attempts: number };
58/// ```
59#[derive(Clone, Debug, Default, Eq, PartialEq)]
60pub struct LandTiming {
61    /// Time to take the index lock,
62    /// which is the whole wait for a foreign holder.
63    pub lock_wait: Duration,
64    /// The holder the evidence named while the lock was waited for,
65    /// when it named one.
66    pub lock_holder: Option<String>,
67    /// Time to make the commits:
68    /// one import for the batch,
69    /// or the checks of a commit Git built.
70    pub objects: Duration,
71    /// Time to write the intent to the journal.
72    pub journal: Duration,
73    /// Time of the compare-and-swap that moves the branch.
74    pub ref_update: Duration,
75    /// Time to remove the conclusion state and to build and install the new index.
76    pub sync: Duration,
77    /// Passes over the batch:
78    /// one,
79    /// and one more each time the branch moved under it.
80    pub attempts: usize,
81}
82
83impl LandTiming {
84    /// What `landing` spent outside the named phases:
85    /// the branch reads,
86    /// the comparisons with the branch,
87    /// the check for removed
88    ///  content.
89    pub fn reads(&self, landing: Duration) -> Duration {
90        return landing.saturating_sub(self.lock_wait + self.objects + self.journal + self.ref_update + self.sync);
91    }
92}
93
94/// Everything the line for one answered request states.
95pub struct Answered<'record> {
96    /// The request's identity.
97    pub request: &'record str,
98    /// The reference the request names.
99    pub reference: &'record str,
100    /// What became of it:
101    /// a word such as `landed`,
102    /// `already`,
103    /// `retry-prepare`,
104    /// `branch-moved` or `failed:<code>`.
105    pub outcome: &'record str,
106    /// The batch number,
107    /// shared by every request landed together.
108    pub batch: u64,
109    /// How many requests the batch of this branch held.
110    pub size: usize,
111    /// This request's place among them,
112    /// from one.
113    pub position: usize,
114    /// How the connection and its request line arrived.
115    pub arrival: Arrival,
116    /// Time from the request being read to its batch starting to land.
117    pub queued: Duration,
118    /// Time to settle the batches a killed service left open,
119    /// which runs before each batch.
120    pub settle: Duration,
121    /// The phases of the landing.
122    pub landing: &'record LandTiming,
123    /// Time the landing took,
124    /// from its start to its return.
125    pub land_total: Duration,
126    /// Time from the landing's end to this request being answered.
127    pub answer: Duration,
128    /// Time from the acceptance of the connection to the answer.
129    pub total: Duration,
130}
131
132/// The word for a named holder,
133/// in the quoted form.
134fn holder_field(who: &String) -> String {
135    return format!(" lock_holder={who:?}");
136}
137
138/// What:
139///  One answered request as a line of `key=value` words,
140/// without the prefix.
141/// Why:
142///   Text values are written in Rust's quoted form,
143/// which has no raw newline or unescaped quote,
144/// so the line stays one line
145///  whatever a client sent;
146/// the numbers are whole milliseconds.
147///
148/// In TS you'd write (pseudocode):
149/// ```ts
150/// `answered request=${JSON.stringify(id)} outcome=${outcome} queued_ms=${queued} ...`
151/// ```
152pub fn answered_line(answered: &Answered<'_>) -> String {
153    let holder: String = answered.landing.lock_holder.as_ref().map_or_else(String::new, holder_field);
154    return format!(
155        "answered request={:?} branch={:?} outcome={} batch={} size={} position={} read_ms={} queued_ms={} settle_ms={} \
156         lock_wait_ms={}{holder} reads_ms={} objects_ms={} journal_ms={} ref_ms={} sync_ms={} attempts={} landing_ms={} \
157         answer_ms={} total_ms={}",
158        answered.request,
159        answered.reference,
160        answered.outcome,
161        answered.batch,
162        answered.size,
163        answered.position,
164        answered.arrival.read.as_millis(),
165        answered.queued.as_millis(),
166        answered.settle.as_millis(),
167        answered.landing.lock_wait.as_millis(),
168        answered.landing.reads(answered.land_total).as_millis(),
169        answered.landing.objects.as_millis(),
170        answered.landing.journal.as_millis(),
171        answered.landing.ref_update.as_millis(),
172        answered.landing.sync.as_millis(),
173        answered.landing.attempts,
174        answered.land_total.as_millis(),
175        answered.answer.as_millis(),
176        answered.total.as_millis(),
177    );
178}
179
180/// The line a service writes as it starts,
181/// before `ready`:
182/// the release it announces,
183/// the protocol,
184/// its process and the worktree
185///  instance it serves.
186pub fn started_line(release: &str, protocol: u32, instance: Option<&str>) -> String {
187    return format!("started release={release:?} protocol={protocol} pid={} instance={}", std::process::id(), instance.unwrap_or("any"));
188}
189
190/// The line for a retire message:
191/// a client of another release found this service and asked it to finish its batch and exit.
192pub fn retire_line(release: &str) -> String {
193    return format!("retire requested by a client; this service is release {release:?} and exits after its batch");
194}
195
196/// Why a service ended.
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum ExitReason {
199    /// Nothing to do for the whole idle period.
200    Idle,
201    /// A client asked it to retire.
202    Retire,
203    /// The queue its connections feed closed,
204    /// which only happens when the acceptor thread died.
205    Error,
206}
207
208impl ExitReason {
209    /// The word written in the line.
210    pub fn word(self) -> &'static str {
211        return match self {
212            ExitReason::Idle => "idle",
213            ExitReason::Retire => "retire",
214            ExitReason::Error => "error",
215        };
216    }
217}
218
219/// The line for the service's own exit,
220/// with the time it ran since it began serving.
221pub fn exit_line(reason: ExitReason, uptime: Duration) -> String {
222    return format!("exiting reason={} uptime_ms={}", reason.word(), uptime.as_millis());
223}
224
225/// What:
226///  Writes one line to the service's log.
227/// Why:
228///   Standard error is the service manager's journal;
229/// one `write` carries the prefix,
230/// the line and the newline together,
231/// and a
232///  closed or broken stream is not the service's failure.
233///
234/// In TS you'd write (pseudocode):
235/// ```ts
236/// console.error(LOG_PREFIX + line)
237/// ```
238pub fn log(line: &str) {
239    let mut text: String = format!("{LOG_PREFIX}{line}");
240    text.push('\n');
241    // The result is dropped on purpose: `eprintln!` panics when the stream breaks (a journal restarting), and a log line may
242    //  never take the landing that wrote it down with it.
243    let _written = std::io::stderr().write_all(text.as_bytes());
244}
245
246/// The lines,
247/// their fields,
248/// and the escaping.
249#[cfg(test)]
250#[path = "service_timing_tests.rs"]
251mod tests;