use std::collections::VecDeque;
use std::num::NonZeroUsize;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraceEvent {
TaskSubmitted,
TaskStarted,
TaskCompleted,
TaskFailed,
CancellationRequested,
CancellationAcknowledged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraceRecordError {
Full(TraceEvent),
Poisoned(TraceEvent),
}
impl TraceRecordError {
#[must_use]
pub const fn into_event(self) -> TraceEvent {
match self {
Self::Full(event) | Self::Poisoned(event) => event,
}
}
}
impl core::fmt::Display for TraceRecordError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Full(_) => f.write_str("trace buffer is full"),
Self::Poisoned(_) => f.write_str("trace buffer lock is poisoned"),
}
}
}
impl std::error::Error for TraceRecordError {}
#[derive(Debug)]
pub struct TraceBuffer {
capacity: NonZeroUsize,
events: Mutex<VecDeque<TraceEvent>>,
}
impl TraceBuffer {
#[must_use]
pub fn new(capacity: NonZeroUsize) -> Self {
Self {
capacity,
events: Mutex::new(VecDeque::with_capacity(capacity.get())),
}
}
pub fn record(&self, event: TraceEvent) -> Result<(), TraceRecordError> {
let mut events = self
.events
.lock()
.map_err(|_| TraceRecordError::Poisoned(event))?;
if events.len() == self.capacity.get() {
return Err(TraceRecordError::Full(event));
}
events.push_back(event);
Ok(())
}
pub fn snapshot(&self) -> Result<Vec<TraceEvent>, TraceRecordError> {
self.events
.lock()
.map(|events| events.iter().copied().collect())
.map_err(|_| {
TraceRecordError::Poisoned(TraceEvent::TaskFailed)
})
}
pub fn drain(&self) -> Result<Vec<TraceEvent>, TraceRecordError> {
self.events
.lock()
.map(|mut events| events.drain(..).collect())
.map_err(|_| TraceRecordError::Poisoned(TraceEvent::TaskFailed))
}
#[must_use]
pub const fn capacity(&self) -> NonZeroUsize {
self.capacity
}
}
#[cfg(test)]
mod tests {
use super::{TraceBuffer, TraceEvent, TraceRecordError};
use std::num::NonZeroUsize;
#[test]
fn records_and_drains_events_in_order() {
let buffer = TraceBuffer::new(NonZeroUsize::new(2).unwrap());
buffer.record(TraceEvent::TaskSubmitted).unwrap();
buffer.record(TraceEvent::TaskStarted).unwrap();
assert_eq!(
buffer.snapshot().unwrap(),
vec![TraceEvent::TaskSubmitted, TraceEvent::TaskStarted]
);
assert_eq!(
buffer.drain().unwrap(),
vec![TraceEvent::TaskSubmitted, TraceEvent::TaskStarted]
);
assert!(buffer.snapshot().unwrap().is_empty());
}
#[test]
fn full_buffer_returns_event_without_overwriting_existing_events() {
let buffer = TraceBuffer::new(NonZeroUsize::new(1).unwrap());
buffer.record(TraceEvent::TaskSubmitted).unwrap();
assert_eq!(
buffer.record(TraceEvent::TaskFailed),
Err(TraceRecordError::Full(TraceEvent::TaskFailed))
);
assert_eq!(buffer.snapshot().unwrap(), vec![TraceEvent::TaskSubmitted]);
}
#[test]
fn concurrent_recording_is_safe_and_bounded() {
let buffer = std::sync::Arc::new(TraceBuffer::new(NonZeroUsize::new(800).unwrap()));
let threads: Vec<_> = (0..8)
.map(|_| {
let buffer = std::sync::Arc::clone(&buffer);
std::thread::spawn(move || {
for _ in 0..100 {
let _ = buffer.record(TraceEvent::TaskCompleted);
}
})
})
.collect();
for thread in threads {
thread.join().unwrap();
}
assert_eq!(buffer.snapshot().unwrap().len(), 800);
}
}