1use 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, GroupAdmission, 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
44pub const BATCH_COST_KNEE: usize = 256;
52
53pub const MAX_BATCH: usize = BATCH_COST_KNEE;
59
60pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
62
63pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
71
72pub const INHERITED_DESCRIPTOR: i32 = 3;
74
75#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct ServeSettings {
78 pub idle: Duration,
80 pub release: String,
83 pub instance: Option<String>,
86 pub stuck_after: Duration,
89}
90
91impl ServeSettings {
92 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#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum Bind {
101 Abstract(String),
103 Path(std::path::PathBuf),
105 Inherited,
107}
108
109pub 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 Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
134 }
135 };
136}
137
138fn 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
162fn 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
190fn 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 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
202type Queued = (Box<CommitRequest>, Reply, Arrival);
207
208type GroupKey = (std::path::PathBuf, String, bool);
213
214fn 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 Withdraw(String, Reply),
247 Activity,
249 Release(std::path::PathBuf),
252}
253
254fn 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 let _written = (&*stream).write_all(line.as_bytes());
271}
272
273fn 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
282const WITHDRAWN_RETAINED: usize = DONE_RETAINED;
285
286const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
288
289fn stuck_notice<'a>(board: &'a LandingBoard, request: Option<&'a str>) -> impl FnMut() -> Option<ServiceMessage> + 'a {
300 let mut last: Option<String> = None;
301 return move || {
302 if let Some((key, words)) = request.and_then(|id| { return board.inside_notice(id); }) {
305 if last.as_deref() == Some(key.as_str()) {
306 return None;
307 }
308 last = Some(key);
309 return Some(ServiceMessage::InsideWrite { text: words });
310 }
311 let words: String = board.describe()?;
312 if last.as_deref() == Some(words.as_str()) {
313 return None;
314 }
315 last = Some(words.clone());
316 return Some(ServiceMessage::Waiting { text: words });
317 };
318}
319
320fn handle_connection(stream: UnixStream, queue: &Sender<Job>, work: &Sender<Wake>, busy: &AtomicUsize, board: &LandingBoard, release: &str, instance: Option<&str>) {
322 let accepted: Instant = Instant::now();
323 send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(release.to_string()) });
324 if peer_user(&stream) != Some(unsafe { libc::geteuid() }) {
326 return;
327 }
328 let _timeout = stream.set_read_timeout(Some(REQUEST_READ_TIMEOUT));
329 let Ok(reader_stream) = stream.try_clone() else { return };
330 let mut line: String = String::new();
331 let limit: u64 = u64::try_from(MAX_LINE_BYTES).unwrap_or(u64::MAX) + 1;
332 if BufReader::new(reader_stream.take(limit)).read_line(&mut line).is_err() {
333 return;
334 }
335 let _cleared = stream.set_read_timeout(None);
336 if line.is_empty() {
337 return;
339 }
340 let foreign = |git_dir: &std::path::Path| -> bool { return instance.is_some_and(|mine| { return instance_of(git_dir) != mine; }); };
343 match decode_client(line.trim_end_matches('\n')) {
344 Ok(ClientMessage::Commit(request)) if foreign(request.git_dir.as_path()) => {
345 crate::service_warn!("refused a commit for {}, which another instance serves", request.git_dir.display());
346 send(&stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Rejected });
347 }
348 Ok(ClientMessage::Scratch { git_dir, .. } | ClientMessage::Work { git_dir, .. }) if foreign(git_dir.as_path()) => {
349 crate::service_warn!("refused a message for {}, which another instance serves", git_dir.display());
350 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
351 }
352 Ok(ClientMessage::Commit(request)) => {
353 let id: String = request.id.clone();
354 let (reply, messages) = Reply::new();
355 let _queued = queue.send(Job::Commit(request, reply, Arrival { accepted, read: accepted.elapsed() }));
356 crate::service_fault::queued(id.as_str());
357 write_until_answered_noting(&stream, &messages, HEARTBEAT_EVERY, &mut stuck_notice(board, Some(id.as_str())));
360 }
361 Ok(ClientMessage::Withdraw { id }) => {
362 let withdrawing: String = id.clone();
367 match board.withdraw_early(id.as_str()) {
368 EarlyAnswer::Landed(commit) => send(&stream, &ServiceMessage::Landed { id, commit, already: true }),
369 EarlyAnswer::Withdrawn(words) => {
370 send(&stream, &ServiceMessage::Waiting { text: words });
371 send(&stream, &ServiceMessage::Withdrawn { id });
372 }
373 EarlyAnswer::Queued => {
374 let (reply, messages) = Reply::new();
375 let _queued = queue.send(Job::Withdraw(id, reply));
376 write_until_answered_noting(&stream, &messages, HEARTBEAT_EVERY, &mut stuck_notice(board, Some(withdrawing.as_str())));
379 }
380 }
381 }
382 Ok(ClientMessage::Retire) => {
383 log(retire_line(release).as_str());
384 let _queued = queue.send(Job::Retire);
385 }
386 Ok(ClientMessage::Scratch { git_dir, path }) => {
387 busy.fetch_add(1, Ordering::SeqCst);
390 wait_for_close(&stream);
391 busy.fetch_sub(1, Ordering::SeqCst);
392 let _unused = git_dir;
393 let _queued = queue.send(Job::Release(path));
394 }
395 Ok(ClientMessage::Work { git_dir, review_key }) => {
396 let _queued = work.send((git_dir, review_key));
397 let _touched = queue.send(Job::Activity);
398 }
399 Err(reason) => {
400 crate::service_warn!("refused a request line: {reason}");
401 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
402 }
403 }
404}
405
406fn settle_open_batches(context: &LandContext, instance: Option<&str>) -> Vec<crate::service_land::RecoveryReport> {
408 let reports: Vec<crate::service_land::RecoveryReport> = match instance {
409 Some(mine) => recover_instance(context, mine),
410 None => recover(context),
411 };
412 for report in &reports {
414 log_entry(
415 Priority::Warning,
416 format!("settled the batch an earlier service left open batch={} decision={}", report.seq, if report.decision == Recovery::Finish { "finish" } else { "drop" }).as_str(),
417 &[("CLI_GIT_BATCH", report.seq.to_string())],
418 );
419 }
420 return reports;
421}
422
423struct BatchTimes {
426 batch: u64,
429 size: usize,
431 group_began: Instant,
434 settle: Duration,
436 landing: LandTiming,
438 land_total: Duration,
441 landed_at: Instant,
444}
445
446impl BatchTimes {
447 fn not_landed(group_began: Instant) -> Self {
449 return Self { batch: 0, size: 0, group_began, settle: Duration::ZERO, landing: LandTiming::default(), land_total: Duration::ZERO, landed_at: Instant::now() };
450 }
451}
452
453fn log_answer(times: &BatchTimes, request: &CommitRequest, arrival: &Arrival, position: usize, outcome: &str) {
457 let queued: Duration = times.group_began.saturating_duration_since(arrival.accepted + arrival.read);
458 let quiet: Vec<(String, Duration)> = crate::quiet_ledger::drain();
460 let line: String = answered_line(&Answered {
461 request: request.id.as_str(),
462 reference: request.reference.as_str(),
463 outcome,
464 batch: times.batch,
465 size: times.size,
466 position,
467 arrival: *arrival,
468 queued,
469 settle: times.settle,
470 landing: ×.landing,
471 land_total: times.land_total,
472 answer: times.landed_at.elapsed(),
473 total: arrival.accepted.elapsed(),
474 quiet: quiet.as_slice(),
475 });
476 let priority: Priority = if outcome.starts_with("failed") { Priority::Warning } else { Priority::Info };
478 let fields: [(&str, String); 4] = [
479 ("CLI_GIT_REQUEST_ID", request.id.clone()),
480 ("CLI_GIT_BRANCH", request.reference.clone()),
481 ("CLI_GIT_OUTCOME", outcome.to_string()),
482 ("CLI_GIT_BATCH", times.batch.to_string()),
483 ];
484 log_entry(priority, line.as_str(), &fields);
485}
486
487fn land_group(context: &LandContext, instance: Option<&str>, seq: &mut u64, group: Vec<Queued>, landed: &mut HashMap<String, String>, captures: &mut CaptureMemory) {
489 let group_began: Instant = Instant::now();
490 let (again, pending): (Vec<_>, Vec<_>) = group.into_iter().partition(|(request, _, _)| { return landed.contains_key(&request.id); });
491 let unlanded: BatchTimes = BatchTimes::not_landed(group_began);
492 for (request, stream, arrival) in again {
493 stream.send(&ServiceMessage::Landed { id: request.id.clone(), commit: landed[&request.id].clone(), already: true });
494 log_answer(&unlanded, &request, &arrival, 1, "already");
495 }
496 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
498 let (fresh, repeats): (Vec<_>, Vec<_>) = pending.into_iter().partition(|(request, _, _)| { return seen.insert(request.id.clone()); });
499 if fresh.is_empty() {
500 return;
501 }
502 let settling: Instant = Instant::now();
505 let _settled = settle_open_batches(context, instance);
506 let settle: Duration = settling.elapsed();
507 let claim: BatchClaim = match claim_batch(context.journal.as_path(), *seq + 1) {
510 Ok(claim) => claim,
511 Err(error) => {
512 let code: FailureCode = storage_failure(&error);
513 crate::service_error!("no batch number could be taken in {}: {error}", context.journal.display());
514 for (position, (request, stream, arrival)) in fresh.iter().enumerate() {
515 stream.send(&ServiceMessage::Failed { id: request.id.clone(), code });
516 log_answer(&unlanded, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
517 }
518 return;
519 }
520 };
521 *seq = claim.seq();
522 for (index, (_, stream, _)) in fresh.iter().enumerate().skip(1) {
523 stream.send(&ServiceMessage::Position { number: index + 1, of: fresh.len() });
524 }
525 let requests: Vec<CommitRequest> = fresh.iter().map(|(request, _, _)| { return (**request).clone(); }).collect();
526 let announce = |note: Announce| {
527 for (_, stream, _) in &fresh {
528 match ¬e {
529 Announce::Waiting(text) => stream.send(&ServiceMessage::Waiting { text: text.clone() }),
530 Announce::Holder(who) => stream.send(&ServiceMessage::Holder { who: who.clone() }),
531 }
532 }
533 };
534 let mut landing: LandTiming = LandTiming::default();
535 let landing_began: Instant = Instant::now();
536 let result = land_batch_timed(context, *seq, &requests, &announce, &mut landing);
537 drop(claim);
539 let times: BatchTimes = BatchTimes { batch: *seq, size: fresh.len(), group_began, settle, landing, land_total: landing_began.elapsed(), landed_at: Instant::now() };
540 match result {
541 Ok(outcomes) => {
542 for ((request, _, _), outcome) in fresh.iter().zip(&outcomes) {
544 if let Outcome::Landed { commit } = outcome {
545 captures.record_landed(request.common_dir.as_path(), request.repository.as_path(), commit.as_str(), request.captured);
546 }
547 }
548 crate::service_fault::reach("before-ack");
549 for ((request, _, _), outcome) in fresh.iter().zip(&outcomes) {
550 if let Outcome::Landed { commit } = outcome {
551 for (repeat, stream, arrival) in repeats.iter().filter(|(repeat, _, _)| { return repeat.id == request.id; }) {
552 stream.send(&ServiceMessage::Landed { id: repeat.id.clone(), commit: commit.clone(), already: true });
553 log_answer(×, repeat, arrival, 1, "already");
554 }
555 }
556 }
557 for (position, ((request, stream, arrival), outcome)) in fresh.iter().zip(outcomes).enumerate() {
558 let word: &str = match outcome {
559 Outcome::Landed { commit } => {
560 landed.insert(request.id.clone(), commit.clone());
561 stream.send(&ServiceMessage::Landed { id: request.id.clone(), commit, already: false });
562 "landed"
563 }
564 Outcome::RetryPrepare { base } => {
565 let known = captures.remembered(request.common_dir.as_path(), request.repository.as_path());
566 stream.send(&ServiceMessage::RetryPrepare { id: request.id.clone(), base, landed: known });
567 "retry-prepare"
568 }
569 Outcome::BranchMoved => {
570 stream.send(&ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::BranchMoved });
571 "branch-moved"
572 }
573 };
574 log_answer(×, request, arrival, position + 1, word);
575 }
576 }
577 Err(code) => {
578 for (position, (request, stream, arrival)) in fresh.iter().enumerate() {
579 stream.send(&ServiceMessage::Failed { id: request.id.clone(), code });
580 log_answer(×, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
581 }
582 }
583 }
584}
585
586pub fn work_loop(receiver: &Receiver<Wake>, handlers: &GitHandlers, busy: &AtomicUsize, mut push_retries: PushRetries) {
602 let mut retries: RetrySchedule = RetrySchedule::default();
603 let mut counted_waiting: bool = false;
604 let started: Instant = Instant::now();
606 let restored: Vec<(PathBuf, Duration)> = push_retries
607 .directories()
608 .into_iter()
609 .filter_map(|directory| { return push_retries.next_wait(directory.as_path(), started).map(|wait| { return (directory, wait); }); })
610 .collect();
611 retries.replace(&[], restored, started);
612 if !retries.is_empty() {
613 busy.fetch_add(1, Ordering::SeqCst);
614 counted_waiting = true;
615 }
616 loop {
617 let first: Option<Wake> = match retries.next_wait(Instant::now()) {
618 None => match receiver.recv() {
619 Ok(wake) => Some(wake),
620 Err(_) => return,
621 },
622 Some(wait) => match receiver.recv_timeout(wait) {
623 Ok(wake) => Some(wake),
624 Err(RecvTimeoutError::Timeout) => None,
625 Err(RecvTimeoutError::Disconnected) => return,
626 },
627 };
628 busy.fetch_add(1, Ordering::SeqCst);
629 let mut wakes: Vec<Wake> = first.into_iter().chain(std::iter::from_fn(|| { return receiver.try_recv().ok(); })).collect();
630 wakes.extend(retries.take_due(Instant::now()).into_iter().map(|directory| { return (directory, None); }));
631 let mut ran: Vec<PathBuf> = wakes.iter().map(|(directory, _)| { return directory.clone(); }).collect();
632 let asked: Vec<(PathBuf, Duration)> = run_round(wakes, handlers, &mut push_retries, &ClaimTime::read());
633 ran.extend(asked.iter().map(|(directory, _)| { return directory.clone(); }));
635 retries.replace(ran.as_slice(), asked, Instant::now());
636 if !retries.is_empty() && !counted_waiting {
638 busy.fetch_add(1, Ordering::SeqCst);
639 counted_waiting = true;
640 } else if retries.is_empty() && counted_waiting {
641 busy.fetch_sub(1, Ordering::SeqCst);
642 counted_waiting = false;
643 }
644 busy.fetch_sub(1, Ordering::SeqCst);
645 }
646}
647
648pub fn serve(listener: UnixListener, context: &LandContext, settings: &ServeSettings) {
659 let serving_since: Instant = Instant::now();
660 let idle: Duration = settings.idle;
661 let _reports = settle_open_batches(context, settings.instance.as_deref());
662 let pruned: usize = prune_done(context.journal.as_path(), DONE_RETAINED);
664 if pruned > 0 {
665 crate::service_log!("removed the records of {pruned} old finished batches from {}", context.journal.display());
666 }
667 let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
668 let mut withdrawn: std::collections::HashSet<String> = std::collections::HashSet::new();
670 let mut captures: CaptureMemory = CaptureMemory::default();
672 let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
673 let acceptor_queue: Sender<Job> = sender.clone();
674 let (work_sender, work_receiver): (Sender<Wake>, Receiver<Wake>) = channel();
676 let handlers: GitHandlers = GitHandlers::new(context.git.clone());
677 let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
678 let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
679 let board: Arc<LandingBoard> = LandingBoard::new(context.journal.clone(), settings.stuck_after);
681 crate::service_busy::use_on_this_thread(Arc::clone(&board));
682 let connection_board: Arc<LandingBoard> = Arc::clone(&board);
683 let plan_directory: PathBuf = context.journal.join(PLAN_DIRECTORY);
685 start_thread_or_exit("the after-commit work", move || { work_loop(&work_receiver, &handlers, &worker_busy, PushRetries::restored_from_environment(plan_directory.as_path())); });
686 let announced_release: String = settings.release.clone();
687 let connection_busy: Arc<AtomicUsize> = Arc::clone(&busy);
688 let instance: Option<String> = settings.instance.clone();
689 start_thread_or_exit("the acceptor of connections", move || {
690 for connection in listener.incoming().flatten() {
691 let queue: Sender<Job> = acceptor_queue.clone();
692 let work: Sender<Wake> = work_sender.clone();
693 let release: String = announced_release.clone();
694 let counted: Arc<AtomicUsize> = Arc::clone(&connection_busy);
695 let record: Arc<LandingBoard> = Arc::clone(&connection_board);
696 let mine: Option<String> = instance.clone();
697 let notice: Option<UnixStream> = connection.try_clone().ok();
700 let spawned = std::thread::Builder::new().spawn(move || { handle_connection(connection, &queue, &work, &counted, &record, release.as_str(), mine.as_deref()); });
701 if let Err(error) = spawned {
702 crate::service_error!("no thread for a connection, which is closed after telling the client: {error}");
703 if let Some(stream) = notice {
704 send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(announced_release.clone()) });
705 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Unavailable });
706 }
707 std::thread::sleep(ACCEPT_BACKOFF);
709 }
710 }
711 });
712 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); });
715 let mut seq: u64 = crate::service_fault::pinned_first_batch().unwrap_or(clock_seed);
716 let _prestaged = PrestageGuard;
718 let mut retiring: bool = false;
719 while !retiring {
720 let first: Job = match receiver.recv_timeout(idle) {
721 Ok(job) => job,
722 Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
724 Err(RecvTimeoutError::Timeout) => {
725 crate::service_durable::drain();
727 log(exit_line(ExitReason::Idle, serving_since.elapsed()).as_str());
728 return;
729 }
730 Err(RecvTimeoutError::Disconnected) => {
731 crate::service_durable::drain();
733 log(exit_line(ExitReason::Error, serving_since.elapsed()).as_str());
734 return;
735 }
736 };
737 let mut jobs: Vec<Job> = vec![first];
738 while jobs.len() < MAX_BATCH {
739 match receiver.try_recv() {
740 Ok(job) => jobs.push(job),
741 Err(_) => break,
742 }
743 }
744 if withdrawn.len() > WITHDRAWN_RETAINED {
748 withdrawn.clear();
749 }
750 for job in &jobs {
751 if let Job::Withdraw(id, reply) = job {
752 match landed.get(id) {
753 Some(commit) => reply.send(&ServiceMessage::Landed { id: id.clone(), commit: commit.clone(), already: true }),
754 None => {
755 withdrawn.insert(id.clone());
756 reply.send(&ServiceMessage::Withdrawn { id: id.clone() });
757 }
758 }
759 }
760 }
761 let mut groups: Vec<(GroupKey, Vec<Queued>)> = Vec::new();
763 let mut releases: Vec<std::path::PathBuf> = Vec::new();
764 for job in jobs {
765 match job {
766 Job::Withdraw(..) => {}
767 Job::Commit(request, _dropped, _arrival) if withdrawn.contains(&request.id) => {
768 crate::service_log!("dropped the request {} that its client withdrew", request.id);
769 }
770 Job::Retire => retiring = true,
771 Job::Activity => {}
772 Job::Release(path) => releases.push(path),
773 Job::Commit(request, stream, arrival) => {
774 let queue_reference: String = if request.reference == "HEAD" {
778 format!("HEAD@{}", request.repository.display())
779 } else {
780 request.reference.clone()
781 };
782 let key: GroupKey = (request.common_dir.clone(), queue_reference, request.lands_alone());
783 place_in_groups(&mut groups, key, (request, stream, arrival));
784 }
785 }
786 }
787 for (_, queued_group) in groups {
788 let index: Option<std::path::PathBuf> = queued_group.first().map(|(request, _, _)| { return request.index.clone(); });
789 let ids: Vec<String> = queued_group.iter().map(|(request, _, _)| { return request.id.clone(); }).collect();
792 let admission: GroupAdmission = board.begin_group(ids.as_slice());
793 for id in &admission.dropped {
794 crate::service_log!("dropped the request {id} that its client withdrew while the service was stuck in a write");
795 }
796 for id in &admission.unavailable {
797 crate::service_error!("the request {id} could not be admitted to its group (the record of the landing thread cannot be written), so it is answered with a failure and not landed");
798 }
799 let (unavailable, admitted): (Vec<Queued>, Vec<Queued>) = queued_group.into_iter().filter(|(request, _, _)| { return !admission.dropped.contains(&request.id); }).partition(|(request, _, _)| { return admission.unavailable.contains(&request.id); });
800 for (request, stream, _) in &unavailable {
801 stream.send(&ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Unavailable });
802 }
803 let group: Vec<Queued> = admitted;
804 if group.is_empty() {
805 board.end_group(ids.as_slice());
806 continue;
807 }
808 land_group(context, settings.instance.as_deref(), &mut seq, group, &mut landed, &mut captures);
809 board.end_group(ids.as_slice());
810 if let Some(next_index) = index {
813 start_prestage(next_index.as_path(), seq + 1);
814 }
815 }
816 if unsettled_locks() > 0 {
818 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");
819 return;
820 }
821 for path in releases {
822 if let Err(error) = remove_tree(path.as_path()) {
823 crate::service_warn!("the scratch directory {} could not be removed: {error}", path.display());
824 }
825 }
826 }
827 crate::service_durable::drain();
829 log(exit_line(ExitReason::Retire, serving_since.elapsed()).as_str());
830}
831
832#[cfg(test)]
834#[path = "service_server_tests.rs"]
835mod tests;