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;
pub const MIN_RUN_LEASE_MS: i64 = 5_000;
pub fn default_node_id() -> String {
let host = std::env::var("HOSTNAME")
.or_else(|_| std::env::var("COMPUTERNAME"))
.unwrap_or_else(|_| "node".into());
format!("{host}/{}", std::process::id())
}
pub fn holder_for(principal: &str, node: Option<&str>) -> String {
let node = node
.map(str::to_string)
.unwrap_or_else(default_node_id);
format!("{principal}#{node}")
}
#[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 peek(
facade: &AreevFacade,
run_id: &str,
) -> Result<Option<(String, i64)>, RunError> {
let key = format!("{RUN_LEASE_PREFIX}{run_id}");
let raw = facade
.with_store(|m| m.meta_get(&key))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
Ok(raw.and_then(|raw| {
serde_json::from_str::<LeaseRow>(&raw)
.ok()
.map(|r| (r.holder, r.until_ms))
}))
}
pub fn lease_ms(&self) -> i64 {
self.lease_ms
}
pub fn release(self, facade: &AreevFacade) {
let _ = facade.with_store(|m| m.meta_delete(&self.key));
}
}
const RUN_SLOT_PREFIX: &str = "runslot:";
pub struct RunSlots {
held: Vec<(String, String)>,
lease_ms: i64,
}
impl RunSlots {
pub fn claim(
facade: &AreevFacade,
run_id: &str,
principal: &str,
now_ms: i64,
lease_ms: i64,
max_memory: Option<u32>,
max_principal: Option<u32>,
) -> Result<RunSlots, RunError> {
let mut slots = RunSlots { held: Vec::new(), lease_ms };
let scopes = [
(max_memory, "mem".to_string(), "this memory".to_string()),
(
max_principal,
format!("principal:{principal}"),
format!("principal '{principal}'"),
),
];
for (cap, scope, describe) in scopes {
let Some(cap) = cap else { continue };
match slots.claim_one(facade, run_id, &scope, now_ms, cap) {
Ok(()) => {}
Err(e) => {
slots.release(facade);
return Err(match e {
RunError::Storage { detail } => RunError::Storage { detail },
_ => RunError::ConcurrencyLimit {
scope: describe,
limit: cap,
},
});
}
}
}
Ok(slots)
}
fn claim_one(
&mut self,
facade: &AreevFacade,
run_id: &str,
scope: &str,
now_ms: i64,
cap: u32,
) -> Result<(), RunError> {
for k in 0..cap {
let key = format!("{RUN_SLOT_PREFIX}{scope}:{k}");
let existing = facade
.with_store(|m| m.meta_get(&key))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
if let Some(raw) = &existing {
let row: LeaseRow = serde_json::from_str(raw).unwrap_or_default();
if row.until_ms > now_ms && row.holder != run_id {
continue;
}
}
let row = LeaseRow {
holder: run_id.to_string(),
fence: 0,
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(&key, existing.as_deref(), &json))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
if won {
self.held.push((key, json));
return Ok(());
}
}
Err(RunError::ConcurrencyLimit { scope: scope.to_string(), limit: cap })
}
pub fn renew(&mut self, facade: &AreevFacade, run_id: &str, now_ms: i64) {
for (key, seen) in self.held.iter_mut() {
let row = LeaseRow {
holder: run_id.to_string(),
fence: 0,
until_ms: now_ms + self.lease_ms,
};
let json = serde_json::to_string(&row).unwrap_or_default();
if facade
.with_store(|m| m.meta_cas(key, Some(seen.as_str()), &json))
.unwrap_or(false)
{
*seen = json;
}
}
}
pub fn release(&mut self, facade: &AreevFacade) {
for (key, _) in std::mem::take(&mut self.held) {
let _ = facade.with_store(|m| m.meta_delete(&key));
}
}
pub fn occupancy(facade: &AreevFacade, now_ms: i64) -> Result<Vec<(String, String)>, RunError> {
let rows = facade
.with_store(|m| m.meta_scan(RUN_SLOT_PREFIX))
.map_err(|e| RunError::Storage { detail: e.to_string() })?;
Ok(rows
.into_iter()
.filter_map(|(k, raw)| {
let row: LeaseRow = serde_json::from_str(&raw).ok()?;
(row.until_ms > now_ms).then_some((k, row.holder))
})
.collect())
}
}
#[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());
}
}