use crate::{
AllocationDeclaration, AllocationLedger, AllocationRecord, AllocationState,
LedgerIntegrityError, MemoryManagerSlot, StableKey,
key::StableKeyError,
ledger::{ClaimConflict, ClaimOutcome, checked_next_generation, validate_new_claim},
};
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
pub enum AllocationReservationError {
#[error(transparent)]
Integrity(#[from] LedgerIntegrityError),
#[error("ledger generation {generation} cannot be advanced without overflow")]
GenerationOverflow {
generation: u64,
},
#[error("generation contains {count} reservations, exceeding the 255-allocation limit")]
TooManyReservations {
count: usize,
},
#[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
StableKeySlotConflict {
stable_key: StableKey,
historical_slot: MemoryManagerSlot,
reserved_slot: MemoryManagerSlot,
},
#[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
SlotStableKeyConflict {
slot: MemoryManagerSlot,
historical_key: StableKey,
reserved_key: StableKey,
},
#[error("stable key '{stable_key}' is already active and cannot be reserved")]
ActiveAllocation {
stable_key: StableKey,
slot: MemoryManagerSlot,
},
#[error("stable key '{stable_key}' was explicitly retired and cannot be reserved")]
RetiredAllocation {
stable_key: StableKey,
slot: MemoryManagerSlot,
},
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
pub enum AllocationRetirementError {
#[error(transparent)]
Integrity(#[from] LedgerIntegrityError),
#[error(transparent)]
Key(StableKeyError),
#[error("ledger generation {generation} cannot be advanced without overflow")]
GenerationOverflow {
generation: u64,
},
#[error("stable key '{0}' has no allocation record to retire")]
UnknownStableKey(StableKey),
#[error("stable key '{stable_key}' cannot be retired for a different allocation slot")]
SlotMismatch {
stable_key: StableKey,
historical_slot: MemoryManagerSlot,
retired_slot: MemoryManagerSlot,
},
#[error("stable key '{stable_key}' was already retired")]
AlreadyRetired {
stable_key: StableKey,
slot: MemoryManagerSlot,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AllocationRetirement {
pub(crate) stable_key: StableKey,
pub(crate) slot: MemoryManagerSlot,
}
impl AllocationRetirement {
pub fn new(
stable_key: impl AsRef<str>,
slot: MemoryManagerSlot,
) -> Result<Self, AllocationRetirementError> {
let stable_key = StableKey::parse(stable_key).map_err(AllocationRetirementError::Key)?;
Ok(Self { stable_key, slot })
}
#[must_use]
pub const fn stable_key(&self) -> &StableKey {
&self.stable_key
}
#[must_use]
pub const fn slot(&self) -> &MemoryManagerSlot {
&self.slot
}
}
impl AllocationLedger {
pub fn stage_reservation_generation(
&self,
reservations: &[AllocationDeclaration],
) -> Result<Self, AllocationReservationError> {
self.validate_bounds()?;
let parent_generation = self.current_generation;
let next_generation = checked_next_generation(parent_generation)
.map_err(|generation| AllocationReservationError::GenerationOverflow { generation })?;
checked_reservation_count(reservations.len())?;
let mut next = self.clone();
next.current_generation = next_generation;
for reservation in reservations {
record_reservation(&mut next, reservation)?;
}
next.validate_bounds()?;
Ok(next)
}
pub fn stage_retirement_generation(
&self,
retirement: &AllocationRetirement,
) -> Result<Self, AllocationRetirementError> {
self.validate_bounds()?;
let parent_generation = self.current_generation;
let next_generation = checked_next_generation(parent_generation)
.map_err(|generation| AllocationRetirementError::GenerationOverflow { generation })?;
let record_index = self
.records()
.iter()
.position(|record| record.stable_key == retirement.stable_key)
.ok_or_else(|| {
AllocationRetirementError::UnknownStableKey(retirement.stable_key.clone())
})?;
let record = &self.records()[record_index];
if record.slot != retirement.slot {
return Err(AllocationRetirementError::SlotMismatch {
stable_key: retirement.stable_key.clone(),
historical_slot: record.slot.clone(),
retired_slot: retirement.slot.clone(),
});
}
if matches!(record.state, AllocationState::Retired) {
return Err(AllocationRetirementError::AlreadyRetired {
stable_key: retirement.stable_key.clone(),
slot: record.slot.clone(),
});
}
let mut next = self.clone();
next.records[record_index].state = AllocationState::Retired;
next.current_generation = next_generation;
Ok(next)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ReservationClaimConflict<'ledger> {
Claim(ClaimConflict<'ledger>),
ActiveAllocation { record: &'ledger AllocationRecord },
}
fn validate_reservation_claim<'ledger>(
ledger: &'ledger AllocationLedger,
reservation: &AllocationDeclaration,
) -> Result<ClaimOutcome, ReservationClaimConflict<'ledger>> {
if let Some(record_index) = ledger
.records()
.iter()
.position(|record| record.stable_key() == reservation.stable_key())
{
let record = &ledger.records()[record_index];
if record.slot != reservation.slot {
return Err(ReservationClaimConflict::Claim(
ClaimConflict::StableKeyMoved { record },
));
}
return match record.state {
AllocationState::Reserved => Ok(ClaimOutcome::Existing { record_index }),
AllocationState::Active => Err(ReservationClaimConflict::ActiveAllocation { record }),
AllocationState::Retired => {
Err(ReservationClaimConflict::Claim(ClaimConflict::Tombstoned {
record,
}))
}
};
}
validate_new_claim(ledger, &reservation.slot).map_err(ReservationClaimConflict::Claim)
}
fn record_reservation(
ledger: &mut AllocationLedger,
reservation: &AllocationDeclaration,
) -> Result<(), AllocationReservationError> {
match validate_reservation_claim(ledger, reservation) {
Ok(ClaimOutcome::Existing { record_index }) => {
ledger.records[record_index]
.schema
.clone_from(&reservation.schema);
Ok(())
}
Ok(ClaimOutcome::New) => {
let record = AllocationRecord::reserved(reservation);
ledger.records.push(record);
Ok(())
}
Err(conflict) => Err(map_reservation_stage_conflict(reservation, conflict)),
}
}
const fn checked_reservation_count(count: usize) -> Result<(), AllocationReservationError> {
if count > crate::constants::MAX_ALLOCATIONS {
return Err(AllocationReservationError::TooManyReservations { count });
}
Ok(())
}
fn map_reservation_stage_conflict(
reservation: &AllocationDeclaration,
conflict: ReservationClaimConflict<'_>,
) -> AllocationReservationError {
let conflict = match conflict {
ReservationClaimConflict::ActiveAllocation { record } => {
return AllocationReservationError::ActiveAllocation {
stable_key: reservation.stable_key.clone(),
slot: record.slot.clone(),
};
}
ReservationClaimConflict::Claim(conflict) => conflict,
};
match conflict {
ClaimConflict::StableKeyMoved { record } => {
AllocationReservationError::StableKeySlotConflict {
stable_key: reservation.stable_key.clone(),
historical_slot: record.slot.clone(),
reserved_slot: reservation.slot.clone(),
}
}
ClaimConflict::SlotReused { record } => AllocationReservationError::SlotStableKeyConflict {
slot: reservation.slot.clone(),
historical_key: record.stable_key.clone(),
reserved_key: reservation.stable_key.clone(),
},
ClaimConflict::Tombstoned { record } => AllocationReservationError::RetiredAllocation {
stable_key: reservation.stable_key.clone(),
slot: record.slot.clone(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ledger::validate_declaration_claim;
#[test]
fn reservation_count_fails_closed_on_overflow() {
assert_eq!(checked_reservation_count(0), Ok(()));
assert_eq!(checked_reservation_count(255), Ok(()));
for count in [256, usize::MAX] {
assert_eq!(
checked_reservation_count(count),
Err(AllocationReservationError::TooManyReservations { count })
);
}
}
#[test]
fn retired_moved_key_keeps_each_operations_error_precedence() {
let original = AllocationDeclaration::memory_manager("app.rows.v1", 100).unwrap();
let mut record = AllocationRecord::active(&original);
record.state = AllocationState::Retired;
let ledger = AllocationLedger::new(1, vec![record]).unwrap();
let moved = AllocationDeclaration::memory_manager("app.rows.v1", 101).unwrap();
assert!(matches!(
validate_declaration_claim(&ledger, &moved),
Err(ClaimConflict::Tombstoned { .. })
));
assert!(matches!(
validate_reservation_claim(&ledger, &moved),
Err(ReservationClaimConflict::Claim(
ClaimConflict::StableKeyMoved { .. }
))
));
assert!(matches!(
validate_reservation_claim(&ledger, &original),
Err(ReservationClaimConflict::Claim(
ClaimConflict::Tombstoned { .. }
))
));
}
}