minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use thiserror::Error;

use crate::metis::VersionVector;

/// The witness [`Stability::abandon`](super::Stability::abandon) hands back:
/// a station has left the meet's family, and this is what of its history the
/// survivors kept.
///
/// Everything the station minted above [`bound`](Self::bound) is written
/// off. That tail is unknowable in principle, so it is accepted loudly and
/// never certified; this type is where the loudness lives
/// (`docs/metis-membership-departure.adoc`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Abandoned {
    station: u32,
    bound: VersionVector,
}

impl Abandoned {
    pub(super) const fn new(station: u32, bound: VersionVector) -> Self {
        Self { station, bound }
    }

    /// The station that left the family.
    #[must_use]
    pub const fn station(&self) -> u32 {
        self.station
    }

    /// The abandonment bound: the surviving members' join, restricted to
    /// the departing station's coordinate.
    ///
    /// A lower bound on what survives, so it over-states the loss; a read
    /// rather than a payload, so honest members may disagree about it and
    /// nothing in the protocol may consume it.
    #[must_use]
    pub const fn bound(&self) -> &VersionVector {
        &self.bound
    }

    /// The bound's counter: the number an operator reads.
    #[must_use]
    pub fn counter(&self) -> u64 {
        self.bound.get(self.station)
    }
}

/// The rejected outcome of [`Stability::abandon`](super::Stability::abandon):
/// the tracker is unchanged.
///
/// Both variants guard the same invariant: the family
/// a meet ranges over stays a family.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum AbandonRefusal {
    /// Off the roster: abandoning a non-member would install the belief
    /// that it was one.
    #[error("cannot abandon station {station}: not on the roster")]
    UnknownStation {
        /// The station named.
        station: u32,
    },
    /// The last surviving member. An empty family's meet is the lattice top
    /// ("everything is stable", vacuously), the fabrication
    /// [`watermark`](super::Stability::watermark) refuses to invent.
    #[error("cannot abandon station {station}: it is the last surviving member")]
    LastSurvivor {
        /// The station named.
        station: u32,
    },
    /// The attestation's round did not range over every member this tracker
    /// still meets over.
    ///
    /// A round over a smaller family agrees a bound the members outside it
    /// never fenced at, so consuming it here would install a coordinate this
    /// tracker's own survivors never promised anything about. A *larger*
    /// family is not an error: the condition is coverage, so an attestation
    /// survives a later departure rather than being invalidated by it. Only
    /// [`abandon_attested`](super::Stability::abandon_attested) raises this;
    /// the family it wanted is
    /// [`Departed::family`](super::Departed::family).
    #[error("station {station}'s attestation was agreed over another family")]
    FamilyMismatch {
        /// The station named.
        station: u32,
    },
    /// Departing would leave an earlier attested bound unservable.
    ///
    /// An attestation promises that every dot at or below its bound is held
    /// by *some* survivor, which is what lets the survivors complete each
    /// other through the repair lane. Narrowing past the last member
    /// reporting that high strands it: the meet at the departed coordinate
    /// can never reach the bound a sealed record names, and the holder is
    /// gone.
    ///
    /// The condition is about who *remains*: some surviving member must
    /// report through the bound after the narrowing. It is deliberately not
    /// conditioned on what the *leaver* reported, because a report is a
    /// durable floor and lags what a member holds --- "the leaver had not
    /// reported that high" proves nothing about whether it was the only
    /// holder.
    ///
    /// Conservative, therefore, and it clears as reports rise: this refuses
    /// some departures that would in fact have been safe. That is the
    /// direction to be wrong in, since the alternative is a fleet that can
    /// never seal again.
    #[error(
        "cannot abandon station {station}: it is the last server of station {stranded}'s attested bound"
    )]
    StrandsAttestation {
        /// The station named.
        station: u32,
        /// The attested station whose bound would be stranded.
        stranded: u32,
    },
    /// A second attestation for a station already attested, at a different
    /// bound. The agreed coordinate is what peers seal over, so it is
    /// latched: only
    /// [`abandon_attested`](super::Stability::abandon_attested) raises this.
    #[error("station {station} is already attested at {held}, not {offered}")]
    Reattested {
        /// The station named.
        station: u32,
        /// The bound already recorded.
        held: u64,
        /// The bound offered.
        offered: u64,
    },
}