use std::{
collections::{HashMap, VecDeque},
num::NonZeroUsize,
sync::Arc,
};
#[cfg(any(feature = "testing", test))]
use bytes_utils::Str;
use super::DeliveryInner;
#[cfg(any(feature = "testing", test))]
use crate::RawMessage;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Retention {
Messages(NonZeroUsize),
Bytes(NonZeroUsize),
MessagesAndBytes {
messages: NonZeroUsize,
bytes: NonZeroUsize,
},
}
impl Retention {
fn admits(self, messages: usize, bytes: usize) -> bool {
match self {
Self::Messages(limit) => messages <= limit.get(),
Self::Bytes(limit) => bytes <= limit.get(),
Self::MessagesAndBytes {
messages: message_limit,
bytes: byte_limit,
} => messages <= message_limit.get() && bytes <= byte_limit.get(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Discarding;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Retaining;
pub trait LogMode: sealed::Sealed + Send + Sync + 'static {}
impl LogMode for Discarding {}
impl LogMode for Retaining {}
mod sealed {
pub trait Sealed {
const RETAINS: bool;
}
impl Sealed for super::Discarding {
const RETAINS: bool = false;
}
impl Sealed for super::Retaining {
const RETAINS: bool = true;
}
}
pub(super) const fn retains<Log: LogMode>() -> bool {
<Log as sealed::Sealed>::RETAINS
}
pub(super) enum LogState {
Discarding,
Recording {
budget: Budget,
names: HashMap<Arc<str>, NameLog>,
},
}
pub(super) enum Budget {
Bounded(Retention),
#[cfg(feature = "testing")]
Unbounded,
}
impl LogState {
pub(super) fn recording(retention: Retention) -> Self {
Self::Recording {
budget: Budget::Bounded(retention),
names: HashMap::new(),
}
}
#[cfg(feature = "testing")]
pub(super) fn record_for_harness(&mut self) -> bool {
if matches!(self, Self::Recording { .. }) {
return false;
}
*self = Self::Recording {
budget: Budget::Unbounded,
names: HashMap::new(),
};
true
}
pub(super) fn append(&mut self, shared: &Arc<DeliveryInner>) -> usize {
let Self::Recording { budget, names } = self else {
return 0;
};
let log = names.entry(Arc::clone(&shared.name)).or_default();
log.push(
LogEntry {
shared: Arc::clone(shared),
},
budget,
)
}
pub(super) fn name(&self, name: &str) -> Option<&NameLog> {
match self {
Self::Discarding => None,
Self::Recording { names, .. } => names.get(name),
}
}
#[cfg(test)]
pub(super) fn recorded_names(&self) -> Vec<&str> {
match self {
Self::Discarding => Vec::new(),
Self::Recording { names, .. } => names.keys().map(|name| &**name).collect(),
}
}
pub(super) fn forget(&mut self, name: &str) {
if let Self::Recording { names, .. } = self {
names.remove(name);
}
}
}
#[derive(Default)]
pub(super) struct NameLog {
entries: VecDeque<LogEntry>,
first_seq: usize,
bytes: usize,
}
impl NameLog {
pub(super) fn first_seq(&self) -> usize {
self.first_seq
}
pub(super) fn next_seq(&self) -> usize {
self.first_seq + self.entries.len()
}
pub(super) fn replay_from(&self, seq: usize) -> impl Iterator<Item = (usize, &LogEntry)> {
let skip = seq.saturating_sub(self.first_seq);
self.entries
.iter()
.enumerate()
.skip(skip)
.map(|(offset, entry)| (self.first_seq + offset, entry))
}
#[cfg(any(feature = "testing", test))]
pub(super) fn messages(&self, name: &str) -> Vec<RawMessage> {
let name = Str::from(name);
self.entries
.iter()
.map(|entry| {
RawMessage::new(name.clone(), entry.shared.payload.clone())
.with_headers(entry.shared.headers.clone())
})
.collect()
}
fn push(&mut self, entry: LogEntry, budget: &Budget) -> usize {
let seq = self.next_seq();
self.bytes += entry.shared.payload.len();
self.entries.push_back(entry);
#[cfg_attr(not(feature = "testing"), allow(irrefutable_let_patterns))]
if let Budget::Bounded(retention) = budget {
while self.entries.len() > 1 && !retention.admits(self.entries.len(), self.bytes) {
if let Some(evicted) = self.entries.pop_front() {
self.bytes -= evicted.shared.payload.len();
self.first_seq += 1;
}
}
}
seq
}
}
pub(super) struct LogEntry {
shared: Arc<DeliveryInner>,
}
impl LogEntry {
pub(super) fn shared(&self) -> Arc<DeliveryInner> {
Arc::clone(&self.shared)
}
}