use crate::claim_time::ClaimTime;
use crate::private_storage::create_private_directory_all;
use crate::service_timing::log;
use crate::work_item::{
CLAIMED, FAILED, MAX_OBJECT_NAME_LENGTH, PENDING, RECOVERING, WorkItem, claim_file_name, entries_oldest_first, id_of_entry, is_valid_id,
new_generation, parse_claim_name, state_directory,
};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub const STRAY_TEMPORARY_AGE: Duration = Duration::from_secs(24 * 60 * 60);
pub const TEMPORARY_SUFFIX: &str = ".retry";
pub fn requeue_expired(work_dir: &Path, now: &ClaimTime, lease: Duration, max_tries: u32) -> usize {
return requeue_expired_reporting(work_dir, now, lease, max_tries).moved;
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Recovered {
pub moved: usize,
pub given_up: Vec<WorkItem>,
}
pub fn requeue_expired_reporting(work_dir: &Path, now: &ClaimTime, lease: Duration, max_tries: u32) -> Recovered {
return requeue_with(work_dir, now, max_tries, &|stamp| { return now.is_past_the_boot_of(stamp) || now.elapsed_since(stamp) > lease; });
}
pub fn next_claim_expiry(work_dir: &Path, now: &ClaimTime, lease: Duration) -> Option<Duration> {
return [CLAIMED, RECOVERING]
.into_iter()
.flat_map(|state| { return entries_oldest_first(work_dir, state); })
.filter(|(name, _)| { return !name.ends_with(TEMPORARY_SUFFIX); })
.map(|(name, modified)| { return lease_left(now, &stamp_of_entry(name.as_str(), modified), lease); })
.min();
}
fn lease_left(now: &ClaimTime, stamp: &ClaimTime, lease: Duration) -> Duration {
if now.is_past_the_boot_of(stamp) {
return Duration::ZERO;
}
return lease.saturating_sub(now.elapsed_since(stamp));
}
fn stamp_of_entry(name: &str, modified: SystemTime) -> ClaimTime {
return parse_claim_name(name).map_or_else(|| { return ClaimTime::from_wall(modified); }, |(_, stamp)| { return stamp; });
}
pub(crate) fn requeue_with(work_dir: &Path, now: &ClaimTime, max_tries: u32, expired: &dyn Fn(&ClaimTime) -> bool) -> Recovered {
let mut recovered: Recovered = Recovered::default();
for state in [CLAIMED, RECOVERING] {
for (name, modified) in entries_oldest_first(work_dir, state) {
if name.ends_with(TEMPORARY_SUFFIX) {
remove_stray_temporary(work_dir, state, name.as_str(), modified, now);
continue;
}
if !expired(&stamp_of_entry(name.as_str(), modified)) {
continue;
}
if let Some(given_up) = recover(work_dir, state, name.as_str(), now, max_tries) {
recovered.moved += 1;
recovered.given_up.extend(given_up);
}
}
}
return recovered;
}
fn remove_stray_temporary(work_dir: &Path, state: &str, name: &str, modified: SystemTime, now: &ClaimTime) {
if now.elapsed_since(&ClaimTime::from_wall(modified)) > STRAY_TEMPORARY_AGE {
let _removed = std::fs::remove_file(state_directory(work_dir, state).join(name));
}
}
fn recover(work_dir: &Path, state: &str, name: &str, now: &ClaimTime, max_tries: u32) -> Option<Option<WorkItem>> {
let id: &str = id_of_entry(name);
if !is_valid_id(id) {
let moved: bool = create_private_directory_all(state_directory(work_dir, FAILED).as_path()).is_ok()
&& std::fs::rename(state_directory(work_dir, state).join(name), state_directory(work_dir, FAILED).join(id)).is_ok();
log(format!("the claim {name:?} is not of a kind and an object name of at most {MAX_OBJECT_NAME_LENGTH} letters and digits, so it was {}", if moved { "moved to failed" } else { "left where it is because moving it failed" }).as_str());
return moved.then_some(None);
}
let Ok(token) = new_generation() else {
return None;
};
let owned_name: String = claim_file_name(id, token.as_str(), now);
let recovering: PathBuf = state_directory(work_dir, RECOVERING);
if create_private_directory_all(recovering.as_path()).is_err() {
return None;
}
let owned: PathBuf = recovering.join(owned_name.as_str());
if std::fs::rename(state_directory(work_dir, state).join(name), owned.as_path()).is_err() {
return None;
}
let Ok(text) = std::fs::read_to_string(owned.as_path()) else {
return std::fs::rename(owned.as_path(), state_directory(work_dir, FAILED).join(id)).is_ok().then_some(None);
};
let (target, given_up): (&str, Option<WorkItem>) = match WorkItem::decode(text.as_str()) {
Ok(mut item) if item.id() == id => {
item.tries += 1;
let rewritten: PathBuf = recovering.join(format!("{owned_name}{TEMPORARY_SUFFIX}"));
let destination: &str = if item.tries >= max_tries { FAILED } else { PENDING };
if std::fs::write(rewritten.as_path(), item.encode()).is_err() || std::fs::rename(rewritten.as_path(), owned.as_path()).is_err() {
return None;
}
(destination, (destination == FAILED).then_some(item))
}
_ => (FAILED, None),
};
return std::fs::rename(owned.as_path(), state_directory(work_dir, target).join(id)).is_ok().then_some(given_up);
}