use std::collections::VecDeque;
use std::panic::Location;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use wasm_lite_std::time::Instant;
pub const DEFAULT_CAPACITY: usize = 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Outcome {
Outstanding,
Sent,
FutureHungUp,
SenderDroppedUnsent,
}
impl Outcome {
pub const fn name(self) -> &'static str {
match self {
Outcome::Outstanding => "outstanding",
Outcome::Sent => "sent",
Outcome::FutureHungUp => "future-hung-up",
Outcome::SenderDroppedUnsent => "sender-dropped-unsent",
}
}
}
#[derive(Clone, Debug)]
pub struct Entry {
pub seq: u64,
pub context: u64,
pub created_at_file: &'static str,
pub created_at_line: u32,
pub created: Instant,
pub outcome: Outcome,
pub polled: bool,
}
struct Registry {
capacity: usize,
entries: VecDeque<Entry>,
}
impl Registry {
fn push(&mut self, entry: Entry) -> u64 {
let mut forgotten = 0;
while self.entries.len() >= self.capacity {
let victim = self
.entries
.iter()
.position(|entry| entry.outcome != Outcome::Outstanding)
.unwrap_or(0);
self.entries.remove(victim);
forgotten += 1;
}
self.entries.push_back(entry);
forgotten
}
}
static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
static DROPPED: AtomicU64 = AtomicU64::new(0);
static CREATED: AtomicU64 = AtomicU64::new(0);
static NEXT_SEQ: AtomicU64 = AtomicU64::new(1);
fn configured_capacity() -> usize {
std::env::var("CONTINUE_REGISTRY_CAPACITY")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|capacity| *capacity > 0)
.unwrap_or(DEFAULT_CAPACITY)
}
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> Option<R> {
let mut guard = REGISTRY.try_lock().ok()?;
let registry = guard.get_or_insert_with(|| Registry {
capacity: configured_capacity(),
entries: VecDeque::new(),
});
Some(f(registry))
}
pub(crate) fn record_created(context: u64, location: &'static Location<'static>) -> u64 {
CREATED.fetch_add(1, Ordering::Relaxed);
let seq = NEXT_SEQ.fetch_add(1, Ordering::Relaxed);
let recorded = with(|registry| {
let forgotten = registry.push(Entry {
seq,
context,
created_at_file: location.file(),
created_at_line: location.line(),
created: Instant::now(),
outcome: Outcome::Outstanding,
polled: false,
});
DROPPED.fetch_add(forgotten, Ordering::Relaxed);
});
if recorded.is_none() {
DROPPED.fetch_add(1, Ordering::Relaxed);
}
seq
}
pub(crate) fn record_polled(seq: u64) {
let _ = with(|registry| {
if let Some(entry) = registry.entries.iter_mut().find(|entry| entry.seq == seq) {
entry.polled = true;
}
});
}
pub(crate) fn record_outcome(seq: u64, outcome: Outcome) {
let _ = with(|registry| {
if let Some(entry) = registry.entries.iter_mut().find(|entry| entry.seq == seq) {
if entry.outcome == Outcome::Outstanding {
entry.outcome = outcome;
}
}
});
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RegistryStats {
pub created: u64,
pub dropped: u64,
pub retained: usize,
pub outstanding: usize,
pub capacity: usize,
}
pub fn stats() -> RegistryStats {
let (retained, outstanding, capacity) = with(|registry| {
(
registry.entries.len(),
registry
.entries
.iter()
.filter(|entry| entry.outcome == Outcome::Outstanding)
.count(),
registry.capacity,
)
})
.unwrap_or((0, 0, configured_capacity()));
RegistryStats {
created: CREATED.load(Ordering::Relaxed),
dropped: DROPPED.load(Ordering::Relaxed),
retained,
outstanding,
capacity,
}
}
pub fn entries() -> Option<Vec<Entry>> {
with(|registry| registry.entries.iter().cloned().collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(seq: u64, outcome: Outcome) -> Entry {
Entry {
seq,
context: 0,
created_at_file: "test",
created_at_line: 1,
created: Instant::now(),
outcome,
polled: false,
}
}
fn registry(capacity: usize) -> Registry {
Registry {
capacity,
entries: VecDeque::new(),
}
}
#[test]
fn overflow_forgets_settled_before_outstanding() {
let mut registry = registry(2);
assert_eq!(registry.push(entry(1, Outcome::Outstanding)), 0);
assert_eq!(registry.push(entry(2, Outcome::Sent)), 0);
assert_eq!(registry.push(entry(3, Outcome::Outstanding)), 1);
let ids: Vec<u64> = registry.entries.iter().map(|entry| entry.seq).collect();
assert_eq!(ids, vec![1, 3], "the settled entry went first");
}
#[test]
fn overflow_of_all_outstanding_drops_the_oldest_and_counts_it() {
let mut registry = registry(2);
registry.push(entry(1, Outcome::Outstanding));
registry.push(entry(2, Outcome::Outstanding));
assert_eq!(registry.push(entry(3, Outcome::Outstanding)), 1);
let ids: Vec<u64> = registry.entries.iter().map(|entry| entry.seq).collect();
assert_eq!(ids, vec![2, 3]);
}
#[test]
fn a_capacity_of_one_keeps_only_the_newest() {
let mut registry = registry(1);
registry.push(entry(1, Outcome::Outstanding));
assert_eq!(registry.push(entry(2, Outcome::Outstanding)), 1);
let ids: Vec<u64> = registry.entries.iter().map(|entry| entry.seq).collect();
assert_eq!(ids, vec![2]);
}
}