pub use crate::service_server_work_loop::work_loop;
pub use crate::service_server_socket_binding::{Bind, INHERITED_DESCRIPTOR, bind, socket_passed_in};
#[cfg(test)]
use crate::service_server_socket_binding::ensure_socket_passed_in;
use crate::service_server_queue_types_and_send::{GroupKey, Job, Queued, id_of_queued, index_of_queued, place_in_groups, send};
use crate::service_server_land_group::{land_group, settle_open_batches};
use crate::service_server_connection_handling::handle_connection;
use crate::service_journal::{DONE_RETAINED, landed_commits, prune_done};
use crate::service_index_lock::unsettled_locks;
use crate::service_index_prestage::{PrestageGuard, start as start_prestage};
use crate::service_captures::CaptureMemory;
use crate::service_land::LandContext;
use crate::service_timing::{ExitReason, exit_line, log};
use crate::private_storage::remove_tree;
use crate::service_claim_watch::{ClaimWatch, WATCH_DIRECTORY};
use crate::push_retry::{PLAN_DIRECTORY, PushRetries};
use crate::service_work::{GitHandlers, Wake};
use crate::service_busy::{GroupAdmission, LandingBoard};
use crate::wait_ceilings::NEVER_ENDED_STEP_SLOW_AFTER;
use crate::service_protocol::{
FailureCode, PROTOCOL_VERSION, RELEASE, ServiceMessage,
};
use std::collections::HashMap;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
use std::sync::Arc;
use std::path::PathBuf;
use std::time::{Duration, Instant};
pub const BATCH_COST_KNEE: usize = 256;
pub const MAX_BATCH: usize = BATCH_COST_KNEE;
pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServeSettings {
pub idle: Duration,
pub release: String,
pub instance: Option<String>,
pub stuck_after: Duration,
}
impl ServeSettings {
pub fn new(idle: Duration) -> Self {
return Self { idle, release: String::from(RELEASE), instance: None, stuck_after: NEVER_ENDED_STEP_SLOW_AFTER };
}
}
fn start_thread_or_exit(what: &str, body: impl FnOnce() + Send + 'static) {
if let Err(error) = std::thread::Builder::new().spawn(body) {
crate::service_error!("cannot start the thread for {what}: {error}");
std::process::exit(4);
}
}
const WITHDRAWN_RETAINED: usize = DONE_RETAINED;
const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
pub fn serve(listener: UnixListener, context: &LandContext, settings: &ServeSettings) {
let serving_since: Instant = Instant::now();
let idle: Duration = settings.idle;
let _reports = settle_open_batches(context, settings.instance.as_deref());
let pruned: usize = prune_done(context.journal.as_path(), DONE_RETAINED);
if pruned > 0 {
crate::service_log!("removed the records of {pruned} old finished batches from {}", context.journal.display());
}
let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
if let Some(instance) = settings.instance.as_deref() {
landed.extend(crate::earlier_service_journal_adopted::earlier_commits_landed(context.journal.as_path(), instance));
}
let mut withdrawn: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut captures: CaptureMemory = CaptureMemory::default();
let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
let acceptor_queue: Sender<Job> = sender.clone();
let (work_sender, work_receiver): (Sender<Wake>, Receiver<Wake>) = channel();
let handlers: GitHandlers = GitHandlers::new(context.git.clone());
let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
let board: Arc<LandingBoard> = LandingBoard::new(context.journal.clone(), settings.stuck_after);
crate::service_busy_step_record_on_this_thread::use_on_this_thread(Arc::clone(&board));
let connection_board: Arc<LandingBoard> = Arc::clone(&board);
let plan_directory: PathBuf = context.journal.join(PLAN_DIRECTORY);
let watch_directory: PathBuf = context.journal.join(WATCH_DIRECTORY);
let watch_instance: Option<String> = settings.instance.clone();
start_thread_or_exit("the after-commit work", move || { work_loop(&work_receiver, &handlers, &worker_busy, PushRetries::restored_from_environment(plan_directory.as_path()), ClaimWatch::restored(watch_directory.as_path(), watch_instance.as_deref())); });
let announced_release: String = settings.release.clone();
let connection_busy: Arc<AtomicUsize> = Arc::clone(&busy);
let instance: Option<String> = settings.instance.clone();
start_thread_or_exit("the acceptor of connections", move || {
for connection in listener.incoming().flatten() {
let queue: Sender<Job> = acceptor_queue.clone();
let work: Sender<Wake> = work_sender.clone();
let release: String = announced_release.clone();
let counted: Arc<AtomicUsize> = Arc::clone(&connection_busy);
let record: Arc<LandingBoard> = Arc::clone(&connection_board);
let mine: Option<String> = instance.clone();
let notice: Option<UnixStream> = connection.try_clone().ok();
let spawned = std::thread::Builder::new().spawn(move || { handle_connection(connection, &queue, &work, &counted, &record, release.as_str(), mine.as_deref()); });
if let Err(error) = spawned {
crate::service_error!("no thread for a connection, which is closed after telling the client: {error}");
if let Some(stream) = notice {
send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(announced_release.clone()) });
send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Unavailable });
}
std::thread::sleep(ACCEPT_BACKOFF);
}
}
});
let clock_seed: u64 = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(elapsed) => u64::try_from(elapsed.as_micros()).unwrap_or(1),
Err(_before_the_epoch) => 1,
};
let mut seq: u64 = crate::service_fault::pinned_first_batch().unwrap_or(clock_seed);
let _prestaged = PrestageGuard;
let mut retiring: bool = false;
while !retiring {
let first: Job = match receiver.recv_timeout(idle) {
Ok(job) => job,
Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
Err(RecvTimeoutError::Timeout) => {
crate::service_durable::drain();
log(exit_line(ExitReason::Idle, serving_since.elapsed()).as_str());
return;
}
Err(RecvTimeoutError::Disconnected) => {
crate::service_durable::drain();
log(exit_line(ExitReason::Error, serving_since.elapsed()).as_str());
return;
}
};
let mut jobs: Vec<Job> = vec![first];
while jobs.len() < MAX_BATCH {
match receiver.try_recv() {
Ok(job) => jobs.push(job),
Err(_) => break,
}
}
if withdrawn.len() > WITHDRAWN_RETAINED {
withdrawn.clear();
}
for job in &jobs {
if let Job::Withdraw(id, reply) = job {
match landed.get(id) {
Some(commit) => reply.send(&ServiceMessage::Landed { id: id.clone(), commit: commit.clone(), already: true }),
None => {
withdrawn.insert(id.clone());
reply.send(&ServiceMessage::Withdrawn { id: id.clone() });
}
}
}
}
let mut groups: Vec<(GroupKey, Vec<Queued>)> = Vec::new();
let mut releases: Vec<std::path::PathBuf> = Vec::new();
for job in jobs {
match job {
Job::Withdraw(..) => {}
Job::Commit(request, _dropped, _arrival) if withdrawn.contains(&request.id) => {
crate::service_log!("dropped the request {} that its client withdrew", request.id);
}
Job::Retire => retiring = true,
Job::Activity => {}
Job::Release(path) => releases.push(path),
Job::Commit(request, stream, arrival) => {
let queue_reference: String = if request.reference == "HEAD" {
format!("HEAD@{}", request.repository.display())
} else {
request.reference.clone()
};
let key: GroupKey = (request.common_dir.clone(), queue_reference, request.lands_alone());
place_in_groups(&mut groups, key, (request, stream, arrival));
}
}
}
for (_, queued_group) in groups {
let index: Option<std::path::PathBuf> = queued_group.first().map(index_of_queued);
let ids: Vec<String> = queued_group.iter().map(id_of_queued).collect();
let admission: GroupAdmission = board.begin_group(ids.as_slice());
for id in &admission.dropped {
crate::service_log!("dropped the request {id} that its client withdrew while the service was stuck in a write");
}
for id in &admission.unavailable {
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");
}
let mut unavailable: Vec<Queued> = Vec::new();
let mut admitted: Vec<Queued> = Vec::new();
for queued in queued_group {
if admission.dropped.contains(&queued.0.id) {
continue;
}
if admission.unavailable.contains(&queued.0.id) {
unavailable.push(queued);
} else {
admitted.push(queued);
}
}
for (request, stream, _) in &unavailable {
stream.send(&ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Unavailable });
}
let group: Vec<Queued> = admitted;
if group.is_empty() {
board.end_group(ids.as_slice());
continue;
}
land_group(context, settings.instance.as_deref(), &mut seq, group, &mut landed, &mut captures);
board.end_group(ids.as_slice());
if let Some(next_index) = index {
start_prestage(next_index.as_path(), seq + 1);
}
}
if unsettled_locks() > 0 {
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");
return;
}
for path in releases {
if let Err(error) = remove_tree(path.as_path()) {
crate::service_warn!("the scratch directory {} could not be removed: {error}", path.display());
}
}
}
crate::service_durable::drain();
log(exit_line(ExitReason::Retire, serving_since.elapsed()).as_str());
}
#[cfg(test)]
#[path = "service_server_tests.rs"]
mod tests;