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;