use std::sync::atomic::Ordering;
use tracing::{debug, warn};
use super::{CovSubscriptionKey, TimedHistories};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DropReason {
HistoryFull,
TooLarge,
Superseded,
}
impl DropReason {
fn describe(self) -> &'static str {
match self {
Self::HistoryFull => "context history full",
Self::TooLarge => {
"a timestamped value exceeds every notification to the subscriber on its own"
}
Self::Superseded => "superseded by a delivered newer change",
}
}
fn bit(self) -> u8 {
1 << self as u8
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct DropWarnings(u8);
impl DropWarnings {
fn first(&mut self, reason: DropReason) -> bool {
let first = self.0 & reason.bit() == 0;
self.0 |= reason.bit();
first
}
}
impl TimedHistories {
pub(super) fn dropped(&mut self, key: &CovSubscriptionKey, count: usize, reason: DropReason) {
self.counters
.timed_changes_dropped
.fetch_add(count as u64, Ordering::Relaxed);
let context = key.multiple_context();
let terms = context.and_then(|context| self.terms.get_mut(context));
let apdu = terms.as_ref().map(|terms| terms.apdu);
if terms.is_none_or(|terms| terms.warned.first(reason)) {
warn!(
object = ?key.object(),
recipient = ?context.map(|context| &context.recipient),
process = context.map(|context| context.process_id),
apdu,
count,
reason = reason.describe(),
"Dropped pending timestamped COV-multiple changes; further drops in this \
context for the same reason are only counted in timed_changes_dropped"
);
} else {
debug!(
object = ?key.object(),
count,
reason = reason.describe(),
"Dropped pending timestamped COV-multiple changes"
);
}
}
}
#[cfg(test)]
#[derive(Clone, Default)]
pub(crate) struct DropWarningCount(std::sync::Arc<std::sync::atomic::AtomicUsize>);
#[cfg(test)]
impl DropWarningCount {
pub(crate) fn install(&self) -> tracing::subscriber::DefaultGuard {
static REGISTRATION_PEER: std::sync::OnceLock<tracing::Dispatch> =
std::sync::OnceLock::new();
REGISTRATION_PEER
.get_or_init(|| tracing::Dispatch::new(tracing::subscriber::NoSubscriber::default()));
tracing::subscriber::set_default(self.clone())
}
pub(crate) fn get(&self) -> usize {
self.0.load(Ordering::SeqCst)
}
}
#[cfg(test)]
impl tracing::Subscriber for DropWarningCount {
fn register_callsite(
&self,
_: &'static tracing::Metadata<'static>,
) -> tracing::subscriber::Interest {
tracing::subscriber::Interest::sometimes()
}
fn max_level_hint(&self) -> Option<tracing::metadata::LevelFilter> {
Some(tracing::metadata::LevelFilter::WARN)
}
fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
metadata.target() == module_path!() && *metadata.level() == tracing::Level::WARN
}
fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
if self.enabled(event.metadata()) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
fn enter(&self, _: &tracing::span::Id) {}
fn exit(&self, _: &tracing::span::Id) {}
}
#[cfg(test)]
mod tests {
use super::super::tests::{histories, key};
use super::*;
#[test]
fn each_cause_warns_once_per_context_until_it_is_admitted_afresh() {
let warnings = DropWarningCount::default();
let _guard = warnings.install();
let (mut h, counters) = histories(8, 4);
let (a, b, other) = (key(1, 1), key(1, 2), key(2, 1));
for k in [&a, &b, &other] {
h.reset(k, 1, 10);
}
let context = a.multiple_context().unwrap().clone();
h.set_sizing(&context, Some(50), 60);
let dropped = || counters.timed_changes_dropped.load(Ordering::Relaxed);
h.dropped(&a, 1, DropReason::TooLarge);
h.dropped(&b, 2, DropReason::TooLarge);
h.dropped(&a, 1, DropReason::TooLarge);
assert_eq!((warnings.get(), dropped()), (1, 4));
h.dropped(&a, 1, DropReason::HistoryFull);
h.dropped(&other, 1, DropReason::TooLarge);
assert_eq!((warnings.get(), dropped()), (3, 6));
h.set_sizing(&context, Some(50), 60);
h.dropped(&a, 1, DropReason::TooLarge);
assert_eq!(warnings.get(), 3);
h.set_sizing(&context, Some(128), 60);
h.dropped(&a, 1, DropReason::TooLarge);
assert_eq!(warnings.get(), 4);
h.dropped(&b, 1, DropReason::TooLarge);
assert_eq!(warnings.get(), 4);
h.reset(&b, 2, 10);
h.dropped(&a, 1, DropReason::TooLarge);
assert_eq!((warnings.get(), dropped()), (5, 10));
h.remove(&other);
h.dropped(&other, 1, DropReason::Superseded);
h.dropped(&other, 1, DropReason::Superseded);
assert_eq!((warnings.get(), dropped()), (7, 12));
}
}