use crate::earlier_service_process_look::{Hold, ServiceLook, earlier_service_hold, effective_user};
use crate::wait_ceilings::PROCESS_LOOK_CEILING;
use crate::service_land::{Handled, LandContext, RecoveryReport, recover_instance_without_drops};
use crate::service_instance::instance_of;
use crate::transaction_git::GitContext;
use crate::journald_native::Priority;
use crate::service_journal::{OPEN_EXTENSION, Recovery, batch_path, list_intents_read_only};
use crate::service_timing::log_entry;
use crate::wrapper_names_old_and_new::DATA_FOLDER;
use crate::service_journal::landed_commits_read_only;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
const SERVICE_FOLDER: &str = "service";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EarlierJournals {
pub per_instance: PathBuf,
pub shared: PathBuf,
}
pub fn earlier_journals_of(journal: &Path, instance: &str) -> Option<EarlierJournals> {
if journal.file_name()? != instance {
return None;
}
let service_folder: &Path = journal.parent()?;
if service_folder.file_name()? != SERVICE_FOLDER {
return None;
}
let data_folder: &Path = service_folder.parent()?;
if data_folder.file_name()? != DATA_FOLDER.current {
return None;
}
return Some(earlier_journals_in_state(data_folder.parent()?, instance));
}
pub fn earlier_journals_in_state(state: &Path, instance: &str) -> EarlierJournals {
let shared: PathBuf = state.join(DATA_FOLDER.earlier).join(SERVICE_FOLDER);
return EarlierJournals { per_instance: shared.join(instance), shared };
}
pub fn open_batch_count(earlier: &EarlierJournals, instance: &str) -> usize {
return open_batches_of(earlier.per_instance.as_path(), instance).len() + open_batches_of(earlier.shared.as_path(), instance).len();
}
fn open_batches_of(journal: &Path, instance: &str) -> Vec<u64> {
let mut seqs: Vec<u64> = Vec::new();
for (_path, parsed) in list_intents_read_only(journal, OPEN_EXTENSION) {
if let Some(intent) = parsed
&& instance_of(intent.git_dir.as_path()) == instance
{
seqs.push(intent.seq);
}
}
return seqs;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SettledBatch {
pub journal: PathBuf,
pub report: RecoveryReport,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnansweredBatch {
pub journal: PathBuf,
pub seq: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RequestedSettlement {
NothingOpen,
Held {
open: usize,
because: Hold,
},
Settled {
batches: Vec<SettledBatch>,
unanswered: Vec<UnansweredBatch>,
},
}
pub fn hold_on_earlier_helper(earlier: &EarlierJournals, instance: &str, proc_root: &Path) -> Option<Hold> {
let look: ServiceLook = ServiceLook {
instance: instance.to_string(),
journals: vec![earlier.per_instance.clone(), earlier.shared.clone()],
own_pid: std::process::id(),
own_user: effective_user(),
ceiling: PROCESS_LOOK_CEILING,
};
return earlier_service_hold(look, proc_root);
}
pub fn settle_on_request(git: &GitContext, earlier: &EarlierJournals, instance: &str, proc_root: &Path, lock_ceiling: std::time::Duration) -> RequestedSettlement {
let open: usize = open_batch_count(earlier, instance);
if open == 0 {
return RequestedSettlement::NothingOpen;
}
if let Some(because) = hold_on_earlier_helper(earlier, instance, proc_root) {
crate::service_log!(
"the earlier service's journals {} and {} were not settled, because {}; {open} open batch(es) stay where they are",
earlier.per_instance.display(),
earlier.shared.display(),
because.words()
);
return RequestedSettlement::Held { open, because };
}
let mut settled: Vec<SettledBatch> = Vec::new();
let mut unanswered: Vec<UnansweredBatch> = Vec::new();
for journal in [earlier.per_instance.clone(), earlier.shared.clone()] {
if !journal.is_dir() {
continue;
}
let selected: Vec<u64> = open_batches_of(journal.as_path(), instance);
let earlier_context: LandContext = LandContext { git: git.clone(), journal: journal.clone(), on_objects_written: None, lock_ceiling };
let reports: Vec<RecoveryReport> = recover_instance_without_drops(&earlier_context, instance);
log_adopted_batches(reports.as_slice(), journal.as_path());
for seq in selected {
let reported: bool = reports.iter().any(|report| { return report.seq == seq; });
if !reported && batch_path(journal.as_path(), seq, OPEN_EXTENSION).exists() {
unanswered.push(UnansweredBatch { journal: journal.clone(), seq });
}
}
settled.extend(reports.into_iter().map(|report| { return SettledBatch { journal: journal.clone(), report }; }));
}
return RequestedSettlement::Settled { batches: settled, unanswered };
}
pub fn earlier_commits_landed(journal: &Path, instance: &str) -> HashMap<String, String> {
let mut landed: HashMap<String, String> = HashMap::new();
let Some(earlier) = earlier_journals_of(journal, instance) else {
return landed;
};
for folder in [earlier.shared, earlier.per_instance] {
landed.extend(landed_commits_read_only(folder.as_path()));
}
return landed;
}
fn log_adopted_batches(reports: &[RecoveryReport], journal: &Path) {
for report in reports {
let what: &str = match report.handled {
Handled::Finished => "finished the batch the earlier service left open",
Handled::StillOpen => "left open the batch the earlier service left open, which could not be finished now (the index lock is held or a record cannot be read); the next start tries again",
Handled::Left => "left on disk the intent of a batch the earlier service wrote: the branch shows it as not landed, and a landing in progress writes its intent first, so the earlier service's own start settles it",
Handled::Dropped => "dropped the intent of a batch the earlier service wrote",
};
log_entry(
Priority::Warning,
format!("{what} batch={} decision={} journal={}", report.seq, if report.decision == Recovery::Finish { "finish" } else { "drop" }, journal.display()).as_str(),
&[("CLI_GIT_BATCH", report.seq.to_string())],
);
}
}