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 is_unsettled(entry: &(Arc<Claim>, String)) -> bool {
return !entry.0.settled.load(Ordering::SeqCst);
}
fn names_claim(name: &str, claim: &Claim) -> bool {
if id_of_entry(name) != claim.id {
return false;
}
if claim.token.is_empty() {
return true;
}
return match parse_claim_name(name) {
Some((token, _stamp)) => token == claim.token,
None => false,
};
}
fn current_name(work_dir: &Path, claim: &Claim) -> Option<String> {
for _ in 0..LOOKS {
let Ok(listing) = std::fs::read_dir(state_directory(work_dir, CLAIMED)) else {
continue;
};
for entry in listing.flatten() {
let name: String = entry.file_name().to_string_lossy().into_owned();
if names_claim(name.as_str(), claim) {
return Some(name);
}
}
}
return None;
}
impl HeldClaims {
pub fn hold(&self, claimed: &Claimed) {
let token: String = match parse_claim_name(claimed.name.as_str()) {
Some((found, _stamp)) => found.to_owned(),
None => String::new(),
};
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) {
let mut claims = self.claims.borrow_mut();
let mut kept: Vec<Arc<Claim>> = Vec::with_capacity(claims.len());
for claim in claims.drain(..) {
if claim.id == id {
claim.settled.store(true, Ordering::SeqCst);
} else {
kept.push(claim);
}
}
*claims = kept;
}
pub fn snapshot(&self) -> Vec<String> {
let mut ids: Vec<String> = Vec::new();
for claim in self.claims.borrow().iter() {
ids.push(claim.id.clone());
}
return ids;
}
pub fn is_held(&self, id: &str) -> bool {
for claim in self.claims.borrow().iter() {
if claim.id == id && !claim.lost.load(Ordering::SeqCst) {
return true;
}
}
return false;
}
pub fn is_held_on_disk(&self, work_dir: &Path, id: &str) -> bool {
let claims = self.claims.borrow();
let mut found: Option<&Arc<Claim>> = None;
for candidate in claims.iter() {
if candidate.id == id {
found = Some(candidate);
break;
}
}
let Some(claim) = found 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 mut found_at: Option<usize> = None;
for (at, candidate) in claims.iter().enumerate() {
if candidate.id == id {
found_at = Some(at);
break;
}
}
let Some(position) = found_at 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(is_unsettled);
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();
let mut names: Vec<String> = Vec::new();
for (_claim, name) in &self.names {
names.push(name.clone());
}
return names;
}
}
#[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 spawned: std::io::Result<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;
}
}
}
});
let worker: Option<JoinHandle<()>> = match spawned {
Ok(handle) => Some(handle),
Err(error) => {
crate::service_error!("no thread to renew claims: {error}");
None
}
};
return ClaimRenewal { control, worker };
}
pub fn driven(mut renewer: Renewer, driver: &DrivenRenewal) -> ClaimRenewal {
let (control, requests) = channel::<Control>();
driver.attach(control.clone());
let spawned: std::io::Result<JoinHandle<()>> = std::thread::Builder::new()
.spawn(move || {
while let Ok(request) = requests.recv() {
if !obey(&mut renewer, request) {
return;
}
}
});
let worker: Option<JoinHandle<()>> = match spawned {
Ok(handle) => Some(handle),
Err(error) => {
crate::service_error!("no thread to renew claims: {error}");
None
}
};
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;