Skip to main content

git_policy_cli/service/
service_server.rs

1//! What:
2//!  The resident service's loop: accept connections, queue requests in arrival order, land what is queued as a batch,
3//!  answer each client, and exit when told to retire.
4//! Why:
5//!  One writer lands every commit, so there is no lock to take, hold or wait for: order is the order of the queue, the
6//!  cost of the one durable write is shared by the batch, and a client that waits blocks on its socket instead of
7//!  polling (`doc/planning/cli-git-resident-service.md`). The service never starts itself: the operating system hands it
8//!  the listening socket (socket activation), or a development run binds one.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! for await (const batch of queue.batches()) { const outcomes = await landBatch(batch); for (...) reply(outcome); }
13//! ```
14
15use crate::service_journal::{BatchClaim, DONE_RETAINED, Recovery, claim_batch, landed_commits, prune_done};
16use crate::service_land_follow::storage_failure;
17use crate::service_index_lock::{Announce, unsettled_locks};
18use crate::service_index_prestage::{PrestageGuard, start as start_prestage};
19use crate::service_captures::CaptureMemory;
20use crate::service_land::{LandContext, Outcome, land_batch_timed, recover, recover_instance};
21use crate::journald_native::Priority;
22use crate::service_timing::{Answered, Arrival, ExitReason, LandTiming, answered_line, exit_line, log, log_entry, retire_line};
23use crate::private_storage::remove_tree;
24use crate::service_instance::instance_of;
25use crate::claim_time::ClaimTime;
26use crate::service_retry::RetrySchedule;
27use crate::push_retry::{PLAN_DIRECTORY, PushRetries};
28use crate::service_work::{GitHandlers, Wake, run_round};
29use crate::service_busy::{EarlyAnswer, LandingBoard};
30use crate::service_reply::{Reply, write_until_answered_noting};
31use crate::wait_ceilings::NEVER_ENDED_STEP_SLOW_AFTER;
32use crate::service_protocol::{
33    ClientMessage, CommitRequest, FailureCode, HEARTBEAT_EVERY, MAX_LINE_BYTES, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_client, encode_service,
34};
35use std::collections::HashMap;
36use std::io::{BufRead, BufReader, Read, Write};
37use std::os::unix::net::{UnixListener, UnixStream};
38use std::sync::atomic::{AtomicUsize, Ordering};
39use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
40use std::sync::Arc;
41use std::path::PathBuf;
42use std::time::{Duration, Instant};
43
44/// The largest batch whose landing still costs about what its size predicts: 256. Measured with the service binary on throwaway
45///  repositories, 2026-11-03, bursts of simultaneous commit requests (`MAX_BATCH` raised to 4096 for the probe, three rounds
46///  alternating over 64, 128, 256, 512 and 1024 clients, other sessions running, a debug build): a landing took 71 to 76 ms for
47///  62 to 63 requests, 82 to 85 ms for 126, 104 to 109 ms for 253 to 255, 193 to 204 ms for 509 to 511 (once 2,002 ms, a host stall)
48///  and 525 to 547 ms for 1,021 to 1,023. The cost of each added request is 0.16 ms up to 254 requests, 0.36 ms from there to
49///  510, and 0.66 ms to 1,022, so the cost per request has not yet doubled at 256. The time a client waits for a burst of 128 was
50///  176 to 206 ms in one landing and 243 to 249 ms in three at the old cap of 64.
51pub const BATCH_COST_KNEE: usize = 256;
52
53/// The most requests landed as one batch: `BATCH_COST_KNEE`. A batch is answered all at once and holds the index lock while it
54///  lands, so the cap bounds both. The prototype's burst reached 64, and production never reached more than one request in a
55///  batch (1,302 answered requests, 2026-11-02T09:01:38Z to 2026-11-03T15:11:58Z), so the cap bites only in a burst. Option B of the
56///  batch size question, picked by the coordinator under the owner's delegation, open to the owner's veto
57///  (`doc/planning/cli-git-shipped-limits-audit.md`, "Picked for these follow-ups").
58pub const MAX_BATCH: usize = BATCH_COST_KNEE;
59
60/// How long a connection may take to send its request line before it is dropped.
61pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
62
63/// How long the service waits with nothing to do before it exits; the manager's socket stays, so the next connection starts it
64///  again. Bazel's server does the same with `--max_idle_secs`, whose default is three hours, which is this value's precedent.
65///  Measured cost of exceeding it: of 1,051 commits in the commit timing log (2026-11-02T09:01:37Z to 2026-11-03T15:10:12Z), the 10
66///  that followed an idle gap of more than three hours reached the service in at most 70 ms (`reachMs`) after a start wait of at
67///  most 47 ms (`startWaitMs`); 110 idle exits happened in that window. The 28 running services held 212 MiB together (7.9 MiB each,
68///  `systemctl --user show --property=MemoryCurrent`). A shorter exit would trade at most that start for that memory; the owner's
69///  bound is that a commit should not take 5 s longer than native Git (`SPEC.md`). Kept at three hours as the coordinator's pick under the owner's delegation, open to the owner's veto (`doc/planning/cli-git-shipped-limits-audit.md`, "Sizes and ages of the service's own housekeeping").
70pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
71
72/// File descriptor the service manager passes the listening socket on (`SD_LISTEN_FDS_START`).
73pub const INHERITED_DESCRIPTOR: i32 = 3;
74
75/// How a service run behaves apart from where it listens and what it lands.
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct ServeSettings {
78    /// How long the service waits with nothing to do before it exits.
79    pub idle: Duration,
80    /// The release the service announces to every client. It is this build's release (`RELEASE`) except in a test that
81    ///  needs a service of another release.
82    pub release: String,
83    /// The worktree this instance serves, as `service_instance::instance_of` names it; every message about another worktree
84    ///  is refused. Nothing in a development run that serves any worktree.
85    pub instance: Option<String>,
86    /// How long a write the service never ends runs before the clients behind it are told, and may be withdrawn while the
87    ///  service is inside it (`wait_ceilings.rs`, `NEVER_ENDED_STEP_SLOW_AFTER`); a control shortens it.
88    pub stuck_after: Duration,
89}
90
91impl ServeSettings {
92    /// The settings of an ordinary run: the given idle exit and this build's release.
93    pub fn new(idle: Duration) -> Self {
94        return Self { idle, release: String::from(RELEASE), instance: None, stuck_after: NEVER_ENDED_STEP_SLOW_AFTER };
95    }
96}
97
98/// Where the listening socket comes from.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum Bind {
101    /// A Linux abstract name: one binder at a time, gone with the process, no file to go stale.
102    Abstract(String),
103    /// A path in a private directory, for platforms without abstract names.
104    Path(std::path::PathBuf),
105    /// The socket the service manager opened and passed in.
106    Inherited,
107}
108
109/// What:
110///  Opens the listening socket.
111/// Why:
112///   Socket activation hands over a bound socket, which is the normal way; the others serve tests and development runs.
113///
114/// In TS you'd write (pseudocode):
115/// ```ts
116/// const listener = bind(address)
117/// ```
118pub fn bind(address: &Bind) -> std::io::Result<UnixListener> {
119    return match address {
120        #[cfg(target_os = "linux")]
121        Bind::Abstract(name) => {
122            use std::os::linux::net::SocketAddrExt;
123            let socket = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?;
124            UnixListener::bind_addr(&socket)
125        }
126        #[cfg(not(target_os = "linux"))]
127        Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
128        Bind::Path(path) => bind_path(path),
129        Bind::Inherited => {
130            use std::os::fd::FromRawFd;
131            ensure_socket_passed_in(std::env::var("LISTEN_PID").ok().as_deref(), std::env::var("LISTEN_FDS").ok().as_deref(), std::process::id())?;
132            // SAFETY: the manager passed this descriptor to this process and nothing else in the process owns it.
133            Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
134        }
135    };
136}
137
138/// What:
139///  Checks the service manager's socket-activation variables:
140/// `listen_pid` must name this process (`own_pid`) and
141///  `listen_fds` must count at least one descriptor.
142/// Why:
143///   The descriptor the manager passes is taken over as the listening socket,
144/// so a variable set for another process,
145/// or a
146///  count of none,
147/// must be refused instead of adopting a descriptor that was never passed to this process.
148///
149/// In TS you'd write (pseudocode):
150/// ```ts
151/// if (listenPid !== String(ownPid) || !(Number.parseInt(listenFds ?? '0') >= 1)) { throw new Error('no socket was passed in') }
152/// ```
153fn ensure_socket_passed_in(listen_pid: Option<&str>, listen_fds: Option<&str>, own_pid: u32) -> std::io::Result<()> {
154    let announced_for: bool = listen_pid.is_some_and(|pid| { return pid == own_pid.to_string(); });
155    let count: i32 = listen_fds.and_then(|text| { return text.parse().ok(); }).unwrap_or(0);
156    if !announced_for || count < 1 {
157        return Err(std::io::Error::other("no socket was passed in"));
158    }
159    return Ok(());
160}
161
162/// What:
163///  Binds a socket file, taking over one that no service listens on.
164/// Why:
165///   A service that was killed leaves its socket file behind, and the next bind of the same path then fails with
166///  "Address already in use" although nothing answers there. Only a socket that refuses a connection is replaced: one that
167///  answers belongs to a running service, and any other file is the person's. Two services started on one path at the same
168///  moment are the caller's to avoid (the service manager's socket, which production uses, never comes here).
169///
170/// In TS you'd write (pseudocode):
171/// ```ts
172/// try { return listen(path) } catch (inUse) { if (await refusesConnections(path)) { unlink(path); return listen(path) } throw inUse }
173/// ```
174fn bind_path(path: &std::path::Path) -> std::io::Result<UnixListener> {
175    use std::os::unix::fs::FileTypeExt;
176    let first: std::io::Error = match UnixListener::bind(path) {
177        Ok(listener) => return Ok(listener),
178        Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => error,
179        Err(error) => return Err(error),
180    };
181    let is_socket: bool = std::fs::symlink_metadata(path).is_ok_and(|meta| { return meta.file_type().is_socket(); });
182    let refused: bool = matches!(UnixStream::connect(path), Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused);
183    if !is_socket || !refused {
184        return Err(first);
185    }
186    std::fs::remove_file(path)?;
187    return UnixListener::bind(path);
188}
189
190/// The user id of the process on the other end of a connection.
191fn peer_user(stream: &UnixStream) -> Option<u32> {
192    use std::os::fd::AsRawFd;
193    let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
194    let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
195    // SAFETY: `credentials` and `length` are valid for the call and `length` is its size.
196    let status = unsafe {
197        libc::getsockopt(stream.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PEERCRED, (&raw mut credentials).cast(), &raw mut length)
198    };
199    return if status == 0 { Some(credentials.uid) } else { None };
200}
201
202/// A request waiting to be landed:
203/// what the client sent,
204/// the connection to answer it on,
205/// and how it arrived.
206type Queued = (Box<CommitRequest>, Reply, Arrival);
207
208/// What a landing group shares:
209/// the common Git directory,
210/// the queue reference,
211/// and whether its request lands alone.
212type GroupKey = (std::path::PathBuf, String, bool);
213
214/// What:
215///  Puts `member` into the latest group of its common Git directory and queue reference,
216/// or into a group of its own when
217///  it lands alone,
218/// when that group's request does,
219/// or when no group of that branch exists yet.
220/// Why:
221///   Requests of one branch that may share a batch land together and in arrival order;
222/// an amend is a group of its own and
223///  what arrives after it starts another group,
224/// so the order on one branch is the order of arrival,
225/// and a request never
226///  lands in a batch of another branch.
227///
228/// In TS you'd write (pseudocode):
229/// ```ts
230/// const joined = groups.findLast((group) => group.common === key.common && group.reference === key.reference)
231/// if (joined && !key.alone && !joined.alone) { joined.members.push(member) } else { groups.push({ key, members: [member] }) }
232/// ```
233fn place_in_groups<T>(groups: &mut Vec<(GroupKey, Vec<T>)>, key: GroupKey, member: T) {
234    let joins = groups.iter_mut().rev().find(|((common, reference, _), _)| { return *common == key.0 && *reference == key.1; });
235    match joins {
236        Some((existing, members)) if !key.2 && !existing.2 => members.push(member),
237        _ => groups.push((key, vec![member])),
238    }
239}
240
241enum Job {
242    Commit(Box<CommitRequest>, Reply, Arrival),
243    Retire,
244    /// The client stopped waiting for the commit of this identity (its ceiling passed): answered with the landed commit, or
245    ///  withdrawn so that it never lands.
246    Withdraw(String, Reply),
247    /// After-commit work was announced; it only restarts the idle clock.
248    Activity,
249    /// A client that held a commit's scratch directory is gone; the directory is removed between batches, never during a
250    ///  landing that may read it.
251    Release(std::path::PathBuf),
252}
253
254/// Returns when the other end of the connection has closed it, or the connection failed. Anything the client sends is ignored.
255fn wait_for_close(stream: &UnixStream) {
256    let mut sink: [u8; 64] = [0; 64];
257    let mut reader = stream;
258    loop {
259        match reader.read(&mut sink) {
260            Ok(0) | Err(_) => return,
261            Ok(_) => {}
262        }
263    }
264}
265
266fn send(stream: &UnixStream, message: &ServiceMessage) {
267    let mut line: String = encode_service(message);
268    line.push('\n');
269    // A client that went away has nothing to be told.
270    let _written = (&*stream).write_all(line.as_bytes());
271}
272
273/// Starts a thread the service cannot do without, or ends the service with a plain line and status 4: a service that goes on
274///  without its acceptor or its work thread answers no one, while an ended service is started again by the manager.
275fn start_thread_or_exit(what: &str, body: impl FnOnce() + Send + 'static) {
276    if let Err(error) = std::thread::Builder::new().spawn(body) {
277        crate::service_error!("cannot start the thread for {what}: {error}");
278        std::process::exit(4);
279    }
280}
281
282/// How many withdrawn identities the service remembers before it forgets them all: the same count as the finished batches it
283///  remembers (`DONE_RETAINED`), since a client that withdrew sends nothing more for its request.
284const WITHDRAWN_RETAINED: usize = DONE_RETAINED;
285
286/// How long the acceptor waits after a connection it could not give a thread.
287const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
288
289/// What:
290///  The note for a connection's beat: what the landing thread is stuck in when it is inside a write that is never ended, said again
291///  only when the words change (they name whole minutes).
292/// Why:
293///   A client queued behind a write that never returns is told why it waits, and the words become the blocker its error names.
294///
295/// In TS you'd write (pseudocode):
296/// ```ts
297/// () => { const words = board.describe(); return words !== last ? (last = words, { type: 'waiting', text: words }) : undefined; }
298/// ```
299fn stuck_notice<'a>(board: &'a LandingBoard, request: Option<&'a str>) -> impl FnMut() -> Option<ServiceMessage> + 'a {
300    let mut last: Option<(String, bool)> = None;
301    return move || {
302        let words: String = board.describe()?;
303        // A client whose own request is in the group being landed is told so, because its commit may be on the branch already.
304        let inside: bool = request.is_some_and(|id| { return board.is_in_flight(id); });
305        if last.as_ref().is_some_and(|(sent, was_inside)| { return *sent == words && *was_inside == inside; }) {
306            return None;
307        }
308        last = Some((words.clone(), inside));
309        return Some(if inside { ServiceMessage::InsideWrite { text: words } } else { ServiceMessage::Waiting { text: words } });
310    };
311}
312
313/// Reads one request line from a new connection, checks who sent it, and queues it.
314fn handle_connection(stream: UnixStream, queue: &Sender<Job>, work: &Sender<Wake>, busy: &AtomicUsize, board: &LandingBoard, release: &str, instance: Option<&str>) {
315    let accepted: Instant = Instant::now();
316    send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(release.to_string()) });
317    // SAFETY: `geteuid` takes no arguments and cannot fail.
318    if peer_user(&stream) != Some(unsafe { libc::geteuid() }) {
319        return;
320    }
321    let _timeout = stream.set_read_timeout(Some(REQUEST_READ_TIMEOUT));
322    let Ok(reader_stream) = stream.try_clone() else { return };
323    let mut line: String = String::new();
324    let limit: u64 = u64::try_from(MAX_LINE_BYTES).unwrap_or(u64::MAX) + 1;
325    if BufReader::new(reader_stream.take(limit)).read_line(&mut line).is_err() {
326        return;
327    }
328    let _cleared = stream.set_read_timeout(None);
329    if line.is_empty() {
330        // A connection that only looked (a readiness probe) sends nothing and is not an error.
331        return;
332    }
333    // A message about another worktree is not this instance's to act on: each worktree has its own instance, so one that
334    //  reaches the wrong socket is a mistake of the sender's, refused where it can be seen.
335    let foreign = |git_dir: &std::path::Path| -> bool { return instance.is_some_and(|mine| { return instance_of(git_dir) != mine; }); };
336    match decode_client(line.trim_end_matches('\n')) {
337        Ok(ClientMessage::Commit(request)) if foreign(request.git_dir.as_path()) => {
338            crate::service_warn!("refused a commit for {}, which another instance serves", request.git_dir.display());
339            send(&stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Rejected });
340        }
341        Ok(ClientMessage::Scratch { git_dir, .. } | ClientMessage::Work { git_dir, .. }) if foreign(git_dir.as_path()) => {
342            crate::service_warn!("refused a message for {}, which another instance serves", git_dir.display());
343            send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
344        }
345        Ok(ClientMessage::Commit(request)) => {
346            let id: String = request.id.clone();
347            let (reply, messages) = Reply::new();
348            let _queued = queue.send(Job::Commit(request, reply, Arrival { accepted, read: accepted.elapsed() }));
349            crate::service_fault::queued(id.as_str());
350            // This thread has nothing else to do until the request is answered, so it writes the answers the landing thread
351            //  queues and keeps the client told that the service lives, and what it is stuck in when it is stuck in a write.
352            write_until_answered_noting(&stream, &messages, HEARTBEAT_EVERY, &mut stuck_notice(board, Some(id.as_str())));
353        }
354        Ok(ClientMessage::Withdraw { id }) => {
355            // Behind a write that never returns the landing thread answers nothing, so a request that has not begun to land is
356            //  withdrawn here (`service_busy.rs`); otherwise the landing thread answers between two landings, so the answer is
357            //  either that the commit landed or that it never will, and meanwhile this thread keeps the client told that the
358            //  service lives.
359            match board.withdraw_early(id.as_str()) {
360                EarlyAnswer::Landed(commit) => send(&stream, &ServiceMessage::Landed { id, commit, already: true }),
361                EarlyAnswer::Withdrawn(words) => {
362                    send(&stream, &ServiceMessage::Waiting { text: words });
363                    send(&stream, &ServiceMessage::Withdrawn { id });
364                }
365                EarlyAnswer::Queued => {
366                    let (reply, messages) = Reply::new();
367                    let _queued = queue.send(Job::Withdraw(id, reply));
368                    write_until_answered_noting(&stream, &messages, HEARTBEAT_EVERY, &mut stuck_notice(board, None));
369                }
370            }
371        }
372        Ok(ClientMessage::Retire) => {
373            log(retire_line(release).as_str());
374            let _queued = queue.send(Job::Retire);
375        }
376        Ok(ClientMessage::Scratch { git_dir, path }) => {
377            // The connection is the client's claim on the directory. While it is open the service is not idle, and when it
378            //  closes, by the client's end or its death, the directory is no one's.
379            busy.fetch_add(1, Ordering::SeqCst);
380            wait_for_close(&stream);
381            busy.fetch_sub(1, Ordering::SeqCst);
382            let _unused = git_dir;
383            let _queued = queue.send(Job::Release(path));
384        }
385        Ok(ClientMessage::Work { git_dir, review_key }) => {
386            let _queued = work.send((git_dir, review_key));
387            let _touched = queue.send(Job::Activity);
388        }
389        Err(reason) => {
390            crate::service_warn!("refused a request line: {reason}");
391            send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
392        }
393    }
394}
395
396/// Finishes the open batches this instance owns: those of its worktree when it serves one, all of them in a development run.
397fn settle_open_batches(context: &LandContext, instance: Option<&str>) -> Vec<crate::service_land::RecoveryReport> {
398    let reports: Vec<crate::service_land::RecoveryReport> = match instance {
399        Some(mine) => recover_instance(context, mine),
400        None => recover(context),
401    };
402    // An open batch means an earlier service stopped between its intent and its end; each one settled is said.
403    for report in &reports {
404        log_entry(
405            Priority::Warning,
406            format!("settled the batch an earlier service left open batch={} decision={}", report.seq, if report.decision == Recovery::Finish { "finish" } else { "drop" }).as_str(),
407            &[("CLI_GIT_BATCH", report.seq.to_string())],
408        );
409    }
410    return reports;
411}
412
413/// The times of one batch that every request of it shares,
414/// and the moment the landing ended.
415struct BatchTimes {
416    /// The batch number,
417    /// zero when no batch ran (a request answered as already landed).
418    batch: u64,
419    /// How many requests of the branch the batch held.
420    size: usize,
421    /// When the group began to be handled,
422    /// which ends every request's wait in the queue.
423    group_began: Instant,
424    /// Time to settle the batches a killed service left open.
425    settle: Duration,
426    /// The phases of the landing.
427    landing: LandTiming,
428    /// Time the landing took,
429    /// from its start to its return.
430    land_total: Duration,
431    /// When the landing returned,
432    /// which starts the answers.
433    landed_at: Instant,
434}
435
436impl BatchTimes {
437    /// The times of a request that no batch landed.
438    fn not_landed(group_began: Instant) -> Self {
439        return Self { batch: 0, size: 0, group_began, settle: Duration::ZERO, landing: LandTiming::default(), land_total: Duration::ZERO, landed_at: Instant::now() };
440    }
441}
442
443/// Writes the log line for an answered request:
444/// the phases of its wait,
445/// from the acceptance of its connection to the answer.
446fn log_answer(times: &BatchTimes, request: &CommitRequest, arrival: &Arrival, position: usize, outcome: &str) {
447    let queued: Duration = times.group_began.saturating_duration_since(arrival.accepted + arrival.read);
448    // What the Git steps and tools since the previous answer showed; the answer that finds them takes them.
449    let quiet: Vec<(String, Duration)> = crate::quiet_ledger::drain();
450    let line: String = answered_line(&Answered {
451        request: request.id.as_str(),
452        reference: request.reference.as_str(),
453        outcome,
454        batch: times.batch,
455        size: times.size,
456        position,
457        arrival: *arrival,
458        queued,
459        settle: times.settle,
460        landing: &times.landing,
461        land_total: times.land_total,
462        answer: times.landed_at.elapsed(),
463        total: arrival.accepted.elapsed(),
464        quiet: quiet.as_slice(),
465    });
466    // A request that was answered with a failure is a warning, so `journalctl --priority=warning` finds the commits that failed.
467    let priority: Priority = if outcome.starts_with("failed") { Priority::Warning } else { Priority::Info };
468    let fields: [(&str, String); 4] = [
469        ("CLI_GIT_REQUEST_ID", request.id.clone()),
470        ("CLI_GIT_BRANCH", request.reference.clone()),
471        ("CLI_GIT_OUTCOME", outcome.to_string()),
472        ("CLI_GIT_BATCH", times.batch.to_string()),
473    ];
474    log_entry(priority, line.as_str(), &fields);
475}
476
477/// Lands one group of jobs that share a branch, and answers each client.
478fn land_group(context: &LandContext, instance: Option<&str>, seq: &mut u64, group: Vec<Queued>, landed: &mut HashMap<String, String>, captures: &mut CaptureMemory) {
479    let group_began: Instant = Instant::now();
480    let (again, pending): (Vec<_>, Vec<_>) = group.into_iter().partition(|(request, _, _)| { return landed.contains_key(&request.id); });
481    let unlanded: BatchTimes = BatchTimes::not_landed(group_began);
482    for (request, stream, arrival) in again {
483        stream.send(&ServiceMessage::Landed { id: request.id.clone(), commit: landed[&request.id].clone(), already: true });
484        log_answer(&unlanded, &request, &arrival, 1, "already");
485    }
486    // The same request sent twice into one batch (a client that reconnected) is landed once and answered twice.
487    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
488    let (fresh, repeats): (Vec<_>, Vec<_>) = pending.into_iter().partition(|(request, _, _)| { return seen.insert(request.id.clone()); });
489    if fresh.is_empty() {
490        return;
491    }
492    // A batch whose branch moved but whose index update had to give up (another program held Git's index lock the whole
493    //  time) left its intent open; settling it first puts the index right before anything else changes it.
494    let settling: Instant = Instant::now();
495    let _settled = settle_open_batches(context, instance);
496    let settle: Duration = settling.elapsed();
497    // The number is taken from the journal, not from this service's count alone: another service of the same journal may count
498    //  from the same clock reading, and two landings with one number would overwrite each other's records.
499    let claim: BatchClaim = match claim_batch(context.journal.as_path(), *seq + 1) {
500        Ok(claim) => claim,
501        Err(error) => {
502            let code: FailureCode = storage_failure(&error);
503            crate::service_error!("no batch number could be taken in {}: {error}", context.journal.display());
504            for (position, (request, stream, arrival)) in fresh.iter().enumerate() {
505                stream.send(&ServiceMessage::Failed { id: request.id.clone(), code });
506                log_answer(&unlanded, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
507            }
508            return;
509        }
510    };
511    *seq = claim.seq();
512    for (index, (_, stream, _)) in fresh.iter().enumerate().skip(1) {
513        stream.send(&ServiceMessage::Position { number: index + 1, of: fresh.len() });
514    }
515    let requests: Vec<CommitRequest> = fresh.iter().map(|(request, _, _)| { return (**request).clone(); }).collect();
516    let announce = |note: Announce| {
517        for (_, stream, _) in &fresh {
518            match &note {
519                Announce::Waiting(text) => stream.send(&ServiceMessage::Waiting { text: text.clone() }),
520                Announce::Holder(who) => stream.send(&ServiceMessage::Holder { who: who.clone() }),
521            }
522        }
523    };
524    let mut landing: LandTiming = LandTiming::default();
525    let landing_began: Instant = Instant::now();
526    let result = land_batch_timed(context, *seq, &requests, &announce, &mut landing);
527    // The intent or the finished batch keeps the number now; a landing that wrote none gives the number back.
528    drop(claim);
529    let times: BatchTimes = BatchTimes { batch: *seq, size: fresh.len(), group_began, settle, landing, land_total: landing_began.elapsed(), landed_at: Instant::now() };
530    match result {
531        Ok(outcomes) => {
532            // What this batch landed is remembered before any answer, so a commit sent back in the same batch is told about it.
533            for ((request, _, _), outcome) in fresh.iter().zip(&outcomes) {
534                if let Outcome::Landed { commit } = outcome {
535                    captures.record_landed(request.common_dir.as_path(), request.repository.as_path(), commit.as_str(), request.captured);
536                }
537            }
538            crate::service_fault::reach("before-ack");
539            for ((request, _, _), outcome) in fresh.iter().zip(&outcomes) {
540                if let Outcome::Landed { commit } = outcome {
541                    for (repeat, stream, arrival) in repeats.iter().filter(|(repeat, _, _)| { return repeat.id == request.id; }) {
542                        stream.send(&ServiceMessage::Landed { id: repeat.id.clone(), commit: commit.clone(), already: true });
543                        log_answer(&times, repeat, arrival, 1, "already");
544                    }
545                }
546            }
547            for (position, ((request, stream, arrival), outcome)) in fresh.iter().zip(outcomes).enumerate() {
548                let word: &str = match outcome {
549                    Outcome::Landed { commit } => {
550                        landed.insert(request.id.clone(), commit.clone());
551                        stream.send(&ServiceMessage::Landed { id: request.id.clone(), commit, already: false });
552                        "landed"
553                    }
554                    Outcome::RetryPrepare { base } => {
555                        let known = captures.remembered(request.common_dir.as_path(), request.repository.as_path());
556                        stream.send(&ServiceMessage::RetryPrepare { id: request.id.clone(), base, landed: known });
557                        "retry-prepare"
558                    }
559                    Outcome::BranchMoved => {
560                        stream.send(&ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::BranchMoved });
561                        "branch-moved"
562                    }
563                };
564                log_answer(&times, request, arrival, position + 1, word);
565            }
566        }
567        Err(code) => {
568            for (position, (request, stream, arrival)) in fresh.iter().enumerate() {
569                stream.send(&ServiceMessage::Failed { id: request.id.clone(), code });
570                log_answer(&times, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
571            }
572        }
573    }
574}
575
576/// What:
577///  Runs the after-commit work of each repository named, one repository at a time, folding wakes that arrive while it
578///  works into the next round, and looks again at a repository whose claims another instance held when their lease is over.
579/// Why:
580///   Draining claims everything pending, so a burst of wakes for one repository is one pass. The instance that held a claim
581///  may have died, and nothing but this loop would then wake the work: it waits for a wake no longer than the first lease still
582///  running, and the service counts itself busy meanwhile, so it does not exit idle with work claimed by a dead holder. The same
583///  holds for a push the service stopped: the loop owns the plan of its new tries (`PushRetries`), each round hands the workers
584///  what is due and applies what they ask, and the wake for the earliest plan is kept in the same schedule as the lease wakes, so
585///  the service neither forgets the plan nor exits idle while a try is pending.
586///
587/// In TS you'd write (pseudocode):
588/// ```ts
589/// for await (const dir of wakesOrDueRetries) await drain(dir, handlers)
590/// ```
591pub fn work_loop(receiver: &Receiver<Wake>, handlers: &GitHandlers, busy: &AtomicUsize, mut push_retries: PushRetries) {
592    let mut retries: RetrySchedule = RetrySchedule::default();
593    let mut counted_waiting: bool = false;
594    // A plan that is already there when the loop starts (one rebuilt from disk) is woken for like one made by a round.
595    let started: Instant = Instant::now();
596    let restored: Vec<(PathBuf, Duration)> = push_retries
597        .directories()
598        .into_iter()
599        .filter_map(|directory| { return push_retries.next_wait(directory.as_path(), started).map(|wait| { return (directory, wait); }); })
600        .collect();
601    retries.replace(&[], restored, started);
602    if !retries.is_empty() {
603        busy.fetch_add(1, Ordering::SeqCst);
604        counted_waiting = true;
605    }
606    loop {
607        let first: Option<Wake> = match retries.next_wait(Instant::now()) {
608            None => match receiver.recv() {
609                Ok(wake) => Some(wake),
610                Err(_) => return,
611            },
612            Some(wait) => match receiver.recv_timeout(wait) {
613                Ok(wake) => Some(wake),
614                Err(RecvTimeoutError::Timeout) => None,
615                Err(RecvTimeoutError::Disconnected) => return,
616            },
617        };
618        busy.fetch_add(1, Ordering::SeqCst);
619        let mut wakes: Vec<Wake> = first.into_iter().chain(std::iter::from_fn(|| { return receiver.try_recv().ok(); })).collect();
620        wakes.extend(retries.take_due(Instant::now()).into_iter().map(|directory| { return (directory, None); }));
621        let mut ran: Vec<PathBuf> = wakes.iter().map(|(directory, _)| { return directory.clone(); }).collect();
622        let asked: Vec<(PathBuf, Duration)> = run_round(wakes, handlers, &mut push_retries, &ClaimTime::read());
623        // A directory the round named for its plan alone (the common directory of a bare repository) replaces its old entry too.
624        ran.extend(asked.iter().map(|(directory, _)| { return directory.clone(); }));
625        retries.replace(ran.as_slice(), asked, Instant::now());
626        // A repository waiting to be looked at again keeps the service from exiting idle, as running work does.
627        if !retries.is_empty() && !counted_waiting {
628            busy.fetch_add(1, Ordering::SeqCst);
629            counted_waiting = true;
630        } else if retries.is_empty() && counted_waiting {
631            busy.fetch_sub(1, Ordering::SeqCst);
632            counted_waiting = false;
633        }
634        busy.fetch_sub(1, Ordering::SeqCst);
635    }
636}
637
638/// What:
639///  Runs the service until it is told to retire.
640/// Why:
641///   Recovery runs before the first connection is accepted; then the loop takes everything queued as one batch, groups it
642///  by branch in order of first arrival, lands each group, and answers.
643///
644/// In TS you'd write (pseudocode):
645/// ```ts
646/// await serve(listener, context)
647/// ```
648pub fn serve(listener: UnixListener, context: &LandContext, settings: &ServeSettings) {
649    let serving_since: Instant = Instant::now();
650    let idle: Duration = settings.idle;
651    let _reports = settle_open_batches(context, settings.instance.as_deref());
652    // The memory of landed requests is bounded: the oldest finished batches go before the rest are read.
653    let pruned: usize = prune_done(context.journal.as_path(), DONE_RETAINED);
654    if pruned > 0 {
655        crate::service_log!("removed the records of {pruned} old finished batches from {}", context.journal.display());
656    }
657    let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
658    // The identities of the requests their clients withdrew; a request with one of these is never landed.
659    let mut withdrawn: std::collections::HashSet<String> = std::collections::HashSet::new();
660    // What the landed commits were captured at: owned by this loop, the only thread that lands and answers.
661    let mut captures: CaptureMemory = CaptureMemory::default();
662    let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
663    let acceptor_queue: Sender<Job> = sender.clone();
664    // After-commit work (push, upkeep) runs on its own thread, so a slow network never delays a landing.
665    let (work_sender, work_receiver): (Sender<Wake>, Receiver<Wake>) = channel();
666    let handlers: GitHandlers = GitHandlers::new(context.git.clone());
667    let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
668    let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
669    // What this landing thread is inside, for the threads that talk to clients (`service_busy.rs`).
670    let board: Arc<LandingBoard> = LandingBoard::new(context.journal.clone(), settings.stuck_after);
671    crate::service_busy::use_on_this_thread(Arc::clone(&board));
672    let connection_board: Arc<LandingBoard> = Arc::clone(&board);
673    // The plan of the pushes waiting for a new try is kept in this instance's own journal, so a restarted service rebuilds it.
674    let plan_directory: PathBuf = context.journal.join(PLAN_DIRECTORY);
675    start_thread_or_exit("the after-commit work", move || { work_loop(&work_receiver, &handlers, &worker_busy, PushRetries::restored_from_environment(plan_directory.as_path())); });
676    let announced_release: String = settings.release.clone();
677    let connection_busy: Arc<AtomicUsize> = Arc::clone(&busy);
678    let instance: Option<String> = settings.instance.clone();
679    start_thread_or_exit("the acceptor of connections", move || {
680        for connection in listener.incoming().flatten() {
681            let queue: Sender<Job> = acceptor_queue.clone();
682            let work: Sender<Wake> = work_sender.clone();
683            let release: String = announced_release.clone();
684            let counted: Arc<AtomicUsize> = Arc::clone(&connection_busy);
685            let record: Arc<LandingBoard> = Arc::clone(&connection_board);
686            let mine: Option<String> = instance.clone();
687            // A connection the service cannot give a thread is told so and closed; the acceptor goes on, because a panic here
688            //  would end the acceptor and leave a service that is running and answers no one.
689            let notice: Option<UnixStream> = connection.try_clone().ok();
690            let spawned = std::thread::Builder::new().spawn(move || { handle_connection(connection, &queue, &work, &counted, &record, release.as_str(), mine.as_deref()); });
691            if let Err(error) = spawned {
692                crate::service_error!("no thread for a connection, which is closed after telling the client: {error}");
693                if let Some(stream) = notice {
694                    send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(announced_release.clone()) });
695                    send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Unavailable });
696                }
697                // Starting a thread keeps failing while the pressure lasts; waiting a moment keeps the acceptor from spinning on it.
698                std::thread::sleep(ACCEPT_BACKOFF);
699            }
700        }
701    });
702    // Microseconds, so that instances started at different times count from different numbers; the number each batch takes is
703    //  still claimed in the journal (`claim_batch`), because equal readings and counts that overtake another's are possible.
704    let clock_seed: u64 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(1, |elapsed| { return u64::try_from(elapsed.as_micros()).unwrap_or(1); });
705    let mut seq: u64 = crate::service_fault::pinned_first_batch().unwrap_or(clock_seed);
706    // The marker made ahead is removed whichever way this loop ends.
707    let _prestaged = PrestageGuard;
708    let mut retiring: bool = false;
709    while !retiring {
710        let first: Job = match receiver.recv_timeout(idle) {
711            Ok(job) => job,
712            // Work in flight keeps the service; otherwise it exits and the manager starts it on the next connection.
713            Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
714            Err(RecvTimeoutError::Timeout) => {
715                // Leaving: the syncs of the landings that were answered are made before the service goes (a signal cannot wait for them).
716                crate::service_durable::drain();
717                log(exit_line(ExitReason::Idle, serving_since.elapsed()).as_str());
718                return;
719            }
720            Err(RecvTimeoutError::Disconnected) => {
721                // Leaving: the syncs of the landings that were answered are made before the service goes (a signal cannot wait for them).
722                crate::service_durable::drain();
723                log(exit_line(ExitReason::Error, serving_since.elapsed()).as_str());
724                return;
725            }
726        };
727        let mut jobs: Vec<Job> = vec![first];
728        while jobs.len() < MAX_BATCH {
729            match receiver.try_recv() {
730                Ok(job) => jobs.push(job),
731                Err(_) => break,
732            }
733        }
734        // Withdrawals are answered before anything lands, on this one thread, so each is either after a landing that is over (the
735        //  commit is on the branch, and that is the answer) or before the landing of its request begins (the request is dropped
736        //  and remembered, so it can never land, not even when it reaches the queue after the withdrawal).
737        if withdrawn.len() > WITHDRAWN_RETAINED {
738            withdrawn.clear();
739        }
740        for job in &jobs {
741            if let Job::Withdraw(id, reply) = job {
742                match landed.get(id) {
743                    Some(commit) => reply.send(&ServiceMessage::Landed { id: id.clone(), commit: commit.clone(), already: true }),
744                    None => {
745                        withdrawn.insert(id.clone());
746                        reply.send(&ServiceMessage::Withdrawn { id: id.clone() });
747                    }
748                }
749            }
750        }
751        // Branches in order of first arrival; within a branch, arrival order.
752        let mut groups: Vec<(GroupKey, Vec<Queued>)> = Vec::new();
753        let mut releases: Vec<std::path::PathBuf> = Vec::new();
754        for job in jobs {
755            match job {
756                Job::Withdraw(..) => {}
757                Job::Commit(request, _dropped, _arrival) if withdrawn.contains(&request.id) => {
758                    crate::service_log!("dropped the request {} that its client withdrew", request.id);
759                }
760                Job::Retire => retiring = true,
761                Job::Activity => {}
762                Job::Release(path) => releases.push(path),
763                Job::Commit(request, stream, arrival) => {
764                    // An amend is a group of its own, and what arrives after it starts another group, so the order on
765                    //  one branch is the order of arrival.
766                    // `HEAD` is a head of one worktree, so two detached worktrees of one repository are two branches.
767                    let queue_reference: String = if request.reference == "HEAD" {
768                        format!("HEAD@{}", request.repository.display())
769                    } else {
770                        request.reference.clone()
771                    };
772                    let key: GroupKey = (request.common_dir.clone(), queue_reference, request.lands_alone());
773                    place_in_groups(&mut groups, key, (request, stream, arrival));
774                }
775            }
776        }
777        for (_, queued_group) in groups {
778            let index: Option<std::path::PathBuf> = queued_group.first().map(|(request, _, _)| { return request.index.clone(); });
779            // A request whose client was answered "withdrawn" while this thread was stuck in a write is dropped here, and the
780            //  rest are marked as being landed, under one lock (`service_busy.rs`).
781            let ids: Vec<String> = queued_group.iter().map(|(request, _, _)| { return request.id.clone(); }).collect();
782            let dropped: Vec<String> = board.begin_group(ids.as_slice());
783            for id in &dropped {
784                crate::service_log!("dropped the request {id} that its client withdrew while the service was stuck in a write");
785            }
786            let group: Vec<Queued> = queued_group.into_iter().filter(|(request, _, _)| { return !dropped.contains(&request.id); }).collect();
787            if group.is_empty() {
788                board.end_group(ids.as_slice());
789                continue;
790            }
791            land_group(context, settings.instance.as_deref(), &mut seq, group, &mut landed, &mut captures);
792            board.end_group(ids.as_slice());
793            // The batch after this one is numbered one more and lands on this index (a worktree has one), so its marker is
794            //  made and flushed now, while the service is idle, and the next commit only places it.
795            if let Some(next_index) = index {
796                start_prestage(next_index.as_path(), seq + 1);
797            }
798        }
799        // A lock this thread could not settle names this process: the restart reclaims it once this process is gone.
800        if unsettled_locks() > 0 {
801            crate::service_error!("ending, because an index lock could not be settled; the next connection starts the service again and its recovery reclaims the lock");
802            return;
803        }
804        for path in releases {
805            if let Err(error) = remove_tree(path.as_path()) {
806                crate::service_warn!("the scratch directory {} could not be removed: {error}", path.display());
807            }
808        }
809    }
810    // Leaving: the syncs of the landings that were answered are made before the service goes (a signal cannot wait for them).
811    crate::service_durable::drain();
812    log(exit_line(ExitReason::Retire, serving_since.elapsed()).as_str());
813}
814
815/// Socket controls stay out of the release executable.
816#[cfg(test)]
817#[path = "service_server_tests.rs"]
818mod tests;