#![allow(async_fn_in_trait)]
use core::future::Future;
use crate::lifecycle::{self, State};
use crate::{Claim, Retainer, Timestamp, Transition};
pub trait AsyncRegistry: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static + From<lifecycle::NotCurrentHolder>;
async fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error>;
fn lease_millis(&self) -> u64;
async fn apply(
&self,
retainer: &Retainer,
transition: &Transition<'_>,
) -> Result<(), Self::Error>;
async fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error> {
let lease = self.lease_millis();
self.apply(retainer, &|state| {
lifecycle::on_heartbeat(state, retainer, now, lease)
})
.await
}
async fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error> {
self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
.await
}
async fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error> {
self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
.await
}
async fn release(
&self,
retainer: &Retainer,
reclaimable_at: Timestamp,
) -> Result<(), Self::Error> {
self.apply(retainer, &|state| {
lifecycle::on_release(state, retainer, reclaimable_at)
})
.await
}
}
pub async fn apply_via_cas<E, L, C, LFut, CFut>(
load: L,
cas: C,
transition: &Transition<'_>,
) -> Result<(), E>
where
E: From<lifecycle::NotCurrentHolder>,
L: Fn() -> LFut,
C: Fn(State, State) -> CFut,
LFut: Future<Output = Result<Option<State>, E>>,
CFut: Future<Output = Result<bool, E>>,
{
loop {
let current = load().await?.ok_or(lifecycle::NotCurrentHolder)?;
let next = transition(¤t)?;
if cas(current, next).await? {
return Ok(());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Pact;
use std::sync::Mutex;
use uuid::Uuid;
struct MemAsync {
records: Mutex<Vec<(Pact, State)>>,
lease_millis: u64,
}
#[derive(Debug, PartialEq, Eq)]
struct NotHeld;
impl std::fmt::Display for NotHeld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not the current holder")
}
}
impl std::error::Error for NotHeld {}
impl From<lifecycle::NotCurrentHolder> for NotHeld {
fn from(_: lifecycle::NotCurrentHolder) -> Self {
NotHeld
}
}
impl MemAsync {
fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
Self {
records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
lease_millis,
}
}
}
impl AsyncRegistry for MemAsync {
type Error = NotHeld;
async fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
let mut records = self.records.lock().unwrap();
let index = records.iter().position(|(pact, state)| {
dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
});
let Some(index) = index else { return Ok(None) };
let retainer = Retainer::new(Uuid::new_v4());
records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
let expiry = lifecycle::lease_expiry(now, self.lease_millis);
Ok(Some(Claim::new(records[index].0.clone(), retainer, expiry)))
}
fn lease_millis(&self) -> u64 {
self.lease_millis
}
async fn apply(
&self,
_retainer: &Retainer,
transition: &Transition<'_>,
) -> Result<(), NotHeld> {
let mut records = self.records.lock().unwrap();
for (_, state) in records.iter_mut() {
if let Ok(next) = transition(state) {
*state = next;
return Ok(());
}
}
Err(NotHeld)
}
}
fn a_pact() -> Pact {
Pact::new(Uuid::new_v4(), "d".to_string(), "k".to_string(), Vec::new())
}
#[tokio::test]
async fn claim_then_fulfill_round_trips_via_defaults() {
let reg = MemAsync::seeded(vec![a_pact()], 1000);
let claim = reg
.claim(&["d"], Timestamp::from_millis(0))
.await
.unwrap()
.expect("a pact is claimable");
reg.fulfill(&claim.retainer).await.expect("fulfill settles");
assert!(
reg.claim(&["d"], Timestamp::from_millis(0))
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn heartbeat_refreshes_and_a_stranger_is_rejected() {
let reg = MemAsync::seeded(vec![a_pact()], 1000);
let claim = reg
.claim(&["d"], Timestamp::from_millis(0))
.await
.unwrap()
.unwrap();
reg.heartbeat(&claim.retainer, Timestamp::from_millis(500))
.await
.expect("live lease refreshes");
let stranger = Retainer::new(Uuid::new_v4());
assert_eq!(
reg.heartbeat(&stranger, Timestamp::from_millis(600)).await,
Err(NotHeld)
);
}
#[tokio::test]
async fn release_then_reclaim_and_the_prior_holder_is_rejected() {
let reg = MemAsync::seeded(vec![a_pact()], 1000);
let first = reg
.claim(&["d"], Timestamp::from_millis(0))
.await
.unwrap()
.unwrap();
reg.release(&first.retainer, Timestamp::from_millis(0))
.await
.expect("release");
let second = reg
.claim(&["d"], Timestamp::from_millis(10))
.await
.unwrap()
.expect("reclaimable after release");
assert_eq!(reg.fulfill(&first.retainer).await, Err(NotHeld));
reg.fulfill(&second.retainer)
.await
.expect("new holder settles");
}
#[tokio::test]
async fn apply_via_cas_applies_once_and_rejects_a_stranger() {
let cell = Mutex::new(Some(State::Held {
retainer: Retainer::new(Uuid::new_v4()),
expiry: Timestamp::from_millis(1000),
}));
let holder = match cell.lock().unwrap().clone().unwrap() {
State::Held { retainer, .. } => retainer,
_ => unreachable!(),
};
let cell_ref = &cell;
let load = || async move { Ok::<_, NotHeld>(cell_ref.lock().unwrap().clone()) };
let cas = |expected: State, next: State| async move {
let mut slot = cell_ref.lock().unwrap();
if slot.as_ref() == Some(&expected) {
*slot = Some(next);
Ok::<_, NotHeld>(true)
} else {
Ok(false)
}
};
apply_via_cas(load, cas, &|s| lifecycle::on_settle(s, &holder))
.await
.expect("holder settles");
assert_eq!(*cell.lock().unwrap(), Some(State::Settled));
let stranger = Retainer::new(Uuid::new_v4());
assert_eq!(
apply_via_cas(load, cas, &|s| lifecycle::on_settle(s, &stranger)).await,
Err(NotHeld)
);
}
}