use areev_cal::AreevFacade;
use areev_run_core::RunError;
use serde::{Deserialize, Serialize};
const RUN_LEASE_PREFIX: &str = "runlease:";
pub const DEFAULT_RUN_LEASE_MS: i64 = 600_000;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
struct LeaseRow {
holder: String,
fence: u64,
until_ms: i64,
}
pub struct RunLease {
key: String,
holder: String,
seen: String,
fence: u64,
lease_ms: i64,
}
impl RunLease {
pub fn acquire(
facade: &AreevFacade,
run_id: &str,
holder: &str,
now_ms: i64,
lease_ms: i64,
) -> Result<RunLease, RunError> {
let key = format!("{RUN_LEASE_PREFIX}{run_id}");
let existing = facade
.with_store(|m| m.meta_get(&key))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
let (expected, next_fence) = match &existing {
Some(raw) => {
let row: LeaseRow = serde_json::from_str(raw).unwrap_or_default();
if row.until_ms > now_ms && row.holder != holder {
return Err(RunError::LeaseLost { run_id: run_id.to_string() });
}
(Some(raw.clone()), row.fence.wrapping_add(1))
}
None => (None, 1),
};
let row = LeaseRow {
holder: holder.to_string(),
fence: next_fence,
until_ms: now_ms + lease_ms,
};
let json = serde_json::to_string(&row).unwrap_or_default();
let won = facade
.with_store(|m| m.meta_cas(&key, expected.as_deref(), &json))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
if !won {
return Err(RunError::LeaseLost { run_id: run_id.to_string() });
}
Ok(RunLease { key, holder: holder.to_string(), seen: json, fence: next_fence, lease_ms })
}
pub fn renew(&mut self, facade: &AreevFacade, run_id: &str, now_ms: i64) -> Result<(), RunError> {
let row = LeaseRow {
holder: self.holder.clone(),
fence: self.fence,
until_ms: now_ms + self.lease_ms,
};
let json = serde_json::to_string(&row).unwrap_or_default();
let won = facade
.with_store(|m| m.meta_cas(&self.key, Some(&self.seen), &json))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
if !won {
return Err(RunError::LeaseLost { run_id: run_id.to_string() });
}
self.seen = json;
Ok(())
}
pub fn release(self, facade: &AreevFacade) {
let _ = facade.with_store(|m| m.meta_delete(&self.key));
}
}
#[cfg(test)]
mod tests {
use super::*;
use areev_store::Areev;
fn facade(dir: &tempfile::TempDir, name: &str) -> AreevFacade {
AreevFacade::new(Areev::open(dir.path().join(name).to_str().unwrap()).unwrap())
}
#[test]
fn one_holder_at_a_time() {
let dir = tempfile::TempDir::new().unwrap();
let f = facade(&dir, "a.db");
let _a = RunLease::acquire(&f, "r1", "A", 1_000, 60_000).unwrap();
let b = RunLease::acquire(&f, "r1", "B", 1_000, 60_000);
assert!(matches!(b, Err(RunError::LeaseLost { .. })));
}
#[test]
fn re_entering_our_own_lease_is_fine() {
let dir = tempfile::TempDir::new().unwrap();
let f = facade(&dir, "b.db");
let _a = RunLease::acquire(&f, "r1", "A", 1_000, 60_000).unwrap();
assert!(RunLease::acquire(&f, "r1", "A", 2_000, 60_000).is_ok());
}
#[test]
fn an_expired_lease_can_be_taken_over() {
let dir = tempfile::TempDir::new().unwrap();
let f = facade(&dir, "c.db");
let _a = RunLease::acquire(&f, "r1", "A", 1_000, 10).unwrap();
assert!(RunLease::acquire(&f, "r1", "B", 1_000_000, 60_000).is_ok());
}
#[test]
fn a_stalled_holder_cannot_renew_after_being_replaced() {
let dir = tempfile::TempDir::new().unwrap();
let f = facade(&dir, "d.db");
let mut a = RunLease::acquire(&f, "r1", "A", 1_000, 10).unwrap();
let _b = RunLease::acquire(&f, "r1", "B", 1_000_000, 60_000).unwrap();
let err = a.renew(&f, "r1", 1_000_001).unwrap_err();
assert_eq!(err.code(), "RUN-E021");
assert!(err.to_string().starts_with("RUN-E021"));
}
#[test]
fn renewing_extends_the_deadline() {
let dir = tempfile::TempDir::new().unwrap();
let f = facade(&dir, "e.db");
let mut a = RunLease::acquire(&f, "r1", "A", 1_000, 5_000).unwrap();
a.renew(&f, "r1", 4_000).unwrap();
assert!(RunLease::acquire(&f, "r1", "B", 6_500, 5_000).is_err());
}
#[test]
fn releasing_lets_the_next_driver_start_at_once() {
let dir = tempfile::TempDir::new().unwrap();
let f = facade(&dir, "f.db");
let a = RunLease::acquire(&f, "r1", "A", 1_000, 600_000).unwrap();
a.release(&f);
assert!(RunLease::acquire(&f, "r1", "B", 1_100, 600_000).is_ok());
}
}