pub use crate::service_journal_intent_text_and_parse::{Intent, intent_text, parse_intent};
use crate::service_land_start::{IndexWitness, decode, encode};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
pub const OPEN_EXTENSION: &str = "intent";
pub const DONE_EXTENSION: &str = "done";
const TEMPORARY_EXTENSION: &str = "tmp";
pub const CLAIM_EXTENSION: &str = "claim";
pub const TEMPORARY_GRACE: std::time::Duration = std::time::Duration::from_secs(10 * 60);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Recovery {
Drop,
Finish,
}
pub fn recovery_for(intent: &Intent, current: &str, new_is_ancestor_of_current: bool) -> Recovery {
if current == intent.new || (current != intent.old && new_is_ancestor_of_current) {
return Recovery::Finish;
}
return Recovery::Drop;
}
pub fn batch_path(dir: &Path, seq: u64, extension: &str) -> PathBuf {
return dir.join(format!("{seq}.{extension}"));
}
#[derive(Debug)]
pub struct BatchClaim {
seq: u64,
path: PathBuf,
}
impl BatchClaim {
pub fn seq(&self) -> u64 {
return self.seq;
}
}
impl Drop for BatchClaim {
fn drop(&mut self) {
let _removed = fs::remove_file(&self.path);
}
}
fn number_has_record(dir: &Path, seq: u64) -> bool {
return batch_path(dir, seq, OPEN_EXTENSION).exists() || batch_path(dir, seq, DONE_EXTENSION).exists();
}
pub fn claim_batch(dir: &Path, first: u64) -> std::io::Result<BatchClaim> {
let mut seq: u64 = first;
loop {
let path: PathBuf = batch_path(dir, seq, CLAIM_EXTENSION);
match File::options().write(true).create_new(true).open(&path) {
Ok(_created) => {
let claim: BatchClaim = BatchClaim { seq, path };
if !number_has_record(dir, seq) {
return Ok(claim);
}
drop(claim);
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
seq = match seq.checked_add(1) {
Some(next) => next,
None => return Err(std::io::Error::other("the journal holds every batch number")),
};
}
}
pub fn write_intent(dir: &Path, intent: &Intent) -> std::io::Result<()> {
let temporary: PathBuf = batch_path(dir, intent.seq, TEMPORARY_EXTENSION);
let mut file: File = File::create(&temporary)?;
file.write_all(intent_text(intent).as_bytes())?;
crate::service_durable::flush_file(&file, "intent-file")?;
fs::rename(&temporary, batch_path(dir, intent.seq, OPEN_EXTENSION))?;
return crate::service_durable::flush_directory(dir, "intent-dir");
}
pub const INDEXED_EXTENSION: &str = "indexed";
pub fn mark_indexed(dir: &Path, seq: u64, new: &str) -> std::io::Result<()> {
let temporary: PathBuf = batch_path(dir, seq, TEMPORARY_EXTENSION);
let mut file: File = File::create(&temporary)?;
file.write_all(format!("{new}\n").as_bytes())?;
crate::service_durable::flush_file(&file, "indexed-file")?;
fs::rename(&temporary, batch_path(dir, seq, INDEXED_EXTENSION))?;
crate::service_fault::note("indexed-recorded");
return crate::service_durable::flush_directory(dir, "indexed-dir");
}
pub const START_EXTENSION: &str = "start";
pub fn write_start(dir: &Path, seq: u64, witness: &IndexWitness) -> std::io::Result<()> {
let temporary: PathBuf = batch_path(dir, seq, TEMPORARY_EXTENSION);
let mut file: File = File::create(&temporary)?;
file.write_all(format!("{}\n", encode(witness)).as_bytes())?;
return fs::rename(&temporary, batch_path(dir, seq, START_EXTENSION));
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Started {
Absent,
Recorded(IndexWitness),
Unreadable,
}
pub fn read_start(dir: &Path, seq: u64) -> Started {
return match fs::read_to_string(batch_path(dir, seq, START_EXTENSION)) {
Ok(text) => decode(text.trim_end_matches('\n')).map_or(Started::Absent, Started::Recorded),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Started::Absent,
Err(error) if error.kind() == std::io::ErrorKind::InvalidData => Started::Absent,
Err(_) => Started::Unreadable,
};
}
pub fn remove_start(dir: &Path, seq: u64) {
let _removed = fs::remove_file(batch_path(dir, seq, START_EXTENSION));
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Indexed {
Absent,
Installed,
Unreadable,
}
pub fn indexed_state(dir: &Path, seq: u64, new: &str) -> Indexed {
return match fs::read_to_string(batch_path(dir, seq, INDEXED_EXTENSION)) {
Ok(text) if text.trim_end() == new => Indexed::Installed,
Ok(_) => Indexed::Unreadable,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Indexed::Absent,
Err(_) => Indexed::Unreadable,
};
}
pub fn mark_done(dir: &Path, seq: u64) -> std::io::Result<()> {
fs::rename(batch_path(dir, seq, OPEN_EXTENSION), batch_path(dir, seq, DONE_EXTENSION))?;
crate::service_durable::flush_directory(dir, "journal-dir")?;
crate::service_fault::note("remove-indexed");
let _record_removed = fs::remove_file(batch_path(dir, seq, INDEXED_EXTENSION));
remove_start(dir, seq);
return Ok(());
}
fn is_abandoned(entry: &fs::DirEntry) -> bool {
let Ok(meta) = entry.metadata() else {
return false;
};
let Ok(modified) = meta.modified() else {
return false;
};
return match modified.elapsed() {
Ok(age) => age > TEMPORARY_GRACE,
Err(_in_the_future) => false,
};
}
fn sort_key_of_found(listed: &(PathBuf, Option<Intent>)) -> u64 {
let (path, intent) = listed;
return match intent {
Some(parsed) => parsed.seq,
None => path.to_string_lossy().len() as u64,
};
}
fn finished_batch_of_entry(entry: fs::DirEntry) -> Option<(u64, PathBuf)> {
let path: PathBuf = entry.path();
if path.extension()? != DONE_EXTENSION || !entry.file_type().ok()?.is_file() {
return None;
}
let seq: u64 = path.file_stem()?.to_str()?.parse().ok()?;
return Some((seq, path));
}
fn newest_first_key(finished: &(u64, PathBuf)) -> std::cmp::Reverse<u64> {
return std::cmp::Reverse(finished.0);
}
pub fn list_intents(dir: &Path, extension: &str) -> Vec<(PathBuf, Option<Intent>)> {
return list_intents_removing_abandoned(dir, extension, true);
}
pub fn list_intents_read_only(dir: &Path, extension: &str) -> Vec<(PathBuf, Option<Intent>)> {
return list_intents_removing_abandoned(dir, extension, false);
}
fn list_intents_removing_abandoned(dir: &Path, extension: &str, remove_abandoned: bool) -> Vec<(PathBuf, Option<Intent>)> {
let Ok(entries) = fs::read_dir(dir) else {
return Vec::new();
};
let mut found: Vec<(PathBuf, Option<Intent>)> = Vec::new();
for entry in entries.flatten() {
let path: PathBuf = entry.path();
match path.extension().and_then(std::ffi::OsStr::to_str) {
Some(TEMPORARY_EXTENSION | CLAIM_EXTENSION) => {
let abandoned: bool = remove_abandoned && is_abandoned(&entry);
if abandoned {
let _removed = fs::remove_file(&path);
}
}
Some(found_extension) if found_extension == extension => {
let parsed: Option<Intent> = match fs::read_to_string(&path) {
Ok(text) => parse_intent(text.as_str()),
Err(_unreadable) => None,
};
found.push((path, parsed));
}
_ => {}
}
}
found.sort_by_key(sort_key_of_found);
return found;
}
pub const DONE_RETAINED: usize = 1024;
pub fn prune_done(dir: &Path, retained: usize) -> usize {
let Ok(entries) = fs::read_dir(dir) else {
return 0;
};
let mut finished: Vec<(u64, PathBuf)> = entries
.flatten()
.filter_map(finished_batch_of_entry)
.collect();
finished.sort_by_key(newest_first_key);
let mut removed: usize = 0;
for (_, path) in finished.into_iter().skip(retained) {
if fs::remove_file(path).is_ok() {
removed += 1;
}
}
return removed;
}
pub fn landed_commits(dir: &Path) -> HashMap<String, String> {
return landed_in(list_intents(dir, DONE_EXTENSION));
}
pub fn landed_commits_read_only(dir: &Path) -> HashMap<String, String> {
return landed_in(list_intents_read_only(dir, DONE_EXTENSION));
}
fn landed_in(listed: Vec<(PathBuf, Option<Intent>)>) -> HashMap<String, String> {
let mut landed: HashMap<String, String> = HashMap::new();
for (_, intent) in listed {
if let Some(parsed) = intent {
landed.extend(parsed.ids.into_iter().zip(parsed.commits));
}
}
return landed;
}
#[cfg(test)]
#[path = "service_journal_tests.rs"]
mod tests;