use crate::service_client_settings_and_watches::RECONNECTS;
use crate::service_client_listen_and_ask_to_retire::connect;
pub use crate::service_client_listen_and_ask_to_retire::{START_WAIT, ask_to_retire};
use crate::service_client_listen_and_ask_to_retire::{Heard, listen, read_message};
pub use crate::service_client_settings_and_watches::{Activation, ClientConfig, FreshScratch, ManagerWatch, START_NOTICE_GRACE, manager_watch_for};
use crate::service_client_settings_and_watches::CONNECT_PAUSE;
use crate::service_client_reach_service::reach_current;
use crate::commit_timing::{ReachTrace, SubmitTrace, holder_kind_of};
use crate::service_messages::{ClientFailure, SILENT_NOTICE, position_notice};
use crate::service_protocol::{
ClientMessage, CommitRequest, FailureCode, LandedCapture, ServiceMessage, encode_client,
};
use crate::service_review_key::ReviewKey;
use std::io::{BufReader, Write};
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Submitted {
Landed { commit: String, already: bool },
PrepareAgain { base: String, landed: Vec<LandedCapture> },
Moved,
}
pub fn failure_for(code: FailureCode, request: &CommitRequest) -> ClientFailure {
let repository: String = request.repository.display().to_string();
return match code {
FailureCode::RepositoryFull => ClientFailure::RepositoryFull(repository),
FailureCode::RepositoryReadOnly => ClientFailure::RepositoryReadOnly(repository),
FailureCode::IndexInUse => ClientFailure::IndexInUse(String::from("Another program")),
FailureCode::Rejected => ClientFailure::Refused,
FailureCode::BranchKeepsChanging => ClientFailure::BranchKeepsChanging {
branch: request.reference.trim_start_matches("refs/heads/").to_string(),
times: crate::service_land::LANDING_ATTEMPTS,
},
FailureCode::BranchMoved => ClientFailure::BranchMoved { branch: request.reference.trim_start_matches("refs/heads/").to_string() },
FailureCode::StepTooSlow => ClientFailure::StepTooSlow,
FailureCode::Unavailable => ClientFailure::Unavailable,
};
}
pub fn submit(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write) -> Result<Submitted, ClientFailure> {
return submit_traced(config, request, notices, &mut SubmitTrace::default());
}
pub fn submit_traced(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write, trace: &mut SubmitTrace) -> Result<Submitted, ClientFailure> {
let began: Instant = Instant::now();
let answered = submit_loop(config, request, notices, trace);
trace.total += began.elapsed();
return answered;
}
fn renew_scratch(settings: &ClientConfig, last_renewed: &mut Option<Instant>) {
let Some(fresh) = settings.keep_fresh.as_ref() else {
return;
};
let due: bool = match *last_renewed {
Some(at) => at.elapsed() >= fresh.every,
None => true,
};
if due {
crate::scratch_location::renew(fresh.path.as_path());
*last_renewed = Some(Instant::now());
}
}
fn submit_loop(config: &ClientConfig, request: &CommitRequest, notices: &mut dyn Write, trace: &mut SubmitTrace) -> Result<Submitted, ClientFailure> {
let line: String = encode_client(&ClientMessage::Commit(Box::new(request.clone())));
let mut announced: bool = false;
let mut breaks: usize = 0;
let mut last_position: Option<usize> = None;
let mut holder: Option<String> = None;
let mut service_seen: bool = false;
let mut last_break_was_silence: bool = false;
let waiting_since: Instant = Instant::now();
let mut last_blocker: Option<String> = None;
let mut inside_write: bool = false;
let answer_deadline: Instant = waiting_since + config.answer_ceiling;
let mut last_renewed: Option<Instant> = None;
loop {
let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced, &mut trace.reach, service_seen)?;
service_seen = true;
let mut payload: String = line.clone();
payload.push('\n');
if reader.get_mut().write_all(payload.as_bytes()).is_err() {
breaks += 1;
trace.reconnects += 1;
if breaks > RECONNECTS {
return Err(ClientFailure::MayNotHaveLanded);
}
continue;
}
trace.sends += 1;
renew_scratch(config, &mut last_renewed);
let written_at: Instant = Instant::now();
let answered: Option<Result<Submitted, ClientFailure>> = loop {
let heard: Heard = listen(&mut reader);
if matches!(heard, Heard::Message(_)) {
renew_scratch(config, &mut last_renewed);
}
if matches!(heard, Heard::Message(ServiceMessage::InsideWrite { .. })) {
inside_write = true;
}
if waiting_since.elapsed() >= config.answer_ceiling && !matches!(heard, Heard::Message(ServiceMessage::Landed { .. } | ServiceMessage::RetryPrepare { .. } | ServiceMessage::Failed { .. })) {
if !inside_write {
trace.answer_wait += written_at.elapsed();
return withdraw(config, request.id.as_str(), last_blocker.take(), answer_deadline);
}
if Instant::now() >= answer_deadline + config.extra_wait {
trace.answer_wait += written_at.elapsed();
return Err(ClientFailure::WithdrawalUnconfirmed { blocker: last_blocker.take() });
}
}
match heard {
Heard::Message(ServiceMessage::Position { number, of }) => {
if last_position != Some(number) {
last_position = Some(number);
trace.positions.push(number);
let _written = writeln!(notices, "{}", position_notice(number, of));
}
}
Heard::Message(ServiceMessage::Landed { commit, already, .. }) => break Some(Ok(Submitted::Landed { commit, already })),
Heard::Message(ServiceMessage::RetryPrepare { base, landed, .. }) => break Some(Ok(Submitted::PrepareAgain { base, landed })),
Heard::Message(ServiceMessage::Failed { code: FailureCode::BranchMoved, .. }) => break Some(Ok(Submitted::Moved)),
Heard::Message(ServiceMessage::Failed { code: FailureCode::IndexInUse, .. }) => {
break Some(Err(ClientFailure::IndexInUse(match holder.take() {
Some(who) => who,
None => String::from("Another program"),
})));
}
Heard::Message(ServiceMessage::Failed { code, .. }) => break Some(Err(failure_for(code, request))),
Heard::Message(ServiceMessage::Waiting { text } | ServiceMessage::InsideWrite { text }) => {
last_blocker = Some(text.trim().to_string());
trace.waiting_notices += 1;
trace.holder_kind = Some(holder_kind_of(text.as_str()));
let _written = write!(notices, "{text}");
}
Heard::Message(ServiceMessage::Holder { who }) => holder = Some(who),
Heard::Message(ServiceMessage::Welcome { .. } | ServiceMessage::Heartbeat | ServiceMessage::Withdrawn { .. }) => {}
Heard::Silent => {
last_break_was_silence = true;
break None;
}
Heard::Closed => {
last_break_was_silence = false;
break None;
}
}
};
trace.answer_wait += written_at.elapsed();
if let Some(done) = answered {
return done;
}
breaks += 1;
if breaks > RECONNECTS {
return Err(if last_break_was_silence { ClientFailure::ServiceSilent } else { ClientFailure::MayNotHaveLanded });
}
if last_break_was_silence {
let _written = writeln!(notices, "{SILENT_NOTICE}");
}
std::thread::sleep(CONNECT_PAUSE * u32::try_from(breaks).unwrap_or(1));
}
}
fn withdraw(config: &ClientConfig, id: &str, heard_blocker: Option<String>, answer_deadline: Instant) -> Result<Submitted, ClientFailure> {
let minutes: u64 = config.answer_ceiling.as_secs() / 60;
let mut blocker: Option<String> = heard_blocker;
let mut announced: bool = true;
let Ok(mut reader) = reach_current(config, &mut std::io::sink(), &mut announced, &mut ReachTrace::default(), true) else {
return Err(ClientFailure::WithdrawalUnconfirmed { blocker });
};
let mut asking: String = encode_client(&ClientMessage::Withdraw { id: id.to_string() });
asking.push('\n');
if reader.get_mut().write_all(asking.as_bytes()).is_err() {
return Err(ClientFailure::WithdrawalUnconfirmed { blocker });
}
let mut deadline: Instant = Instant::now() + config.withdraw_ceiling;
while Instant::now() < deadline {
let _timeout = reader.get_ref().set_read_timeout(Some(deadline.saturating_duration_since(Instant::now()).max(Duration::from_millis(1)).min(config.heartbeat_silence)));
match listen(&mut reader) {
Heard::Message(ServiceMessage::Landed { commit, .. }) => return Ok(Submitted::Landed { commit, already: true }),
Heard::Message(ServiceMessage::Withdrawn { .. }) => return Err(ClientFailure::StillWithService { minutes, blocker }),
Heard::Message(ServiceMessage::Waiting { text }) => blocker = Some(text.trim().to_string()),
Heard::Message(ServiceMessage::InsideWrite { text }) => {
blocker = Some(text.trim().to_string());
deadline = deadline.min(answer_deadline + config.extra_wait);
}
Heard::Message(_) => {}
Heard::Silent | Heard::Closed => return Err(ClientFailure::WithdrawalUnconfirmed { blocker }),
}
}
return Err(ClientFailure::WithdrawalUnconfirmed { blocker });
}
pub struct ScratchWatch {
_connection: UnixStream,
}
pub fn watch_scratch(config: &ClientConfig, git_dir: &std::path::Path, path: &std::path::Path) -> Option<ScratchWatch> {
let mut connection: UnixStream = connect(&config.address).ok()?;
let mut line: String = encode_client(&ClientMessage::Scratch { git_dir: git_dir.to_path_buf(), path: path.to_path_buf() });
line.push('\n');
connection.write_all(line.as_bytes()).ok()?;
return Some(ScratchWatch { _connection: connection });
}
pub fn wake(config: &ClientConfig, git_dir: &std::path::Path, review_key: Option<&ReviewKey>, notices: &mut dyn Write) -> Result<(), ClientFailure> {
let mut line: String = encode_client(&ClientMessage::Work { git_dir: git_dir.to_path_buf(), review_key: review_key.cloned() });
line.push('\n');
let mut announced: bool = false;
let mut reader: BufReader<UnixStream> = reach_current(config, notices, &mut announced, &mut ReachTrace::default(), false)?;
if reader.get_mut().write_all(line.as_bytes()).is_err() {
return Err(ClientFailure::Unavailable);
}
while read_message(&mut reader).is_some() {}
return Ok(());
}
#[cfg(test)]
#[path = "service_client_tests.rs"]
mod tests;