use crate::claim_time::ClaimTime;
use crate::work_item::{CLAIMED, Claimed, id_of_entry, parse_claim_name, renew_claim, settle_named, state_directory};
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, SyncSender, channel, sync_channel};
use std::sync::{Arc, OnceLock};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
pub const RENEWALS_PER_LEASE: u32 = 3;
const LOOKS: usize = 3;
pub fn renewal_interval(lease: Duration) -> Duration {
return lease / RENEWALS_PER_LEASE;
}
#[derive(Debug)]
struct Claim {
id: String,
token: String,
lost: AtomicBool,
settled: AtomicBool,
}
type Taken = (Arc<Claim>, String);
enum Control {
Renew(ClaimTime, Option<SyncSender<()>>),
Stop,
}
#[derive(Debug)]
pub struct HeldClaims {
claims: RefCell<Vec<Arc<Claim>>>,
holds: Sender<Taken>,
}
pub struct Renewer {
work_dir: PathBuf,
holds: Receiver<Taken>,
names: Vec<(Arc<Claim>, String)>,
}
pub fn held_claims(work_dir: &Path) -> (HeldClaims, Renewer) {
let (holds, taken) = channel::<Taken>();
return (
HeldClaims { claims: RefCell::new(Vec::new()), holds },
Renewer { work_dir: work_dir.to_path_buf(), holds: taken, names: Vec::new() },
);
}
fn names_claim(name: &str, claim: &Claim) -> bool {
if id_of_entry(name) != claim.id {
return false;
}
return claim.token.is_empty() || parse_claim_name(name).is_some_and(|(token, _)| { return token == claim.token; });
}
fn current_name(work_dir: &Path, claim: &Claim) -> Option<String> {
for _ in 0..LOOKS {
let found: Option<String> = std::fs::read_dir(state_directory(work_dir, CLAIMED))
.ok()
.and_then(|listing| { return listing.flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).find(|name| { return names_claim(name.as_str(), claim); }); });
if found.is_some() {
return found;
}
}
return None;
}
impl HeldClaims {
pub fn hold(&self, claimed: &Claimed) {
let token: String = parse_claim_name(claimed.name.as_str()).map(|(token, _)| { return token.to_owned(); }).unwrap_or_default();
let claim: Arc<Claim> = Arc::new(Claim { id: claimed.id.clone(), token, lost: AtomicBool::new(false), settled: AtomicBool::new(false) });
self.claims.borrow_mut().push(Arc::clone(&claim));
let _sent = self.holds.send((claim, claimed.name.clone()));
}
pub fn release(&self, id: &str) {
self.claims.borrow_mut().retain(|claim| {
if claim.id == id {
claim.settled.store(true, Ordering::SeqCst);
}
return claim.id != id;
});
}
pub fn snapshot(&self) -> Vec<String> {
return self.claims.borrow().iter().map(|claim| { return claim.id.clone(); }).collect();
}
pub fn is_held(&self, id: &str) -> bool {
return self.claims.borrow().iter().any(|claim| { return claim.id == id && !claim.lost.load(Ordering::SeqCst); });
}
pub fn is_held_on_disk(&self, work_dir: &Path, id: &str) -> bool {
let claims = self.claims.borrow();
let Some(claim) = claims.iter().find(|candidate| { return candidate.id == id; }) else { return false };
if claim.lost.load(Ordering::SeqCst) {
return false;
}
if current_name(work_dir, claim).is_none() {
claim.lost.store(true, Ordering::SeqCst);
return false;
}
return true;
}
pub fn settle(&self, work_dir: &Path, id: &str, state: &str) -> Result<(), String> {
let claim: Arc<Claim> = {
let mut claims = self.claims.borrow_mut();
let Some(position) = claims.iter().position(|candidate| { return candidate.id == id; }) else {
return Err(format!("{id} is not held by this instance"));
};
claims.remove(position)
};
claim.settled.store(true, Ordering::SeqCst);
if claim.lost.load(Ordering::SeqCst) {
return Err(format!("{id} was taken over by another instance"));
}
let mut last: String = String::new();
for _ in 0..LOOKS {
let Some(name) = current_name(work_dir, &claim) else { break };
match settle_named(work_dir, name.as_str(), state) {
Ok(()) => return Ok(()),
Err(reason) => last = reason,
}
}
let reason: String = if last.is_empty() { format!("moving {id} to {state} failed: the claim is not in {CLAIMED}") } else { last };
return Err(format!("{reason}; its lease probably ran out and another instance took the work over"));
}
}
impl Renewer {
fn take_in(&mut self) {
while let Ok(taken) = self.holds.try_recv() {
self.names.push(taken);
}
}
pub fn renew(&mut self, now: &ClaimTime) {
self.take_in();
self.names.retain(|(claim, _)| { return !claim.settled.load(Ordering::SeqCst); });
for (claim, name) in &mut self.names {
if claim.lost.load(Ordering::SeqCst) {
continue;
}
match renew_claim(self.work_dir.as_path(), name.as_str(), now) {
Some(renewed) => *name = renewed,
None => claim.lost.store(true, Ordering::SeqCst),
}
}
}
pub fn names(&mut self) -> Vec<String> {
self.take_in();
return self.names.iter().map(|(_, name)| { return name.clone(); }).collect();
}
}
#[derive(Clone, Copy, Debug)]
pub enum Renewal<'a> {
Timed,
Driven(&'a DrivenRenewal),
}
#[derive(Debug, Default)]
pub struct DrivenRenewal {
ticks: OnceLock<Sender<Control>>,
}
impl DrivenRenewal {
fn attach(&self, control: Sender<Control>) {
let _first: Result<(), Sender<Control>> = self.ticks.set(control);
}
pub fn renew(&self, now: &ClaimTime) -> bool {
let Some(control) = self.ticks.get() else { return false };
let (done, finished) = sync_channel::<()>(1);
return control.send(Control::Renew(now.clone(), Some(done))).is_ok() && finished.recv().is_ok();
}
}
pub struct ClaimRenewal {
control: Sender<Control>,
worker: Option<JoinHandle<()>>,
}
fn obey(renewer: &mut Renewer, control: Control) -> bool {
return match control {
Control::Renew(now, done) => {
renewer.renew(&now);
if let Some(finished) = done {
let _told = finished.send(());
}
true
}
Control::Stop => false,
};
}
impl ClaimRenewal {
pub fn start(mut renewer: Renewer, every: Duration) -> ClaimRenewal {
let (control, requests) = channel::<Control>();
let worker: Option<JoinHandle<()>> = std::thread::Builder::new()
.spawn(move || {
let mut due: Instant = Instant::now() + every;
loop {
match requests.recv_timeout(due.saturating_duration_since(Instant::now())) {
Ok(request) => {
if !obey(&mut renewer, request) {
return;
}
}
Err(RecvTimeoutError::Disconnected) => return,
Err(RecvTimeoutError::Timeout) => {
renewer.renew(&ClaimTime::read());
due = Instant::now() + every;
}
}
}
})
.map_err(|error| { crate::service_error!("no thread to renew claims: {error}"); })
.ok();
return ClaimRenewal { control, worker };
}
pub fn driven(mut renewer: Renewer, driver: &DrivenRenewal) -> ClaimRenewal {
let (control, requests) = channel::<Control>();
driver.attach(control.clone());
let worker: Option<JoinHandle<()>> = std::thread::Builder::new()
.spawn(move || {
while let Ok(request) = requests.recv() {
if !obey(&mut renewer, request) {
return;
}
}
})
.map_err(|error| { crate::service_error!("no thread to renew claims: {error}"); })
.ok();
return ClaimRenewal { control, worker };
}
}
impl Drop for ClaimRenewal {
fn drop(&mut self) {
let _told = self.control.send(Control::Stop);
if let Some(worker) = self.worker.take() {
let _joined = worker.join();
}
}
}
#[cfg(test)]
#[path = "service_claim_renewal_tests.rs"]
mod tests;