Skip to main content

git_policy_cli/
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 super::service_journal::{DONE_RETAINED, Recovery, landed_commits, prune_done};
16use super::service_index_lock::{Announce, unsettled_locks};
17use super::service_index_prestage::{PrestageGuard, start as start_prestage};
18use super::service_land::{LandContext, Outcome, land_batch_timed, recover, recover_instance};
19use super::journald_native::Priority;
20use super::service_timing::{Answered, Arrival, ExitReason, LandTiming, answered_line, exit_line, log, log_entry, retire_line};
21use super::private_storage::remove_tree;
22use super::service_instance::instance_of;
23use super::claim_time::ClaimTime;
24use super::service_retry::RetrySchedule;
25use super::service_work::{GitHandlers, Wake, run_round};
26use super::service_protocol::{
27    ClientMessage, CommitRequest, FailureCode, MAX_LINE_BYTES, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_client, encode_service,
28};
29use std::collections::HashMap;
30use std::io::{BufRead, BufReader, Read, Write};
31use std::os::unix::net::{UnixListener, UnixStream};
32use std::sync::atomic::{AtomicUsize, Ordering};
33use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
34use std::sync::Arc;
35use std::path::PathBuf;
36use std::time::{Duration, Instant};
37
38/// The most requests landed as one batch.
39pub const MAX_BATCH: usize = 64;
40
41/// How long a connection may take to send its request line before it is dropped.
42pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
43
44/// How long the service waits with nothing to do before it exits; the manager's socket stays, so the next connection starts it
45///  again. Bazel's server does the same with `--max_idle_secs`, whose default is three hours, which is this value's precedent.
46pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
47
48/// File descriptor the service manager passes the listening socket on (`SD_LISTEN_FDS_START`).
49pub const INHERITED_DESCRIPTOR: i32 = 3;
50
51/// How a service run behaves apart from where it listens and what it lands.
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct ServeSettings {
54    /// How long the service waits with nothing to do before it exits.
55    pub idle: Duration,
56    /// The release the service announces to every client. It is this build's release (`RELEASE`) except in a test that
57    ///  needs a service of another release.
58    pub release: String,
59    /// The worktree this instance serves, as `service_instance::instance_of` names it; every message about another worktree
60    ///  is refused. Nothing in a development run that serves any worktree.
61    pub instance: Option<String>,
62}
63
64impl ServeSettings {
65    /// The settings of an ordinary run: the given idle exit and this build's release.
66    pub fn new(idle: Duration) -> Self {
67        return Self { idle, release: String::from(RELEASE), instance: None };
68    }
69}
70
71/// Where the listening socket comes from.
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub enum Bind {
74    /// A Linux abstract name: one binder at a time, gone with the process, no file to go stale.
75    Abstract(String),
76    /// A path in a private directory, for platforms without abstract names.
77    Path(std::path::PathBuf),
78    /// The socket the service manager opened and passed in.
79    Inherited,
80}
81
82/// What:
83///  Opens the listening socket.
84/// Why:
85///   Socket activation hands over a bound socket, which is the normal way; the others serve tests and development runs.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// const listener = bind(address)
90/// ```
91pub fn bind(address: &Bind) -> std::io::Result<UnixListener> {
92    return match address {
93        #[cfg(target_os = "linux")]
94        Bind::Abstract(name) => {
95            use std::os::linux::net::SocketAddrExt;
96            let socket = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?;
97            UnixListener::bind_addr(&socket)
98        }
99        #[cfg(not(target_os = "linux"))]
100        Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
101        Bind::Path(path) => bind_path(path),
102        Bind::Inherited => {
103            use std::os::fd::FromRawFd;
104            ensure_socket_passed_in(std::env::var("LISTEN_PID").ok().as_deref(), std::env::var("LISTEN_FDS").ok().as_deref(), std::process::id())?;
105            // SAFETY: the manager passed this descriptor to this process and nothing else in the process owns it.
106            Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
107        }
108    };
109}
110
111/// What:
112///  Checks the service manager's socket-activation variables:
113/// `listen_pid` must name this process (`own_pid`) and
114///  `listen_fds` must count at least one descriptor.
115/// Why:
116///   The descriptor the manager passes is taken over as the listening socket,
117/// so a variable set for another process,
118/// or a
119///  count of none,
120/// must be refused instead of adopting a descriptor that was never passed to this process.
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// if (listenPid !== String(ownPid) || !(Number.parseInt(listenFds ?? '0') >= 1)) { throw new Error('no socket was passed in') }
125/// ```
126fn ensure_socket_passed_in(listen_pid: Option<&str>, listen_fds: Option<&str>, own_pid: u32) -> std::io::Result<()> {
127    let announced_for: bool = listen_pid.is_some_and(|pid| { return pid == own_pid.to_string(); });
128    let count: i32 = listen_fds.and_then(|text| { return text.parse().ok(); }).unwrap_or(0);
129    if !announced_for || count < 1 {
130        return Err(std::io::Error::other("no socket was passed in"));
131    }
132    return Ok(());
133}
134
135/// What:
136///  Binds a socket file, taking over one that no service listens on.
137/// Why:
138///   A service that was killed leaves its socket file behind, and the next bind of the same path then fails with
139///  "Address already in use" although nothing answers there. Only a socket that refuses a connection is replaced: one that
140///  answers belongs to a running service, and any other file is the person's. Two services started on one path at the same
141///  moment are the caller's to avoid (the service manager's socket, which production uses, never comes here).
142///
143/// In TS you'd write (pseudocode):
144/// ```ts
145/// try { return listen(path) } catch (inUse) { if (await refusesConnections(path)) { unlink(path); return listen(path) } throw inUse }
146/// ```
147fn bind_path(path: &std::path::Path) -> std::io::Result<UnixListener> {
148    use std::os::unix::fs::FileTypeExt;
149    let first: std::io::Error = match UnixListener::bind(path) {
150        Ok(listener) => return Ok(listener),
151        Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => error,
152        Err(error) => return Err(error),
153    };
154    let is_socket: bool = std::fs::symlink_metadata(path).is_ok_and(|meta| { return meta.file_type().is_socket(); });
155    let refused: bool = matches!(UnixStream::connect(path), Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused);
156    if !is_socket || !refused {
157        return Err(first);
158    }
159    std::fs::remove_file(path)?;
160    return UnixListener::bind(path);
161}
162
163/// The user id of the process on the other end of a connection.
164fn peer_user(stream: &UnixStream) -> Option<u32> {
165    use std::os::fd::AsRawFd;
166    let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
167    let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
168    // SAFETY: `credentials` and `length` are valid for the call and `length` is its size.
169    let status = unsafe {
170        libc::getsockopt(stream.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PEERCRED, (&raw mut credentials).cast(), &raw mut length)
171    };
172    return if status == 0 { Some(credentials.uid) } else { None };
173}
174
175/// A request waiting to be landed:
176/// what the client sent,
177/// the connection to answer it on,
178/// and how it arrived.
179type Queued = (Box<CommitRequest>, UnixStream, Arrival);
180
181/// What a landing group shares:
182/// the common Git directory,
183/// the queue reference,
184/// and whether its request lands alone.
185type GroupKey = (std::path::PathBuf, String, bool);
186
187/// What:
188///  Puts `member` into the latest group of its common Git directory and queue reference,
189/// or into a group of its own when
190///  it lands alone,
191/// when that group's request does,
192/// or when no group of that branch exists yet.
193/// Why:
194///   Requests of one branch that may share a batch land together and in arrival order;
195/// an amend is a group of its own and
196///  what arrives after it starts another group,
197/// so the order on one branch is the order of arrival,
198/// and a request never
199///  lands in a batch of another branch.
200///
201/// In TS you'd write (pseudocode):
202/// ```ts
203/// const joined = groups.findLast((group) => group.common === key.common && group.reference === key.reference)
204/// if (joined && !key.alone && !joined.alone) { joined.members.push(member) } else { groups.push({ key, members: [member] }) }
205/// ```
206fn place_in_groups<T>(groups: &mut Vec<(GroupKey, Vec<T>)>, key: GroupKey, member: T) {
207    let joins = groups.iter_mut().rev().find(|((common, reference, _), _)| { return *common == key.0 && *reference == key.1; });
208    match joins {
209        Some((existing, members)) if !key.2 && !existing.2 => members.push(member),
210        _ => groups.push((key, vec![member])),
211    }
212}
213
214enum Job {
215    Commit(Box<CommitRequest>, UnixStream, Arrival),
216    Retire,
217    /// After-commit work was announced; it only restarts the idle clock.
218    Activity,
219    /// A client that held a commit's scratch directory is gone; the directory is removed between batches, never during a
220    ///  landing that may read it.
221    Release(std::path::PathBuf),
222}
223
224/// Returns when the other end of the connection has closed it, or the connection failed. Anything the client sends is ignored.
225fn wait_for_close(stream: &UnixStream) {
226    let mut sink: [u8; 64] = [0; 64];
227    let mut reader = stream;
228    loop {
229        match reader.read(&mut sink) {
230            Ok(0) | Err(_) => return,
231            Ok(_) => {}
232        }
233    }
234}
235
236fn send(stream: &UnixStream, message: &ServiceMessage) {
237    let mut line: String = encode_service(message);
238    line.push('\n');
239    // A client that went away has nothing to be told.
240    let _written = (&*stream).write_all(line.as_bytes());
241}
242
243/// Starts a thread the service cannot do without, or ends the service with a plain line and status 4: a service that goes on
244///  without its acceptor or its work thread answers no one, while an ended service is started again by the manager.
245fn start_thread_or_exit(what: &str, body: impl FnOnce() + Send + 'static) {
246    if let Err(error) = std::thread::Builder::new().spawn(body) {
247        crate::service_error!("cannot start the thread for {what}: {error}");
248        std::process::exit(4);
249    }
250}
251
252/// How long the acceptor waits after a connection it could not give a thread.
253const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
254
255/// Reads one request line from a new connection, checks who sent it, and queues it.
256fn handle_connection(stream: UnixStream, queue: &Sender<Job>, work: &Sender<Wake>, busy: &AtomicUsize, release: &str, instance: Option<&str>) {
257    let accepted: Instant = Instant::now();
258    send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(release.to_string()) });
259    // SAFETY: `geteuid` takes no arguments and cannot fail.
260    if peer_user(&stream) != Some(unsafe { libc::geteuid() }) {
261        return;
262    }
263    let _timeout = stream.set_read_timeout(Some(REQUEST_READ_TIMEOUT));
264    let Ok(reader_stream) = stream.try_clone() else { return };
265    let mut line: String = String::new();
266    let limit: u64 = u64::try_from(MAX_LINE_BYTES).unwrap_or(u64::MAX) + 1;
267    if BufReader::new(reader_stream.take(limit)).read_line(&mut line).is_err() {
268        return;
269    }
270    let _cleared = stream.set_read_timeout(None);
271    if line.is_empty() {
272        // A connection that only looked (a readiness probe) sends nothing and is not an error.
273        return;
274    }
275    // A message about another worktree is not this instance's to act on: each worktree has its own instance, so one that
276    //  reaches the wrong socket is a mistake of the sender's, refused where it can be seen.
277    let foreign = |git_dir: &std::path::Path| -> bool { return instance.is_some_and(|mine| { return instance_of(git_dir) != mine; }); };
278    match decode_client(line.trim_end_matches('\n')) {
279        Ok(ClientMessage::Commit(request)) if foreign(request.git_dir.as_path()) => {
280            crate::service_warn!("refused a commit for {}, which another instance serves", request.git_dir.display());
281            send(&stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Rejected });
282        }
283        Ok(ClientMessage::Scratch { git_dir, .. } | ClientMessage::Work { git_dir, .. }) if foreign(git_dir.as_path()) => {
284            crate::service_warn!("refused a message for {}, which another instance serves", git_dir.display());
285            send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
286        }
287        Ok(ClientMessage::Commit(request)) => {
288            let _queued = queue.send(Job::Commit(request, stream, Arrival { accepted, read: accepted.elapsed() }));
289        }
290        Ok(ClientMessage::Retire) => {
291            log(retire_line(release).as_str());
292            let _queued = queue.send(Job::Retire);
293        }
294        Ok(ClientMessage::Scratch { git_dir, path }) => {
295            // The connection is the client's claim on the directory. While it is open the service is not idle, and when it
296            //  closes, by the client's end or its death, the directory is no one's.
297            busy.fetch_add(1, Ordering::SeqCst);
298            wait_for_close(&stream);
299            busy.fetch_sub(1, Ordering::SeqCst);
300            let _unused = git_dir;
301            let _queued = queue.send(Job::Release(path));
302        }
303        Ok(ClientMessage::Work { git_dir, review_key }) => {
304            let _queued = work.send((git_dir, review_key));
305            let _touched = queue.send(Job::Activity);
306        }
307        Err(reason) => {
308            crate::service_warn!("refused a request line: {reason}");
309            send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
310        }
311    }
312}
313
314/// Finishes the open batches this instance owns: those of its worktree when it serves one, all of them in a development run.
315fn settle_open_batches(context: &LandContext, instance: Option<&str>) -> Vec<super::service_land::RecoveryReport> {
316    let reports: Vec<super::service_land::RecoveryReport> = match instance {
317        Some(mine) => recover_instance(context, mine),
318        None => recover(context),
319    };
320    // An open batch means an earlier service stopped between its intent and its end; each one settled is said.
321    for report in &reports {
322        log_entry(
323            Priority::Warning,
324            format!("settled the batch an earlier service left open batch={} decision={}", report.seq, if report.decision == Recovery::Finish { "finish" } else { "drop" }).as_str(),
325            &[("CLI_GIT_BATCH", report.seq.to_string())],
326        );
327    }
328    return reports;
329}
330
331/// The times of one batch that every request of it shares,
332/// and the moment the landing ended.
333struct BatchTimes {
334    /// The batch number,
335    /// zero when no batch ran (a request answered as already landed).
336    batch: u64,
337    /// How many requests of the branch the batch held.
338    size: usize,
339    /// When the group began to be handled,
340    /// which ends every request's wait in the queue.
341    group_began: Instant,
342    /// Time to settle the batches a killed service left open.
343    settle: Duration,
344    /// The phases of the landing.
345    landing: LandTiming,
346    /// Time the landing took,
347    /// from its start to its return.
348    land_total: Duration,
349    /// When the landing returned,
350    /// which starts the answers.
351    landed_at: Instant,
352}
353
354impl BatchTimes {
355    /// The times of a request that no batch landed.
356    fn not_landed(group_began: Instant) -> Self {
357        return Self { batch: 0, size: 0, group_began, settle: Duration::ZERO, landing: LandTiming::default(), land_total: Duration::ZERO, landed_at: Instant::now() };
358    }
359}
360
361/// Writes the log line for an answered request:
362/// the phases of its wait,
363/// from the acceptance of its connection to the answer.
364fn log_answer(times: &BatchTimes, request: &CommitRequest, arrival: &Arrival, position: usize, outcome: &str) {
365    let queued: Duration = times.group_began.saturating_duration_since(arrival.accepted + arrival.read);
366    let line: String = answered_line(&Answered {
367        request: request.id.as_str(),
368        reference: request.reference.as_str(),
369        outcome,
370        batch: times.batch,
371        size: times.size,
372        position,
373        arrival: *arrival,
374        queued,
375        settle: times.settle,
376        landing: &times.landing,
377        land_total: times.land_total,
378        answer: times.landed_at.elapsed(),
379        total: arrival.accepted.elapsed(),
380    });
381    // A request that was answered with a failure is a warning, so `journalctl --priority=warning` finds the commits that failed.
382    let priority: Priority = if outcome.starts_with("failed") { Priority::Warning } else { Priority::Info };
383    let fields: [(&str, String); 4] = [
384        ("CLI_GIT_REQUEST_ID", request.id.clone()),
385        ("CLI_GIT_BRANCH", request.reference.clone()),
386        ("CLI_GIT_OUTCOME", outcome.to_string()),
387        ("CLI_GIT_BATCH", times.batch.to_string()),
388    ];
389    log_entry(priority, line.as_str(), &fields);
390}
391
392/// Lands one group of jobs that share a branch, and answers each client.
393fn land_group(context: &LandContext, instance: Option<&str>, seq: &mut u64, group: Vec<Queued>, landed: &mut HashMap<String, String>) {
394    let group_began: Instant = Instant::now();
395    let (again, pending): (Vec<_>, Vec<_>) = group.into_iter().partition(|(request, _, _)| { return landed.contains_key(&request.id); });
396    let unlanded: BatchTimes = BatchTimes::not_landed(group_began);
397    for (request, stream, arrival) in again {
398        send(&stream, &ServiceMessage::Landed { id: request.id.clone(), commit: landed[&request.id].clone(), already: true });
399        log_answer(&unlanded, &request, &arrival, 1, "already");
400    }
401    // The same request sent twice into one batch (a client that reconnected) is landed once and answered twice.
402    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
403    let (fresh, repeats): (Vec<_>, Vec<_>) = pending.into_iter().partition(|(request, _, _)| { return seen.insert(request.id.clone()); });
404    if fresh.is_empty() {
405        return;
406    }
407    // A batch whose branch moved but whose index update had to give up (another program held Git's index lock the whole
408    //  time) left its intent open; settling it first puts the index right before anything else changes it.
409    let settling: Instant = Instant::now();
410    let _settled = settle_open_batches(context, instance);
411    let settle: Duration = settling.elapsed();
412    *seq += 1;
413    for (index, (_, stream, _)) in fresh.iter().enumerate().skip(1) {
414        send(stream, &ServiceMessage::Position { number: index + 1, of: fresh.len() });
415    }
416    let requests: Vec<CommitRequest> = fresh.iter().map(|(request, _, _)| { return (**request).clone(); }).collect();
417    let announce = |note: Announce| {
418        for (_, stream, _) in &fresh {
419            match &note {
420                Announce::Waiting(text) => send(stream, &ServiceMessage::Waiting { text: text.clone() }),
421                Announce::Holder(who) => send(stream, &ServiceMessage::Holder { who: who.clone() }),
422            }
423        }
424    };
425    let mut landing: LandTiming = LandTiming::default();
426    let landing_began: Instant = Instant::now();
427    let result = land_batch_timed(context, *seq, &requests, &announce, &mut landing);
428    let times: BatchTimes = BatchTimes { batch: *seq, size: fresh.len(), group_began, settle, landing, land_total: landing_began.elapsed(), landed_at: Instant::now() };
429    match result {
430        Ok(outcomes) => {
431            super::service_fault::reach("before-ack");
432            for ((request, _, _), outcome) in fresh.iter().zip(&outcomes) {
433                if let Outcome::Landed { commit } = outcome {
434                    for (repeat, stream, arrival) in repeats.iter().filter(|(repeat, _, _)| { return repeat.id == request.id; }) {
435                        send(stream, &ServiceMessage::Landed { id: repeat.id.clone(), commit: commit.clone(), already: true });
436                        log_answer(&times, repeat, arrival, 1, "already");
437                    }
438                }
439            }
440            for (position, ((request, stream, arrival), outcome)) in fresh.iter().zip(outcomes).enumerate() {
441                let word: &str = match outcome {
442                    Outcome::Landed { commit } => {
443                        landed.insert(request.id.clone(), commit.clone());
444                        send(stream, &ServiceMessage::Landed { id: request.id.clone(), commit, already: false });
445                        "landed"
446                    }
447                    Outcome::RetryPrepare { base } => {
448                        let captures = super::service_captures::remembered(request.common_dir.as_path(), request.repository.as_path());
449                        send(stream, &ServiceMessage::RetryPrepare { id: request.id.clone(), base, landed: captures });
450                        "retry-prepare"
451                    }
452                    Outcome::BranchMoved => {
453                        send(stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::BranchMoved });
454                        "branch-moved"
455                    }
456                };
457                log_answer(&times, request, arrival, position + 1, word);
458            }
459        }
460        Err(code) => {
461            for (position, (request, stream, arrival)) in fresh.iter().enumerate() {
462                send(stream, &ServiceMessage::Failed { id: request.id.clone(), code });
463                log_answer(&times, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
464            }
465        }
466    }
467}
468
469/// What:
470///  Runs the after-commit work of each repository named, one repository at a time, folding wakes that arrive while it
471///  works into the next round, and looks again at a repository whose claims another instance held when their lease is over.
472/// Why:
473///   Draining claims everything pending, so a burst of wakes for one repository is one pass. The instance that held a claim
474///  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
475///  running, and the service counts itself busy meanwhile, so it does not exit idle with work claimed by a dead holder.
476///
477/// In TS you'd write (pseudocode):
478/// ```ts
479/// for await (const dir of wakesOrDueRetries) await drain(dir, handlers)
480/// ```
481fn work_loop(receiver: &Receiver<Wake>, handlers: &GitHandlers, busy: &AtomicUsize) {
482    let mut retries: RetrySchedule = RetrySchedule::default();
483    let mut counted_waiting: bool = false;
484    loop {
485        let first: Option<Wake> = match retries.next_wait(Instant::now()) {
486            None => match receiver.recv() {
487                Ok(wake) => Some(wake),
488                Err(_) => return,
489            },
490            Some(wait) => match receiver.recv_timeout(wait) {
491                Ok(wake) => Some(wake),
492                Err(RecvTimeoutError::Timeout) => None,
493                Err(RecvTimeoutError::Disconnected) => return,
494            },
495        };
496        busy.fetch_add(1, Ordering::SeqCst);
497        let mut wakes: Vec<Wake> = first.into_iter().chain(std::iter::from_fn(|| { return receiver.try_recv().ok(); })).collect();
498        wakes.extend(retries.take_due(Instant::now()).into_iter().map(|directory| { return (directory, None); }));
499        let ran: Vec<PathBuf> = wakes.iter().map(|(directory, _)| { return directory.clone(); }).collect();
500        let asked: Vec<(PathBuf, Duration)> = run_round(wakes, handlers, &ClaimTime::read());
501        retries.replace(ran.as_slice(), asked, Instant::now());
502        // A repository waiting to be looked at again keeps the service from exiting idle, as running work does.
503        if !retries.is_empty() && !counted_waiting {
504            busy.fetch_add(1, Ordering::SeqCst);
505            counted_waiting = true;
506        } else if retries.is_empty() && counted_waiting {
507            busy.fetch_sub(1, Ordering::SeqCst);
508            counted_waiting = false;
509        }
510        busy.fetch_sub(1, Ordering::SeqCst);
511    }
512}
513
514/// What:
515///  Runs the service until it is told to retire.
516/// Why:
517///   Recovery runs before the first connection is accepted; then the loop takes everything queued as one batch, groups it
518///  by branch in order of first arrival, lands each group, and answers.
519///
520/// In TS you'd write (pseudocode):
521/// ```ts
522/// await serve(listener, context)
523/// ```
524pub fn serve(listener: UnixListener, context: &LandContext, settings: &ServeSettings) {
525    let serving_since: Instant = Instant::now();
526    let idle: Duration = settings.idle;
527    let _reports = settle_open_batches(context, settings.instance.as_deref());
528    // The memory of landed requests is bounded: the oldest finished batches go before the rest are read.
529    let pruned: usize = prune_done(context.journal.as_path(), DONE_RETAINED);
530    if pruned > 0 {
531        crate::service_log!("removed the records of {pruned} old finished batches from {}", context.journal.display());
532    }
533    let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
534    let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
535    let acceptor_queue: Sender<Job> = sender.clone();
536    // After-commit work (push, upkeep) runs on its own thread, so a slow network never delays a landing.
537    let (work_sender, work_receiver): (Sender<Wake>, Receiver<Wake>) = channel();
538    let handlers: GitHandlers = GitHandlers::new(context.git.clone());
539    let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
540    let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
541    start_thread_or_exit("the after-commit work", move || { work_loop(&work_receiver, &handlers, &worker_busy); });
542    let announced_release: String = settings.release.clone();
543    let connection_busy: Arc<AtomicUsize> = Arc::clone(&busy);
544    let instance: Option<String> = settings.instance.clone();
545    start_thread_or_exit("the acceptor of connections", move || {
546        for connection in listener.incoming().flatten() {
547            let queue: Sender<Job> = acceptor_queue.clone();
548            let work: Sender<Wake> = work_sender.clone();
549            let release: String = announced_release.clone();
550            let counted: Arc<AtomicUsize> = Arc::clone(&connection_busy);
551            let mine: Option<String> = instance.clone();
552            // A connection the service cannot give a thread is told so and closed; the acceptor goes on, because a panic here
553            //  would end the acceptor and leave a service that is running and answers no one.
554            let notice: Option<UnixStream> = connection.try_clone().ok();
555            let spawned = std::thread::Builder::new().spawn(move || { handle_connection(connection, &queue, &work, &counted, release.as_str(), mine.as_deref()); });
556            if let Err(error) = spawned {
557                crate::service_error!("no thread for a connection, which is closed after telling the client: {error}");
558                if let Some(stream) = notice {
559                    send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(announced_release.clone()) });
560                    send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Unavailable });
561                }
562                // Starting a thread keeps failing while the pressure lasts; waiting a moment keeps the acceptor from spinning on it.
563                std::thread::sleep(ACCEPT_BACKOFF);
564            }
565        }
566    });
567    // Microseconds, so that instances started at nearly the same time do not number their batches alike in a shared journal.
568    let mut seq: 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); });
569    // The marker made ahead is removed whichever way this loop ends.
570    let _prestaged = PrestageGuard;
571    let mut retiring: bool = false;
572    while !retiring {
573        let first: Job = match receiver.recv_timeout(idle) {
574            Ok(job) => job,
575            // Work in flight keeps the service; otherwise it exits and the manager starts it on the next connection.
576            Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
577            Err(RecvTimeoutError::Timeout) => {
578                log(exit_line(ExitReason::Idle, serving_since.elapsed()).as_str());
579                return;
580            }
581            Err(RecvTimeoutError::Disconnected) => {
582                log(exit_line(ExitReason::Error, serving_since.elapsed()).as_str());
583                return;
584            }
585        };
586        let mut jobs: Vec<Job> = vec![first];
587        while jobs.len() < MAX_BATCH {
588            match receiver.try_recv() {
589                Ok(job) => jobs.push(job),
590                Err(_) => break,
591            }
592        }
593        // Branches in order of first arrival; within a branch, arrival order.
594        let mut groups: Vec<(GroupKey, Vec<Queued>)> = Vec::new();
595        let mut releases: Vec<std::path::PathBuf> = Vec::new();
596        for job in jobs {
597            match job {
598                Job::Retire => retiring = true,
599                Job::Activity => {}
600                Job::Release(path) => releases.push(path),
601                Job::Commit(request, stream, arrival) => {
602                    // An amend is a group of its own, and what arrives after it starts another group, so the order on
603                    //  one branch is the order of arrival.
604                    // `HEAD` is a head of one worktree, so two detached worktrees of one repository are two branches.
605                    let queue_reference: String = if request.reference == "HEAD" {
606                        format!("HEAD@{}", request.repository.display())
607                    } else {
608                        request.reference.clone()
609                    };
610                    let key: GroupKey = (request.common_dir.clone(), queue_reference, request.lands_alone());
611                    place_in_groups(&mut groups, key, (request, stream, arrival));
612                }
613            }
614        }
615        for (_, group) in groups {
616            let index: Option<std::path::PathBuf> = group.first().map(|(request, _, _)| { return request.index.clone(); });
617            land_group(context, settings.instance.as_deref(), &mut seq, group, &mut landed);
618            // The batch after this one is numbered one more and lands on this index (a worktree has one), so its marker is
619            //  made and flushed now, while the service is idle, and the next commit only places it.
620            if let Some(next_index) = index {
621                start_prestage(next_index.as_path(), seq + 1);
622            }
623        }
624        // A lock this thread could not settle names this process: the restart reclaims it once this process is gone.
625        if unsettled_locks() > 0 {
626            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");
627            return;
628        }
629        for path in releases {
630            if let Err(error) = remove_tree(path.as_path()) {
631                crate::service_warn!("the scratch directory {} could not be removed: {error}", path.display());
632            }
633        }
634    }
635    log(exit_line(ExitReason::Retire, serving_since.elapsed()).as_str());
636}
637
638/// Socket controls stay out of the release executable.
639#[cfg(test)]
640#[path = "service_server_tests.rs"]
641mod tests;