use std::cell::Cell;
use std::fmt;
use std::marker::PhantomData;
use std::rc::Rc;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LocalAccountingCounter {
RetainedBytes,
UnknownItems,
AbandonedItems,
AbandonedBytes,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LocalAccountingError {
Overflow(LocalAccountingCounter),
Underflow(LocalAccountingCounter),
}
impl fmt::Display for LocalAccountingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Overflow(counter) => write!(formatter, "{counter:?} counter overflow"),
Self::Underflow(counter) => write!(formatter, "{counter:?} counter underflow"),
}
}
}
impl std::error::Error for LocalAccountingError {}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct LocalRetainedSnapshot {
pub retained_bytes: u64,
pub unknown_items: u64,
pub abandoned_items: u64,
pub abandoned_bytes: u64,
pub corruption_count: u64,
}
#[derive(Debug)]
pub struct LocalRetainedAccount {
retained_bytes: Cell<u64>,
unknown_items: Cell<u64>,
abandoned_items: Cell<u64>,
abandoned_bytes: Cell<u64>,
corruption_count: Cell<u64>,
_not_send: PhantomData<Rc<()>>,
}
impl LocalRetainedAccount {
#[must_use]
pub fn new() -> Rc<Self> {
Rc::new(Self {
retained_bytes: Cell::new(0),
unknown_items: Cell::new(0),
abandoned_items: Cell::new(0),
abandoned_bytes: Cell::new(0),
corruption_count: Cell::new(0),
_not_send: PhantomData,
})
}
#[inline]
pub fn charge(
self: &Rc<Self>,
bytes: Option<u64>,
) -> Result<LocalRetainedTicket, LocalAccountingError> {
let charge = match bytes {
Some(bytes) => {
self.checked_add(
&self.retained_bytes,
bytes,
LocalAccountingCounter::RetainedBytes,
)?;
LocalRetainedCharge::Known(bytes)
}
None => {
self.checked_add(&self.unknown_items, 1, LocalAccountingCounter::UnknownItems)?;
LocalRetainedCharge::Unknown
}
};
Ok(LocalRetainedTicket {
account: Rc::clone(self),
charge,
active: true,
})
}
#[must_use]
pub fn snapshot(&self) -> LocalRetainedSnapshot {
LocalRetainedSnapshot {
retained_bytes: self.retained_bytes.get(),
unknown_items: self.unknown_items.get(),
abandoned_items: self.abandoned_items.get(),
abandoned_bytes: self.abandoned_bytes.get(),
corruption_count: self.corruption_count.get(),
}
}
fn settle(&self, charge: LocalRetainedCharge) -> Result<(), LocalAccountingError> {
match charge {
LocalRetainedCharge::Known(bytes) => self.checked_sub(
&self.retained_bytes,
bytes,
LocalAccountingCounter::RetainedBytes,
),
LocalRetainedCharge::Unknown => {
self.checked_sub(&self.unknown_items, 1, LocalAccountingCounter::UnknownItems)
}
}
}
fn record_abandonment(&self, charge: LocalRetainedCharge) {
let _ = self.checked_add(
&self.abandoned_items,
1,
LocalAccountingCounter::AbandonedItems,
);
if let LocalRetainedCharge::Known(bytes) = charge {
let _ = self.checked_add(
&self.abandoned_bytes,
bytes,
LocalAccountingCounter::AbandonedBytes,
);
}
}
fn checked_add(
&self,
cell: &Cell<u64>,
value: u64,
counter: LocalAccountingCounter,
) -> Result<(), LocalAccountingError> {
let Some(next) = cell.get().checked_add(value) else {
self.record_corruption();
return Err(LocalAccountingError::Overflow(counter));
};
cell.set(next);
Ok(())
}
fn checked_sub(
&self,
cell: &Cell<u64>,
value: u64,
counter: LocalAccountingCounter,
) -> Result<(), LocalAccountingError> {
let Some(next) = cell.get().checked_sub(value) else {
self.record_corruption();
return Err(LocalAccountingError::Underflow(counter));
};
cell.set(next);
Ok(())
}
fn record_corruption(&self) {
if let Some(next) = self.corruption_count.get().checked_add(1) {
self.corruption_count.set(next);
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LocalRetainedCharge {
Known(u64),
Unknown,
}
#[derive(Debug)]
#[must_use = "the ticket must be completed normally or dropped as abandoned"]
pub struct LocalRetainedTicket {
account: Rc<LocalRetainedAccount>,
charge: LocalRetainedCharge,
active: bool,
}
impl LocalRetainedTicket {
#[must_use]
pub const fn bytes(&self) -> Option<u64> {
match self.charge {
LocalRetainedCharge::Known(bytes) => Some(bytes),
LocalRetainedCharge::Unknown => None,
}
}
#[inline]
pub fn complete(mut self) -> Result<(), LocalAccountingError> {
self.active = false;
self.account.settle(self.charge)
}
}
impl Drop for LocalRetainedTicket {
fn drop(&mut self) {
if !self.active {
return;
}
self.active = false;
let _ = self.account.settle(self.charge);
self.account.record_abandonment(self.charge);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_charge_completes_normally() {
let account = LocalRetainedAccount::new();
let ticket = account.charge(Some(42)).expect("charge should fit");
assert_eq!(ticket.bytes(), Some(42));
assert_eq!(account.snapshot().retained_bytes, 42);
ticket.complete().expect("completion should settle");
assert_eq!(account.snapshot(), LocalRetainedSnapshot::default());
}
#[test]
fn unknown_charge_completes_normally() {
let account = LocalRetainedAccount::new();
let ticket = account.charge(None).expect("charge should fit");
assert_eq!(ticket.bytes(), None);
assert_eq!(account.snapshot().unknown_items, 1);
ticket.complete().expect("completion should settle");
assert_eq!(account.snapshot(), LocalRetainedSnapshot::default());
}
#[test]
fn unresolved_known_ticket_refunds_and_records_abandonment() {
let account = LocalRetainedAccount::new();
let ticket = account.charge(Some(17)).expect("charge should fit");
drop(ticket);
assert_eq!(
account.snapshot(),
LocalRetainedSnapshot {
abandoned_items: 1,
abandoned_bytes: 17,
..LocalRetainedSnapshot::default()
}
);
}
#[test]
fn unresolved_unknown_ticket_refunds_and_records_abandonment() {
let account = LocalRetainedAccount::new();
let ticket = account.charge(None).expect("charge should fit");
drop(ticket);
assert_eq!(
account.snapshot(),
LocalRetainedSnapshot {
abandoned_items: 1,
..LocalRetainedSnapshot::default()
}
);
}
#[test]
fn completion_prevents_drop_from_settling_twice() {
let account = LocalRetainedAccount::new();
let ticket = account.charge(Some(9)).expect("charge should fit");
ticket.complete().expect("completion should settle");
assert_eq!(account.snapshot(), LocalRetainedSnapshot::default());
}
#[test]
fn charge_overflow_is_rejected_and_recorded() {
let account = LocalRetainedAccount::new();
account.retained_bytes.set(u64::MAX);
let error = account
.charge(Some(1))
.expect_err("overflowing charge must fail");
assert_eq!(
error,
LocalAccountingError::Overflow(LocalAccountingCounter::RetainedBytes)
);
assert_eq!(account.snapshot().retained_bytes, u64::MAX);
assert_eq!(account.snapshot().corruption_count, 1);
}
#[test]
fn settlement_underflow_is_reported_and_recorded() {
let account = LocalRetainedAccount::new();
let ticket = account.charge(Some(5)).expect("charge should fit");
account.retained_bytes.set(0);
let error = ticket
.complete()
.expect_err("corrupted settlement must fail");
assert_eq!(
error,
LocalAccountingError::Underflow(LocalAccountingCounter::RetainedBytes)
);
assert_eq!(account.snapshot().retained_bytes, 0);
assert_eq!(account.snapshot().corruption_count, 1);
assert_eq!(account.snapshot().abandoned_items, 0);
}
}