use liminal_protocol::lifecycle::{
EnrollmentCapacityCounters, EnrollmentCapacityDecision, FreshParticipantCapacityCounter,
ReceiptDeadlines, select_enrollment_capacity,
};
use liminal_protocol::wire::{
EnrollmentReceiptCapacityScope, EnrollmentRequest, EnrollmentResponse,
IdentityCapacityExceeded, IdentityCapacityScope,
};
use super::barrier::OperationFacts;
use super::capacity::{
OccupancyEntry, ReservationEffects, ResourceKind, ScopeCounter, ServerCapacity, Stage8Choice,
Stage8Outcome, scope_counter,
};
use super::ops_enroll::enrollment_envelope;
use super::state::{ConversationAuthority, StateError};
impl ConversationAuthority {
pub(super) fn enrollment_stage8<'cap>(
&mut self,
request: &EnrollmentRequest,
operation_facts: &OperationFacts,
server_capacity: &'cap ServerCapacity,
deadlines: &ReceiptDeadlines,
) -> Result<Stage8Outcome<'cap, EnrollmentResponse, EnrollmentCapacityCounters>, StateError>
{
let now = u128::from(operation_facts.now_ms);
self.prune_expired_provenance(now);
let identity_conversation_occupied = self.next_participant;
let provenance_conversation_occupied = self.provenance_occupancy(now)?;
let token = request.enrollment_token.into_bytes();
let effects = ReservationEffects {
conversation_id: self.conversation_id,
identity_reserved: true,
inserts: vec![
OccupancyEntry {
expires_at: deadlines.receipt_expires_at(),
conversation_id: self.conversation_id,
participant_id: self.next_participant,
kind: ResourceKind::EnrollmentReceipt,
token,
},
OccupancyEntry {
expires_at: deadlines.provenance_expires_at(),
conversation_id: self.conversation_id,
participant_id: self.next_participant,
kind: ResourceKind::EnrollmentProvenance,
token,
},
],
};
server_capacity.admit(now, effects, |server| {
let counters = match enrollment_scope_counters(
request,
operation_facts,
server,
identity_conversation_occupied,
provenance_conversation_occupied,
)? {
Ok(counters) => counters,
Err(response) => return Ok(Stage8Choice::Refuse(response)),
};
match select_enrollment_capacity(request, counters) {
EnrollmentCapacityDecision::Commit(_) => Ok(Stage8Choice::Admit(counters)),
EnrollmentCapacityDecision::Respond(response) => Ok(Stage8Choice::Refuse(response)),
}
})
}
}
fn fresh_participant_counter(
limit: u64,
scope: &'static str,
) -> Result<FreshParticipantCapacityCounter, StateError> {
FreshParticipantCapacityCounter::try_new(limit, 0).map_err(|error| {
StateError::invariant(format!(
"validated per-participant {scope} limit rejected: {error:?}"
))
})
}
#[derive(Clone, Copy)]
enum EnrollmentScope {
Identity(IdentityCapacityScope),
Receipt(EnrollmentReceiptCapacityScope),
}
const fn enrollment_scope_refusal(
request: &EnrollmentRequest,
scope: EnrollmentScope,
limit: u64,
occupied: u64,
) -> EnrollmentResponse {
match scope {
EnrollmentScope::Identity(scope) => {
EnrollmentResponse::identity_capacity_exceeded(IdentityCapacityExceeded {
request: enrollment_envelope(request),
scope,
limit,
occupied,
})
}
EnrollmentScope::Receipt(scope) => EnrollmentResponse::receipt_capacity_exceeded(
enrollment_envelope(request),
scope,
limit,
occupied,
),
}
}
fn enrollment_scope_counters(
request: &EnrollmentRequest,
operation_facts: &OperationFacts,
server: super::capacity::ServerOccupancy,
identity_conversation_occupied: u64,
provenance_conversation_occupied: u64,
) -> Result<Result<EnrollmentCapacityCounters, EnrollmentResponse>, StateError> {
let limits = operation_facts.receipt_limits;
let ordered = [
(
limits.identity_server,
server.identity,
EnrollmentScope::Identity(IdentityCapacityScope::Server),
),
(
operation_facts.identity_slots,
identity_conversation_occupied,
EnrollmentScope::Identity(IdentityCapacityScope::Conversation),
),
(
limits.live_receipts_server,
server.live_receipts,
EnrollmentScope::Receipt(EnrollmentReceiptCapacityScope::LiveReceiptServer),
),
(
limits.provenance_server,
server.provenance,
EnrollmentScope::Receipt(EnrollmentReceiptCapacityScope::ProvenanceServer),
),
(
limits.provenance_per_conversation,
provenance_conversation_occupied,
EnrollmentScope::Receipt(EnrollmentReceiptCapacityScope::ProvenanceConversation),
),
];
let mut counters = Vec::with_capacity(ordered.len());
let mut first_full: Option<(EnrollmentScope, u64, u64)> = None;
for (limit, occupied, scope) in ordered {
match scope_counter(limit, occupied)? {
ScopeCounter::Valid(counter) => {
if first_full.is_none() && counter.is_full() {
first_full = Some((scope, counter.limit(), counter.occupied()));
}
counters.push(counter);
}
ScopeCounter::OverLimit { limit, occupied } => {
let (scope, limit, occupied) = first_full.unwrap_or((scope, limit, occupied));
return Ok(Err(enrollment_scope_refusal(
request, scope, limit, occupied,
)));
}
}
}
let [
identity_server,
identity_conversation,
live_receipt_server,
provenance_server,
provenance_conversation,
]: [liminal_protocol::lifecycle::CapacityCounter; 5] = counters.try_into().map_err(|_| {
StateError::invariant("enrollment stage-8 scope construction lost a counter")
})?;
Ok(Ok(EnrollmentCapacityCounters::new(
identity_server,
identity_conversation,
live_receipt_server,
fresh_participant_counter(limits.live_receipts_per_participant, "live-receipt")?,
provenance_server,
provenance_conversation,
fresh_participant_counter(limits.provenance_per_participant, "provenance")?,
)))
}