use corium_store::{RootStore, StoreError};
use thiserror::Error;
#[must_use]
pub fn lease_root(db: &str) -> String {
format!("lease:{db}")
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Lease {
pub owner: String,
pub version: u64,
pub expires_unix_ms: i64,
}
impl Lease {
#[must_use]
pub fn encode(&self) -> Vec<u8> {
format!(
"{}\n{}\n{}\n",
self.owner, self.version, self.expires_unix_ms
)
.into_bytes()
}
#[must_use]
pub fn decode(bytes: &[u8]) -> Option<Self> {
let text = std::str::from_utf8(bytes).ok()?;
let mut lines = text.lines();
Some(Self {
owner: lines.next()?.to_owned(),
version: lines.next()?.parse().ok()?,
expires_unix_ms: lines.next()?.parse().ok()?,
})
}
}
#[derive(Debug, Error)]
pub enum LeaseError {
#[error("lease held by {owner} until {expires_unix_ms}")]
Held {
owner: String,
expires_unix_ms: i64,
},
#[error("lease lost to another owner")]
Lost,
#[error(transparent)]
Store(#[from] StoreError),
}
pub fn acquire(
store: &dyn RootStore,
db: &str,
owner: &str,
ttl_ms: i64,
now_unix_ms: i64,
) -> Result<Lease, LeaseError> {
let name = lease_root(db);
loop {
let current = store.get_root(&name)?;
let decoded = current.as_deref().and_then(Lease::decode);
let version = match &decoded {
Some(lease) if lease.owner == owner => lease.version,
Some(lease) if lease.expires_unix_ms > now_unix_ms => {
return Err(LeaseError::Held {
owner: lease.owner.clone(),
expires_unix_ms: lease.expires_unix_ms,
});
}
Some(lease) => lease.version + 1,
None => 1,
};
let next = Lease {
owner: owner.to_owned(),
version,
expires_unix_ms: now_unix_ms + ttl_ms,
};
match store.cas_root(&name, current.as_deref(), &next.encode()) {
Ok(()) => return Ok(next),
Err(StoreError::CasFailed { .. }) => {}
Err(error) => return Err(error.into()),
}
}
}
pub fn renew(
store: &dyn RootStore,
db: &str,
held: &Lease,
ttl_ms: i64,
now_unix_ms: i64,
) -> Result<Lease, LeaseError> {
let name = lease_root(db);
let current = store.get_root(&name)?;
if current.as_deref() != Some(held.encode().as_slice()) {
return Err(LeaseError::Lost);
}
let next = Lease {
owner: held.owner.clone(),
version: held.version,
expires_unix_ms: now_unix_ms + ttl_ms,
};
match store.cas_root(&name, current.as_deref(), &next.encode()) {
Ok(()) => Ok(next),
Err(StoreError::CasFailed { .. }) => Err(LeaseError::Lost),
Err(error) => Err(error.into()),
}
}
pub fn release(store: &dyn RootStore, db: &str, held: &Lease) -> Result<(), LeaseError> {
let name = lease_root(db);
let current = store.get_root(&name)?;
if current.as_deref() != Some(held.encode().as_slice()) {
return Err(LeaseError::Lost);
}
let expired = Lease {
expires_unix_ms: 0,
..held.clone()
};
match store.cas_root(&name, current.as_deref(), &expired.encode()) {
Ok(()) => Ok(()),
Err(StoreError::CasFailed { .. }) => Err(LeaseError::Lost),
Err(error) => Err(error.into()),
}
}