use crate::adapter::net::behavior::fold::{
ApplyOutcome, EnvelopeMeta, Fold, FoldError, FoldKind, IslandId, JobId, NodeId,
ReservationAnnouncement, ReservationFold, ReservationQuery, ReservationState,
SignedAnnouncement, WireError,
};
use crate::adapter::net::identity::EntityKeypair;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClaimOutcome {
Won,
Lost,
}
impl ClaimOutcome {
fn from_apply(outcome: ApplyOutcome) -> Self {
match outcome {
ApplyOutcome::Inserted | ApplyOutcome::Replaced => ClaimOutcome::Won,
ApplyOutcome::Rejected => ClaimOutcome::Lost,
}
}
}
#[derive(Debug)]
pub enum ClaimError {
Sign(WireError),
Apply(FoldError),
}
impl std::fmt::Display for ClaimError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClaimError::Sign(e) => write!(f, "sign reservation announcement: {e}"),
ClaimError::Apply(e) => write!(f, "apply reservation announcement: {e}"),
}
}
}
impl std::error::Error for ClaimError {}
impl From<WireError> for ClaimError {
fn from(e: WireError) -> Self {
ClaimError::Sign(e)
}
}
impl From<FoldError> for ClaimError {
fn from(e: FoldError) -> Self {
ClaimError::Apply(e)
}
}
pub fn reserve_announcement(
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
until_unix_us: u64,
) -> Result<SignedAnnouncement<ReservationAnnouncement>, WireError> {
sign_state(
keypair,
node_id,
generation,
island,
ReservationState::Reserved {
holder: node_id,
until_unix_us,
},
)
}
pub fn activate_announcement(
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
job_id: JobId,
) -> Result<SignedAnnouncement<ReservationAnnouncement>, WireError> {
sign_state(
keypair,
node_id,
generation,
island,
ReservationState::Active {
holder: node_id,
job_id,
},
)
}
pub fn release_announcement(
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
) -> Result<SignedAnnouncement<ReservationAnnouncement>, WireError> {
sign_state(keypair, node_id, generation, island, ReservationState::Free)
}
fn sign_state(
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
state: ReservationState,
) -> Result<SignedAnnouncement<ReservationAnnouncement>, WireError> {
SignedAnnouncement::sign(
keypair,
ReservationFold::KIND_ID,
0, node_id,
generation,
EnvelopeMeta::default(),
ReservationAnnouncement {
resource_id: island,
state,
},
)
}
pub fn single_island_claim(
reservations: &Fold<ReservationFold>,
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
until_unix_us: u64,
) -> Result<ClaimOutcome, ClaimError> {
let ann = reserve_announcement(keypair, node_id, generation, island, until_unix_us)?;
Ok(ClaimOutcome::from_apply(reservations.apply(ann)?))
}
pub fn activate_island(
reservations: &Fold<ReservationFold>,
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
job_id: JobId,
) -> Result<ClaimOutcome, ClaimError> {
let ann = activate_announcement(keypair, node_id, generation, island, job_id)?;
Ok(ClaimOutcome::from_apply(reservations.apply(ann)?))
}
pub fn release_island(
reservations: &Fold<ReservationFold>,
keypair: &EntityKeypair,
node_id: NodeId,
generation: u64,
island: IslandId,
) -> Result<ClaimOutcome, ClaimError> {
let held_by_us = reservations
.query(ReservationQuery::State(island))
.first()
.and_then(|(_, state)| state.holder())
== Some(node_id);
if !held_by_us {
return Ok(ClaimOutcome::Lost);
}
let ann = release_announcement(keypair, node_id, generation, island)?;
Ok(ClaimOutcome::from_apply(reservations.apply(ann)?))
}
pub struct Claimant<'a> {
pub(super) reservations: &'a Fold<ReservationFold>,
pub(super) keypair: &'a EntityKeypair,
pub(super) node_id: NodeId,
pub(super) generation: u64,
}
impl<'a> Claimant<'a> {
pub fn new(
reservations: &'a Fold<ReservationFold>,
keypair: &'a EntityKeypair,
node_id: NodeId,
) -> Self {
Self::with_generation(reservations, keypair, node_id, 1)
}
pub fn with_generation(
reservations: &'a Fold<ReservationFold>,
keypair: &'a EntityKeypair,
node_id: NodeId,
generation: u64,
) -> Self {
Self {
reservations,
keypair,
node_id,
generation,
}
}
pub(crate) fn next_gen(&mut self) -> u64 {
let g = self.generation;
self.generation = self.generation.saturating_add(1);
g
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::adapter::net::behavior::fold::{Fold, ReservationQuery, ReservationState};
use crate::adapter::net::current_timestamp_micros;
use crate::adapter::net::identity::EntityKeypair;
fn new_reservations() -> Fold<ReservationFold> {
Fold::with_sweep_interval(Duration::ZERO)
}
fn fresh_deadline() -> u64 {
current_timestamp_micros() + 60_000_000
}
#[test]
fn single_island_claim_wins_an_unheld_island() {
let fold = new_reservations();
let kp = EntityKeypair::generate();
let node = kp.entity_id().node_id();
let got = single_island_claim(&fold, &kp, node, 1, 0x10, fresh_deadline()).unwrap();
assert_eq!(got, ClaimOutcome::Won);
let state = fold.query(ReservationQuery::State(0x10));
assert_eq!(state[0].1.holder(), Some(node));
}
#[test]
fn second_claimant_loses_a_held_island() {
let fold = new_reservations();
let a = EntityKeypair::generate();
let b = EntityKeypair::generate();
let (na, nb) = (a.entity_id().node_id(), b.entity_id().node_id());
assert_eq!(
single_island_claim(&fold, &a, na, 1, 0x10, fresh_deadline()).unwrap(),
ClaimOutcome::Won,
);
assert_eq!(
single_island_claim(&fold, &b, nb, 1, 0x10, fresh_deadline()).unwrap(),
ClaimOutcome::Lost,
);
assert_eq!(
fold.query(ReservationQuery::State(0x10))[0].1.holder(),
Some(na),
);
}
#[test]
fn full_lifecycle_reserve_activate_release() {
let fold = new_reservations();
let kp = EntityKeypair::generate();
let node = kp.entity_id().node_id();
assert_eq!(
single_island_claim(&fold, &kp, node, 1, 0x10, fresh_deadline()).unwrap(),
ClaimOutcome::Won,
);
assert_eq!(
activate_island(&fold, &kp, node, 2, 0x10, 0x7B).unwrap(),
ClaimOutcome::Won,
);
assert!(matches!(
fold.query(ReservationQuery::State(0x10))[0].1,
ReservationState::Active { job_id: 0x7B, .. }
));
assert_eq!(
release_island(&fold, &kp, node, 3, 0x10).unwrap(),
ClaimOutcome::Won,
);
assert_eq!(
fold.query(ReservationQuery::State(0x10))[0].1,
ReservationState::Free,
);
}
#[test]
fn foreign_release_is_rejected_as_lost() {
let fold = new_reservations();
let a = EntityKeypair::generate();
let b = EntityKeypair::generate();
let (na, nb) = (a.entity_id().node_id(), b.entity_id().node_id());
single_island_claim(&fold, &a, na, 1, 0x10, fresh_deadline()).unwrap();
assert_eq!(
release_island(&fold, &b, nb, 1, 0x10).unwrap(),
ClaimOutcome::Lost,
);
assert_eq!(
fold.query(ReservationQuery::State(0x10))[0].1.holder(),
Some(na),
);
}
#[test]
fn release_of_an_unheld_island_is_lost_not_won() {
let fold = new_reservations();
let kp = EntityKeypair::generate();
let node = kp.entity_id().node_id();
assert_eq!(
release_island(&fold, &kp, node, 1, 0x99).unwrap(),
ClaimOutcome::Lost,
);
assert!(
fold.query(ReservationQuery::State(0x99)).is_empty(),
"no spurious Free entry for an island we never held",
);
}
#[test]
fn claimant_with_generation_seeds_the_counter() {
let fold = new_reservations();
let kp = EntityKeypair::generate();
let node = kp.entity_id().node_id();
let mut c = Claimant::with_generation(&fold, &kp, node, 100);
assert_eq!(c.next_gen(), 100);
assert_eq!(c.next_gen(), 101);
}
}