use std::collections::{BTreeSet, HashMap};
use std::sync::{Mutex, MutexGuard, PoisonError};
use liminal_protocol::lifecycle::{CapacityCounter, CapacityCounterInvariantError};
use super::state::StateError;
pub(super) enum ScopeCounter {
Valid(CapacityCounter),
OverLimit {
limit: u64,
occupied: u64,
},
}
pub(super) fn scope_counter(limit: u64, occupied: u64) -> Result<ScopeCounter, StateError> {
match CapacityCounter::try_new(limit, occupied) {
Ok(counter) => Ok(ScopeCounter::Valid(counter)),
Err(CapacityCounterInvariantError::OccupiedExceedsLimit { occupied, limit }) => {
Ok(ScopeCounter::OverLimit { limit, occupied })
}
Err(CapacityCounterInvariantError::ZeroLimit) => Err(StateError::invariant(
"validated capacity configuration rejected: zero limit",
)),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum ResourceKind {
EnrollmentReceipt,
AttachReceipt,
EnrollmentProvenance,
AttachProvenance,
}
impl ResourceKind {
const fn is_live_receipt(self) -> bool {
matches!(self, Self::EnrollmentReceipt | Self::AttachReceipt)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct OccupancyEntry {
pub(super) expires_at: u128,
pub(super) conversation_id: u64,
pub(super) participant_id: u64,
pub(super) kind: ResourceKind,
pub(super) token: [u8; 16],
}
impl OccupancyEntry {
const fn floor(expires_at: u128) -> Self {
Self {
expires_at,
conversation_id: 0,
participant_id: 0,
kind: ResourceKind::EnrollmentReceipt,
token: [0; 16],
}
}
}
#[derive(Debug)]
pub(super) struct ConversationContribution {
pub(super) identity: u64,
pub(super) entries: Vec<OccupancyEntry>,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct ServerOccupancy {
pub(super) identity: u64,
pub(super) live_receipts: u64,
pub(super) provenance: u64,
}
#[derive(Debug)]
pub(super) struct ReservationEffects {
pub(super) conversation_id: u64,
pub(super) identity_reserved: bool,
pub(super) inserts: Vec<OccupancyEntry>,
}
pub(super) enum Stage8Choice<R, A = ()> {
Admit(A),
Refuse(R),
}
pub(super) enum Stage8Outcome<'a, R, A = ()> {
Reserved(CapacityReservation<'a>, A),
Refused(R),
}
#[derive(Debug, Default)]
struct CapacityLedger {
identity_total: u64,
identity_by_conversation: HashMap<u64, u64>,
live_receipts: BTreeSet<OccupancyEntry>,
provenance: BTreeSet<OccupancyEntry>,
}
impl CapacityLedger {
fn prune(&mut self, now: u128) {
prune_set(&mut self.live_receipts, now);
prune_set(&mut self.provenance, now);
}
fn occupancy(&self) -> Result<ServerOccupancy, StateError> {
Ok(ServerOccupancy {
identity: self.identity_total,
live_receipts: entry_count(self.live_receipts.len())?,
provenance: entry_count(self.provenance.len())?,
})
}
fn insert(&mut self, entry: OccupancyEntry) {
if entry.kind.is_live_receipt() {
self.live_receipts.insert(entry);
} else {
self.provenance.insert(entry);
}
}
fn remove(&mut self, entry: &OccupancyEntry) {
if entry.kind.is_live_receipt() {
self.live_receipts.remove(entry);
} else {
self.provenance.remove(entry);
}
}
}
fn prune_set(set: &mut BTreeSet<OccupancyEntry>, now: u128) {
match now.checked_add(1) {
Some(bound) => {
let keep = set.split_off(&OccupancyEntry::floor(bound));
*set = keep;
}
None => set.clear(),
}
}
fn entry_count(len: usize) -> Result<u64, StateError> {
u64::try_from(len).map_err(|_| {
StateError::invariant("server-scope occupancy exceeds the u64 counting domain")
})
}
#[derive(Debug, Default)]
pub(super) struct ServerCapacity {
ledger: Mutex<CapacityLedger>,
}
impl ServerCapacity {
fn ledger(&self) -> MutexGuard<'_, CapacityLedger> {
self.ledger.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) fn admit<R, A>(
&self,
now: u128,
effects: ReservationEffects,
decide: impl FnOnce(ServerOccupancy) -> Result<Stage8Choice<R, A>, StateError>,
) -> Result<Stage8Outcome<'_, R, A>, StateError> {
let mut ledger = self.ledger();
ledger.prune(now);
let occupancy = ledger.occupancy()?;
match decide(occupancy)? {
Stage8Choice::Refuse(response) => Ok(Stage8Outcome::Refused(response)),
Stage8Choice::Admit(admitted) => {
if effects.identity_reserved {
let total = ledger.identity_total.checked_add(1).ok_or_else(|| {
StateError::invariant("server identity reservation overflows u64")
})?;
let per_conversation = ledger
.identity_by_conversation
.get(&effects.conversation_id)
.copied()
.unwrap_or(0)
.checked_add(1)
.ok_or_else(|| {
StateError::invariant("conversation identity reservation overflows u64")
})?;
ledger.identity_total = total;
ledger
.identity_by_conversation
.insert(effects.conversation_id, per_conversation);
}
for entry in &effects.inserts {
ledger.insert(*entry);
}
drop(ledger);
Ok(Stage8Outcome::Reserved(
CapacityReservation {
capacity: self,
effects: Some(effects),
},
admitted,
))
}
}
}
pub(super) fn fold_conversation(
&self,
conversation_id: u64,
contribution: ConversationContribution,
) -> Result<(), StateError> {
let mut ledger = self.ledger();
let previous = ledger
.identity_by_conversation
.get(&conversation_id)
.copied()
.unwrap_or(0);
let total = ledger
.identity_total
.checked_sub(previous)
.and_then(|total| total.checked_add(contribution.identity))
.ok_or_else(|| {
StateError::invariant("server identity fold leaves the u64 counting domain")
})?;
ledger.identity_total = total;
if contribution.identity == 0 {
ledger.identity_by_conversation.remove(&conversation_id);
} else {
ledger
.identity_by_conversation
.insert(conversation_id, contribution.identity);
}
for entry in contribution.entries {
ledger.insert(entry);
}
drop(ledger);
Ok(())
}
fn rollback(&self, effects: &ReservationEffects) {
let mut ledger = self.ledger();
if effects.identity_reserved {
ledger.identity_total = ledger.identity_total.saturating_sub(1);
match ledger
.identity_by_conversation
.get(&effects.conversation_id)
.copied()
.unwrap_or(0)
.saturating_sub(1)
{
0 => {
ledger
.identity_by_conversation
.remove(&effects.conversation_id);
}
remaining => {
ledger
.identity_by_conversation
.insert(effects.conversation_id, remaining);
}
}
}
for entry in &effects.inserts {
ledger.remove(entry);
}
}
}
pub(super) struct CapacityReservation<'a> {
capacity: &'a ServerCapacity,
effects: Option<ReservationEffects>,
}
impl CapacityReservation<'_> {
pub(super) fn confirm(mut self, retire: &[OccupancyEntry]) {
if self.effects.take().is_some() {
let mut ledger = self.capacity.ledger();
for entry in retire {
ledger.remove(entry);
}
}
}
}
impl Drop for CapacityReservation<'_> {
fn drop(&mut self) {
if let Some(effects) = self.effects.take() {
self.capacity.rollback(&effects);
}
}
}
impl std::fmt::Debug for CapacityReservation<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CapacityReservation")
.field("confirmed", &self.effects.is_none())
.finish()
}
}