1use 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
38pub const MAX_BATCH: usize = 64;
40
41pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
43
44pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
47
48pub const INHERITED_DESCRIPTOR: i32 = 3;
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct ServeSettings {
54 pub idle: Duration,
56 pub release: String,
59 pub instance: Option<String>,
62}
63
64impl ServeSettings {
65 pub fn new(idle: Duration) -> Self {
67 return Self { idle, release: String::from(RELEASE), instance: None };
68 }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
73pub enum Bind {
74 Abstract(String),
76 Path(std::path::PathBuf),
78 Inherited,
80}
81
82pub 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 Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
107 }
108 };
109}
110
111fn 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
135fn 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
163fn 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 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
175type Queued = (Box<CommitRequest>, UnixStream, Arrival);
180
181type GroupKey = (std::path::PathBuf, String, bool);
186
187fn 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 Activity,
219 Release(std::path::PathBuf),
222}
223
224fn 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 let _written = (&*stream).write_all(line.as_bytes());
241}
242
243fn 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
252const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
254
255fn 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 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 return;
274 }
275 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 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
314fn 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 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
331struct BatchTimes {
334 batch: u64,
337 size: usize,
339 group_began: Instant,
342 settle: Duration,
344 landing: LandTiming,
346 land_total: Duration,
349 landed_at: Instant,
352}
353
354impl BatchTimes {
355 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
361fn 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: ×.landing,
377 land_total: times.land_total,
378 answer: times.landed_at.elapsed(),
379 total: arrival.accepted.elapsed(),
380 });
381 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
392fn 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 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 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 ¬e {
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(×, 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(×, 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(×, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
464 }
465 }
466 }
467}
468
469fn 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 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
514pub 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 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 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 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 std::thread::sleep(ACCEPT_BACKOFF);
564 }
565 }
566 });
567 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 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 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 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 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 if let Some(next_index) = index {
621 start_prestage(next_index.as_path(), seq + 1);
622 }
623 }
624 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#[cfg(test)]
640#[path = "service_server_tests.rs"]
641mod tests;