use std::{
sync::Arc,
sync::atomic::{
AtomicU64,
Ordering,
},
time::{
Duration,
Instant,
},
};
use crate::{
Event,
Metric,
MetricHandle,
MetricSnapshot,
OperationAttributes,
Phase,
Reporter,
Stage,
auto_reporter::{
self,
AutoReporter,
},
error::{
CompletionError,
ConfigurationError,
DeliveryError,
EmissionError,
FinishError,
RecoverableFinishError,
StartError,
TerminalError,
},
internal::OperationState,
validation::{
validate_attributes,
validate_metrics,
validate_stage,
},
};
#[cfg(coverage)]
use crate::{
NoopReporter,
error::ReporterError,
};
static NEXT_OPERATION_ID: AtomicU64 = AtomicU64::new(1);
#[cfg(coverage)]
struct CoverageTerminalReporter {
attempts: AtomicU64,
}
#[cfg(coverage)]
impl Reporter for CoverageTerminalReporter {
fn report(&self, _event: &Event) -> Result<(), ReporterError> {
if self.attempts.fetch_add(1, Ordering::Relaxed) == 0 {
Ok(())
} else {
Err(ReporterError::message("coverage terminal failure"))
}
}
}
enum ReporterHandle<'reporter> {
Borrowed(&'reporter dyn Reporter),
Owned(Arc<dyn Reporter>),
}
impl ReporterHandle<'_> {
fn as_reporter(&self) -> &dyn Reporter {
match self {
Self::Borrowed(reporter) => *reporter,
Self::Owned(reporter) => reporter.as_ref(),
}
}
}
pub struct ProgressBuilder<'reporter> {
reporter: ReporterHandle<'reporter>,
interval: Duration,
metrics: Vec<Metric>,
stage: Option<Stage>,
attributes: OperationAttributes,
}
impl<'reporter> ProgressBuilder<'reporter> {
#[must_use]
pub const fn interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
#[must_use]
pub fn metric(mut self, metric: Metric) -> Self {
self.metrics.push(metric);
self
}
#[must_use]
pub fn stage(mut self, stage: Stage) -> Self {
self.stage = Some(stage);
self
}
#[must_use]
pub fn attribute(mut self, key: &str, value: &str) -> Self {
self.attributes.insert(key, value);
self
}
#[must_use]
pub fn attributes(mut self, attributes: OperationAttributes) -> Self {
self.attributes = attributes;
self
}
pub fn start(self) -> Result<Progress<'reporter>, StartError> {
validate_metrics(&self.metrics)?;
if let Some(stage) = &self.stage {
validate_stage(stage)?;
}
validate_attributes(&self.attributes)?;
let enabled = self.reporter.as_reporter().is_enabled();
let operation_state = OperationState::new();
let operation_id = enabled.then(allocate_operation_id).transpose()?;
let mut progress = Progress {
reporter: self.reporter,
enabled,
metrics: self
.metrics
.into_iter()
.map(|metric| {
MetricHandle::new(metric, Arc::clone(&operation_state))
})
.collect(),
operation_state,
stage: self.stage,
attributes: Arc::new(self.attributes),
interval: self.interval,
started_at: Instant::now(),
next_due_elapsed: None,
operation_id,
next_sequence: 0,
};
if enabled {
let metrics = progress.metric_snapshots();
progress
.emit(Phase::Started, metrics, Duration::ZERO)
.map_err(StartError::from)?;
progress.next_due_elapsed = Some(progress.interval);
progress.started_at = Instant::now();
}
Ok(progress)
}
}
#[must_use]
pub struct Progress<'reporter> {
reporter: ReporterHandle<'reporter>,
enabled: bool,
metrics: Vec<MetricHandle>,
operation_state: Arc<OperationState>,
stage: Option<Stage>,
attributes: Arc<OperationAttributes>,
interval: Duration,
started_at: Instant,
next_due_elapsed: Option<Duration>,
operation_id: Option<u64>,
next_sequence: u64,
}
impl<'reporter> Progress<'reporter> {
#[must_use]
pub fn builder(
reporter: &'reporter dyn Reporter,
) -> ProgressBuilder<'reporter> {
ProgressBuilder {
reporter: ReporterHandle::Borrowed(reporter),
interval: Duration::ZERO,
metrics: Vec::new(),
stage: None,
attributes: OperationAttributes::new(),
}
}
#[must_use]
pub fn builder_arc(
reporter: Arc<dyn Reporter>,
) -> ProgressBuilder<'static> {
ProgressBuilder {
reporter: ReporterHandle::Owned(reporter),
interval: Duration::ZERO,
metrics: Vec::new(),
stage: None,
attributes: OperationAttributes::new(),
}
}
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.enabled
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
pub fn metric(&self, metric_id: &str) -> Option<MetricHandle> {
self.metrics
.iter()
.find(|metric| metric.id() == metric_id)
.cloned()
}
pub fn report(&mut self) -> Result<(), EmissionError> {
if !self.enabled {
return Ok(());
}
let metrics = self.metric_snapshots();
let elapsed = self.elapsed();
let result = self.emit(Phase::Running, metrics, elapsed);
self.reset_deadline();
result
}
pub fn report_if_due(&mut self) -> Result<(), EmissionError> {
if !self.enabled || !self.is_due() {
return Ok(());
}
self.report()
}
pub fn set_stage(
&mut self,
stage: Stage,
) -> Result<(), ConfigurationError> {
validate_stage(&stage)?;
self.stage = Some(stage);
Ok(())
}
pub fn clear_stage(&mut self) {
self.stage = None;
}
pub fn finish_unchecked(self) -> Result<Duration, TerminalError> {
self.terminal(Phase::Succeeded)
}
#[allow(clippy::result_large_err)]
pub fn finish(mut self) -> Result<Duration, FinishError> {
let elapsed = self.elapsed();
let finish_guard = self.operation_state.begin_finish();
if let Err(source) = self.validate_finish() {
finish_guard.close();
return Err(FinishError::Incomplete { elapsed, source });
}
finish_guard.close();
if !self.enabled {
return Ok(elapsed);
}
self.emit(Phase::Succeeded, self.metric_snapshots(), elapsed)
.map(|()| elapsed)
.map_err(|source| {
FinishError::Terminal(TerminalError::new(elapsed, source))
})
}
#[allow(clippy::result_large_err)]
pub fn finish_recoverable(
mut self,
) -> Result<Duration, RecoverableFinishError<'reporter>> {
let elapsed = self.elapsed();
let finish_guard = self.operation_state.begin_finish();
if let Err(source) = self.validate_finish() {
finish_guard.reopen();
return Err(RecoverableFinishError::Incomplete {
progress: self,
source,
});
}
finish_guard.close();
if !self.enabled {
return Ok(elapsed);
}
self.emit(Phase::Succeeded, self.metric_snapshots(), elapsed)
.map(|()| elapsed)
.map_err(|source| {
RecoverableFinishError::Terminal(TerminalError::new(
elapsed, source,
))
})
}
pub fn fail(self) -> Result<Duration, TerminalError> {
self.terminal(Phase::Failed)
}
pub fn cancel(self) -> Result<Duration, TerminalError> {
self.terminal(Phase::Cancelled)
}
pub fn spawn_auto_reporter<'scope, 'env>(
&'scope mut self,
scope: &'scope std::thread::Scope<'scope, 'env>,
) -> AutoReporter<'scope, 'reporter>
where
'reporter: 'scope,
{
auto_reporter::spawn(self, scope)
}
fn metric_snapshots(&self) -> Vec<MetricSnapshot> {
self.metrics.iter().map(MetricHandle::snapshot).collect()
}
fn validate_finish(&self) -> Result<(), CompletionError> {
for metric in &self.metrics {
let snapshot = metric.snapshot();
if snapshot.active() != 0 {
return Err(CompletionError::ActiveWork {
metric_id: snapshot.id().to_owned(),
active: snapshot.active(),
});
}
if let Some(total) = snapshot.total()
&& snapshot.completed() != total
{
return Err(CompletionError::IncompleteTotal {
metric_id: snapshot.id().to_owned(),
completed: snapshot.completed(),
total,
});
}
}
Ok(())
}
fn emit(
&mut self,
phase: Phase,
metrics: Vec<MetricSnapshot>,
elapsed: Duration,
) -> Result<(), EmissionError> {
let operation_id =
self.operation_id.ok_or(EmissionError::SequenceExhausted)?;
let sequence = self.next_sequence;
self.next_sequence = sequence
.checked_add(1)
.ok_or(EmissionError::SequenceExhausted)?;
let event = Event::new(
operation_id,
sequence,
phase,
self.stage.clone(),
Arc::clone(&self.attributes),
metrics,
elapsed,
);
match self.reporter.as_reporter().report(&event) {
Ok(()) => Ok(()),
Err(source) => {
Err(EmissionError::Delivery(DeliveryError::new(event, source)))
}
}
}
fn is_due(&self) -> bool {
self.interval.is_zero()
|| self
.next_due_elapsed
.is_some_and(|deadline| self.elapsed() >= deadline)
}
pub(crate) const fn report_interval(&self) -> Duration {
self.interval
}
pub(crate) fn time_until_due(&self) -> Duration {
self.next_due_elapsed
.map(|deadline| deadline.saturating_sub(self.elapsed()))
.unwrap_or(Duration::MAX)
}
fn reset_deadline(&mut self) {
self.next_due_elapsed = self.elapsed().checked_add(self.interval);
}
#[inline(never)]
fn terminal(mut self, phase: Phase) -> Result<Duration, TerminalError> {
let elapsed = self.elapsed();
let finish_guard = self.operation_state.begin_finish();
finish_guard.close();
if !self.enabled {
return Ok(elapsed);
}
self.emit(phase, self.metric_snapshots(), elapsed)
.map(|()| elapsed)
.map_err(|source| TerminalError::new(elapsed, source))
}
}
impl Drop for Progress<'_> {
#[inline(never)]
fn drop(&mut self) {
self.operation_state.close();
}
}
#[inline(never)]
fn allocate_operation_id() -> Result<u64, StartError> {
loop {
let current = NEXT_OPERATION_ID.load(Ordering::Relaxed);
if current == 0 {
return Err(StartError::OperationIdExhausted);
}
let next = current.checked_add(1).unwrap_or(0);
if NEXT_OPERATION_ID
.compare_exchange_weak(
current,
next,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok()
{
return Ok(current);
}
}
}
#[cfg(coverage)]
#[doc(hidden)]
pub fn __coverage_progress_edges() {
let previous = NEXT_OPERATION_ID.swap(0, Ordering::Relaxed);
assert!(matches!(
allocate_operation_id(),
Err(StartError::OperationIdExhausted)
));
NEXT_OPERATION_ID.store(previous.max(1), Ordering::Relaxed);
let progress = Progress::builder(&NoopReporter)
.metric(Metric::new("coverage", "Coverage"))
.start()
.expect("coverage progress must start");
progress.cancel().expect("coverage progress must cancel");
let reporter = CoverageTerminalReporter {
attempts: AtomicU64::new(0),
};
let progress = Progress::builder_arc(Arc::new(reporter))
.metric(Metric::new("coverage-terminal", "Coverage terminal"))
.start()
.expect("coverage terminal progress must start");
assert!(progress.cancel().is_err());
}