use std::sync::{Mutex, MutexGuard, PoisonError};
use tokio::sync::Notify;
use tokio::sync::futures::Notified;
use super::{LeaseGone, Slot};
pub(crate) struct Leases<Id, Token> {
slots: Mutex<Slots<Id, Token>>,
extended: Notify,
}
struct Slots<Id, Token> {
entries: Vec<Entry<Id, Token>>,
free: Vec<usize>,
}
struct Entry<Id, Token> {
id: Option<Id>,
standing: Standing<Token>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Standing<Token> {
Free,
Held(Token),
Extending {
held: Token,
next: Token,
awaits: Awaits,
},
Settling(Token),
Lost,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Awaits {
Keeper,
Settlement,
Nobody,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Extended {
Written,
Lost,
Failed,
}
pub(crate) struct Round<Id, Token> {
targets: Vec<Target<Id, Token>>,
marked: usize,
}
pub(super) struct Target<Id, Token> {
pub(super) slot: usize,
pub(super) id: Id,
pub(super) held: Token,
pub(super) outcome: Option<Extended>,
}
pub(super) struct Resolving<'a, Id, Token: Copy> {
pub(super) leases: &'a Leases<Id, Token>,
pub(super) round: &'a mut Round<Id, Token>,
}
impl<Id, Token: Copy> Drop for Resolving<'_, Id, Token> {
fn drop(&mut self) {
self.leases.resolve(self.round);
}
}
struct Waiting<'a, Id, Token> {
leases: &'a Leases<Id, Token>,
slot: Option<usize>,
}
impl<Id, Token> Drop for Waiting<'_, Id, Token> {
fn drop(&mut self) {
let Some(index) = self.slot.take() else {
return;
};
let mut slots = self.leases.slots();
if !matches!(slots.entries[index].standing, Standing::Free) {
slots.release(index);
}
}
}
enum Found<'a, Token> {
Done(Result<Token, LeaseGone>),
Wait(Notified<'a>),
}
impl<Id, Token> Default for Leases<Id, Token> {
fn default() -> Self {
Self {
slots: Mutex::new(Slots {
entries: Vec::new(),
free: Vec::new(),
}),
extended: Notify::new(),
}
}
}
impl<Id, Token> Default for Round<Id, Token> {
fn default() -> Self {
Self {
targets: Vec::new(),
marked: 0,
}
}
}
impl<Id, Token> Round<Id, Token> {
pub(super) fn marked_mut(&mut self) -> &mut [Target<Id, Token>] {
&mut self.targets[..self.marked]
}
}
impl<Id, Token> Slots<Id, Token> {
fn release(&mut self, index: usize) {
self.entries[index].standing = Standing::Free;
self.free.push(index);
}
}
impl<Id, Token> Leases<Id, Token> {
fn slots(&self) -> MutexGuard<'_, Slots<Id, Token>> {
self.slots.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl<Id: Clone, Token: Copy> Leases<Id, Token> {
pub(super) fn enter(&self, id: &Id, lease: Token) -> Slot {
let mut slots = self.slots();
let Some(index) = slots.free.pop() else {
slots.entries.push(Entry {
id: Some(id.clone()),
standing: Standing::Held(lease),
});
let index = slots.entries.len() - 1;
drop(slots);
return Slot(index);
};
let entry = &mut slots.entries[index];
match &mut entry.id {
Some(kept) => kept.clone_from(id),
empty => *empty = Some(id.clone()),
}
entry.standing = Standing::Held(lease);
drop(slots);
Slot(index)
}
pub(super) async fn settling(&self, slot: Slot) -> Result<Token, LeaseGone> {
let Slot(index) = slot;
let mut waiting = Waiting {
leases: self,
slot: Some(index),
};
loop {
match self.find(index) {
Found::Done(lease) => {
waiting.slot = None;
return lease;
}
Found::Wait(extended) => extended.await,
}
}
}
fn find(&self, index: usize) -> Found<'_, Token> {
let mut slots = self.slots();
let found = match slots.entries[index].standing {
Standing::Held(lease) | Standing::Settling(lease) => {
slots.release(index);
Found::Done(Ok(lease))
}
Standing::Lost => {
slots.release(index);
Found::Done(Err(LeaseGone))
}
Standing::Extending { held, next, .. } => {
slots.entries[index].standing = Standing::Extending {
held,
next,
awaits: Awaits::Settlement,
};
Found::Wait(self.extended.notified())
}
Standing::Free => Found::Done(Err(LeaseGone)),
};
drop(slots);
found
}
pub(super) fn take_ahead(&self, slot: Slot) -> Result<Token, LeaseGone> {
let index = slot.into_index();
let mut slots = self.slots();
let taken = match slots.entries[index].standing {
Standing::Held(lease) | Standing::Settling(lease) => {
slots.release(index);
Ok(lease)
}
Standing::Lost => {
slots.release(index);
Err(LeaseGone)
}
Standing::Extending { held, next, .. } => {
slots.entries[index].standing = Standing::Extending {
held,
next,
awaits: Awaits::Nobody,
};
Ok(held)
}
Standing::Free => Err(LeaseGone),
};
drop(slots);
taken
}
pub(super) fn taken_ahead(&self, index: usize) -> bool {
matches!(
self.slots().entries[index].standing,
Standing::Extending {
awaits: Awaits::Nobody,
..
}
)
}
pub(super) fn any_held(&self) -> bool {
self.slots()
.entries
.iter()
.any(|entry| matches!(entry.standing, Standing::Held(_)))
}
pub(super) fn mark(&self, next: Token, round: &mut Round<Id, Token>) -> usize {
round.marked = 0;
let mut slots = self.slots();
for (slot, entry) in slots.entries.iter_mut().enumerate() {
let (Standing::Held(held), Some(id)) = (entry.standing, &entry.id) else {
continue;
};
entry.standing = Standing::Extending {
held,
next,
awaits: Awaits::Keeper,
};
match round.targets.get_mut(round.marked) {
Some(target) => {
target.slot = slot;
target.id.clone_from(id);
target.held = held;
target.outcome = None;
}
None => round.targets.push(Target {
slot,
id: id.clone(),
held,
outcome: None,
}),
}
round.marked += 1;
}
drop(slots);
round.marked
}
}
impl<Id, Token: Copy> Leases<Id, Token> {
fn resolve(&self, round: &Round<Id, Token>) {
let mut slots = self.slots();
for target in &round.targets[..round.marked] {
let Standing::Extending { held, next, awaits } = slots.entries[target.slot].standing
else {
continue;
};
let standing = match (target.outcome, awaits) {
(_, Awaits::Nobody) => Standing::Free,
(Some(Extended::Lost), _) => Standing::Lost,
(Some(Extended::Written), Awaits::Keeper) => Standing::Held(next),
(Some(Extended::Written), Awaits::Settlement) => Standing::Settling(next),
(Some(Extended::Failed) | None, Awaits::Keeper) => Standing::Held(held),
(Some(Extended::Failed) | None, Awaits::Settlement) => Standing::Settling(held),
};
if matches!(standing, Standing::Free) {
slots.release(target.slot);
} else {
slots.entries[target.slot].standing = standing;
}
}
drop(slots);
self.extended.notify_waiters();
}
}
#[cfg(test)]
mod tests {
use std::pin::pin;
use futures::poll;
use super::{Extended, LeaseGone, Leases, Round, Standing};
type Book = Leases<String, u32>;
fn standing(book: &Book, slot: usize) -> Standing<u32> {
book.slots().entries[slot].standing
}
fn resolve(book: &Book, round: &mut Round<String, u32>, outcome: Extended) {
for target in round.marked_mut() {
target.outcome = Some(outcome);
}
book.resolve(round);
}
#[tokio::test]
async fn an_extension_racing_a_settlement_never_loses_the_lease() {
let book = Book::default();
let slot = book.enter(&"job-1".to_owned(), 10);
let mut round = Round::default();
assert_eq!(book.mark(20, &mut round), 1);
let mut settling = pin!(book.settling(slot));
assert!(
poll!(settling.as_mut()).is_pending(),
"the settlement waits while its lease is extended"
);
resolve(&book, &mut round, Extended::Written);
assert_eq!(
settling.await,
Ok(20),
"the settlement holds the lease the extension wrote, not the one it replaced"
);
assert_eq!(standing(&book, 0), Standing::Free);
}
#[tokio::test]
async fn a_settlement_that_waited_for_a_lost_lease_learns_it_is_gone() {
let book = Book::default();
let lost = book.enter(&"job-1".to_owned(), 10);
let mut round = Round::default();
book.mark(20, &mut round);
let mut settling = pin!(book.settling(lost));
assert!(poll!(settling.as_mut()).is_pending());
resolve(&book, &mut round, Extended::Lost);
assert_eq!(settling.await, Err(LeaseGone));
let failed = book.enter(&"job-2".to_owned(), 30);
book.mark(40, &mut round);
let mut settling = pin!(book.settling(failed));
assert!(poll!(settling.as_mut()).is_pending());
resolve(&book, &mut round, Extended::Failed);
assert_eq!(settling.await, Ok(30));
}
#[tokio::test]
async fn a_lease_lost_between_settlements_is_gone_for_the_next_one() {
let book = Book::default();
let slot = book.enter(&"job-1".to_owned(), 10);
let mut round = Round::default();
book.mark(20, &mut round);
resolve(&book, &mut round, Extended::Lost);
assert_eq!(standing(&book, 0), Standing::Lost);
assert!(!book.any_held(), "the keeper extends a lost lease no more");
assert_eq!(book.settling(slot).await, Err(LeaseGone));
assert_eq!(standing(&book, 0), Standing::Free);
}
#[tokio::test]
async fn a_settling_slot_is_skipped_by_the_next_round() {
let book = Book::default();
let settled = book.enter(&"job-1".to_owned(), 10);
let kept = book.enter(&"job-2".to_owned(), 10);
let mut round = Round::default();
assert_eq!(book.mark(20, &mut round), 2);
let mut settling = pin!(book.settling(settled));
assert!(poll!(settling.as_mut()).is_pending());
resolve(&book, &mut round, Extended::Written);
assert_eq!(standing(&book, 0), Standing::Settling(20));
assert_eq!(standing(&book, 1), Standing::Held(20));
assert_eq!(book.mark(30, &mut round), 1);
assert_eq!(round.marked_mut()[0].slot, 1);
assert_eq!(standing(&book, 0), Standing::Settling(20));
assert_eq!(settling.await, Ok(20));
resolve(&book, &mut round, Extended::Written);
assert_eq!(book.settling(kept).await, Ok(30));
assert!(!book.any_held());
assert_eq!(book.mark(40, &mut round), 0);
}
#[tokio::test]
async fn a_settlement_dropped_while_it_waits_frees_its_slot() {
let book = Book::default();
let slot = book.enter(&"job-1".to_owned(), 10);
let mut round = Round::default();
book.mark(20, &mut round);
{
let mut settling = pin!(book.settling(slot));
assert!(poll!(settling.as_mut()).is_pending());
}
assert_eq!(standing(&book, 0), Standing::Free);
let next = book.enter(&"job-2".to_owned(), 15);
resolve(&book, &mut round, Extended::Written);
assert_eq!(standing(&book, 0), Standing::Held(15));
assert_eq!(book.settling(next).await, Ok(15));
}
#[tokio::test]
async fn enter_after_leave_reuses_the_slot_and_its_id_allocation() {
let book = Book::default();
let first = book.enter(&"job-0001".to_owned(), 10);
let storage = book.slots().entries[0].id.as_deref().map(str::as_ptr);
assert_eq!(book.settling(first).await, Ok(10));
let second = book.enter(&"job-0002".to_owned(), 20);
{
let slots = book.slots();
assert_eq!(slots.entries.len(), 1, "the delivery took the free slot");
assert_eq!(slots.entries[0].id.as_deref(), Some("job-0002"));
assert_eq!(
slots.entries[0].id.as_deref().map(str::as_ptr),
storage,
"the id was copied into the storage the last one left"
);
}
assert_eq!(book.settling(second).await, Ok(20));
}
#[tokio::test]
async fn a_round_copies_each_id_into_the_storage_the_round_before_left() {
let book = Book::default();
let first = book.enter(&"job-0001".to_owned(), 10);
let mut round = Round::default();
book.mark(20, &mut round);
let storage = round.targets[0].id.as_ptr();
resolve(&book, &mut round, Extended::Written);
assert_eq!(book.settling(first).await, Ok(20));
let second = book.enter(&"job-0002".to_owned(), 30);
assert_eq!(book.mark(40, &mut round), 1);
assert_eq!(round.targets[0].id, "job-0002");
assert_eq!(round.targets[0].id.as_ptr(), storage);
resolve(&book, &mut round, Extended::Written);
assert_eq!(book.settling(second).await, Ok(40));
}
#[tokio::test]
async fn an_acknowledgement_takes_the_lease_ahead_of_its_extension() {
let book = Book::default();
let slot = book.enter(&"job-1".to_owned(), 10);
let mut round = Round::default();
assert_eq!(book.mark(20, &mut round), 1);
assert_eq!(
book.take_ahead(slot),
Ok(10),
"the acknowledgement holds the lease the delivery held before the extension"
);
assert!(
book.taken_ahead(0),
"the round skips the extension it has not run"
);
resolve(&book, &mut round, Extended::Written);
assert_eq!(
standing(&book, 0),
Standing::Free,
"the round frees the slot"
);
let next = book.enter(&"job-2".to_owned(), 30);
assert!(
book.slots().free.is_empty(),
"the next delivery took the freed slot"
);
assert_eq!(book.take_ahead(next), Ok(30));
}
#[tokio::test]
async fn a_lease_taken_ahead_with_no_extension_in_flight_leaves_at_once() {
let book = Book::default();
let held = book.enter(&"job-1".to_owned(), 10);
assert_eq!(book.take_ahead(held), Ok(10));
assert_eq!(standing(&book, 0), Standing::Free);
let lost = book.enter(&"job-2".to_owned(), 10);
let mut round = Round::default();
book.mark(20, &mut round);
resolve(&book, &mut round, Extended::Lost);
assert_eq!(
book.take_ahead(lost),
Err(LeaseGone),
"the keeper found the row gone"
);
assert_eq!(standing(&book, 0), Standing::Free);
}
}