1use std::sync::Arc;
13use std::sync::atomic::AtomicU64;
14use std::sync::atomic::Ordering;
15use std::time::Duration;
16use std::time::Instant;
17
18use crate::Event;
19use crate::Metric;
20use crate::MetricHandle;
21use crate::MetricSnapshot;
22#[cfg(coverage)]
23use crate::NoopReporter;
24use crate::OperationAttributes;
25use crate::Phase;
26use crate::Reporter;
27use crate::Stage;
28use crate::auto_reporter;
29use crate::auto_reporter::AutoReporter;
30use crate::error::CompletionError;
31use crate::error::ConfigurationError;
32use crate::error::DeliveryError;
33use crate::error::EmissionError;
34use crate::error::FinishError;
35use crate::error::RecoverableFinishError;
36#[cfg(coverage)]
37use crate::error::ReporterError;
38use crate::error::StartError;
39use crate::error::TerminalError;
40use crate::internal::OperationState;
41use crate::validation::validate_attributes;
42use crate::validation::validate_metrics;
43use crate::validation::validate_stage;
44
45static NEXT_OPERATION_ID: AtomicU64 = AtomicU64::new(1);
47
48#[cfg(coverage)]
50struct CoverageTerminalReporter {
51 attempts: AtomicU64,
53}
54
55#[cfg(coverage)]
56impl Reporter for CoverageTerminalReporter {
57 fn report(&self, _event: &Event) -> Result<(), ReporterError> {
59 if self.attempts.fetch_add(1, Ordering::Relaxed) == 0 {
60 Ok(())
61 } else {
62 Err(ReporterError::message("coverage terminal failure"))
63 }
64 }
65}
66
67enum ReporterHandle<'reporter> {
69 Borrowed(&'reporter dyn Reporter),
70 Owned(Arc<dyn Reporter>),
71}
72
73impl ReporterHandle<'_> {
74 fn as_reporter(&self) -> &dyn Reporter {
75 match self {
76 Self::Borrowed(reporter) => *reporter,
77 Self::Owned(reporter) => reporter.as_ref(),
78 }
79 }
80}
81
82pub struct ProgressBuilder<'reporter> {
84 reporter: ReporterHandle<'reporter>,
86 interval: Duration,
88 metrics: Vec<Metric>,
90 stage: Option<Stage>,
92 attributes: OperationAttributes,
94}
95
96impl<'reporter> ProgressBuilder<'reporter> {
97 #[must_use]
99 pub const fn interval(mut self, interval: Duration) -> Self {
100 self.interval = interval;
101 self
102 }
103 #[must_use]
105 pub fn metric(mut self, metric: Metric) -> Self {
106 self.metrics.push(metric);
107 self
108 }
109 #[must_use]
111 pub fn stage(mut self, stage: Stage) -> Self {
112 self.stage = Some(stage);
113 self
114 }
115 #[must_use]
117 pub fn attribute(mut self, key: &str, value: &str) -> Self {
118 self.attributes.insert(key, value);
119 self
120 }
121 #[must_use]
123 pub fn attributes(mut self, attributes: OperationAttributes) -> Self {
124 self.attributes = attributes;
125 self
126 }
127 pub fn start(self) -> Result<Progress<'reporter>, StartError> {
130 validate_metrics(&self.metrics)?;
131 if let Some(stage) = &self.stage {
132 validate_stage(stage)?;
133 }
134 validate_attributes(&self.attributes)?;
135
136 let enabled = self.reporter.as_reporter().is_enabled();
137 let operation_state = OperationState::new();
138 let operation_id = enabled.then(allocate_operation_id).transpose()?;
139 let mut progress = Progress {
140 reporter: self.reporter,
141 enabled,
142 metrics: self
143 .metrics
144 .into_iter()
145 .map(|metric| {
146 MetricHandle::new(metric, Arc::clone(&operation_state))
147 })
148 .collect(),
149 operation_state,
150 stage: self.stage,
151 attributes: Arc::new(self.attributes),
152 interval: self.interval,
153 started_at: Instant::now(),
154 next_due_elapsed: None,
155 operation_id,
156 next_sequence: 0,
157 };
158
159 if enabled {
160 let metrics = progress.metric_snapshots();
161 progress
162 .emit(Phase::Started, metrics, Duration::ZERO)
163 .map_err(StartError::from)?;
164 progress.next_due_elapsed = Some(progress.interval);
165 progress.started_at = Instant::now();
166 }
167 Ok(progress)
168 }
169}
170
171#[must_use]
176pub struct Progress<'reporter> {
177 reporter: ReporterHandle<'reporter>,
179 enabled: bool,
181 metrics: Vec<MetricHandle>,
183 operation_state: Arc<OperationState>,
185 stage: Option<Stage>,
187 attributes: Arc<OperationAttributes>,
189 interval: Duration,
191 started_at: Instant,
193 next_due_elapsed: Option<Duration>,
195 operation_id: Option<u64>,
197 next_sequence: u64,
199}
200
201impl<'reporter> Progress<'reporter> {
202 #[must_use]
204 pub fn builder(
205 reporter: &'reporter dyn Reporter,
206 ) -> ProgressBuilder<'reporter> {
207 ProgressBuilder {
208 reporter: ReporterHandle::Borrowed(reporter),
209 interval: Duration::ZERO,
210 metrics: Vec::new(),
211 stage: None,
212 attributes: OperationAttributes::new(),
213 }
214 }
215 #[must_use]
217 pub fn builder_arc(
218 reporter: Arc<dyn Reporter>,
219 ) -> ProgressBuilder<'static> {
220 ProgressBuilder {
221 reporter: ReporterHandle::Owned(reporter),
222 interval: Duration::ZERO,
223 metrics: Vec::new(),
224 stage: None,
225 attributes: OperationAttributes::new(),
226 }
227 }
228 #[must_use]
230 pub const fn is_enabled(&self) -> bool {
231 self.enabled
232 }
233 #[must_use]
235 pub fn elapsed(&self) -> Duration {
236 self.started_at.elapsed()
237 }
238 pub fn metric(&self, metric_id: &str) -> Option<MetricHandle> {
240 self.metrics
241 .iter()
242 .find(|metric| metric.id() == metric_id)
243 .cloned()
244 }
245 pub fn report(&mut self) -> Result<(), EmissionError> {
247 if !self.enabled {
248 return Ok(());
249 }
250 let metrics = self.metric_snapshots();
251 let elapsed = self.elapsed();
252 let result = self.emit(Phase::Running, metrics, elapsed);
253 self.reset_deadline();
254 result
255 }
256 pub fn report_if_due(&mut self) -> Result<(), EmissionError> {
258 if !self.enabled || !self.is_due() {
259 return Ok(());
260 }
261 self.report()
262 }
263 pub fn set_stage(
265 &mut self,
266 stage: Stage,
267 ) -> Result<(), ConfigurationError> {
268 validate_stage(&stage)?;
269 self.stage = Some(stage);
270 Ok(())
271 }
272 pub fn clear_stage(&mut self) {
274 self.stage = None;
275 }
276 pub fn finish_unchecked(self) -> Result<Duration, TerminalError> {
279 self.terminal(Phase::Succeeded)
280 }
281 #[allow(clippy::result_large_err)]
284 pub fn finish(mut self) -> Result<Duration, FinishError> {
285 let elapsed = self.elapsed();
286 let finish_guard = self.operation_state.begin_finish();
287 if let Err(source) = self.validate_finish() {
288 finish_guard.close();
289 return Err(FinishError::Incomplete { elapsed, source });
290 }
291 finish_guard.close();
292 if !self.enabled {
293 return Ok(elapsed);
294 }
295 self.emit(Phase::Succeeded, self.metric_snapshots(), elapsed)
296 .map(|()| elapsed)
297 .map_err(|source| {
298 FinishError::Terminal(TerminalError::new(elapsed, source))
299 })
300 }
301 #[allow(clippy::result_large_err)]
304 pub fn finish_recoverable(
305 mut self,
306 ) -> Result<Duration, RecoverableFinishError<'reporter>> {
307 let elapsed = self.elapsed();
308 let finish_guard = self.operation_state.begin_finish();
309 if let Err(source) = self.validate_finish() {
310 finish_guard.reopen();
311 return Err(RecoverableFinishError::Incomplete {
312 progress: self,
313 source,
314 });
315 }
316 finish_guard.close();
317 if !self.enabled {
318 return Ok(elapsed);
319 }
320 self.emit(Phase::Succeeded, self.metric_snapshots(), elapsed)
321 .map(|()| elapsed)
322 .map_err(|source| {
323 RecoverableFinishError::Terminal(TerminalError::new(
324 elapsed, source,
325 ))
326 })
327 }
328 pub fn fail(self) -> Result<Duration, TerminalError> {
330 self.terminal(Phase::Failed)
331 }
332 pub fn cancel(self) -> Result<Duration, TerminalError> {
334 self.terminal(Phase::Cancelled)
335 }
336 pub fn spawn_auto_reporter<'scope, 'env>(
339 &'scope mut self,
340 scope: &'scope std::thread::Scope<'scope, 'env>,
341 ) -> AutoReporter<'scope, 'reporter>
342 where
343 'reporter: 'scope,
344 {
345 auto_reporter::spawn(self, scope)
346 }
347 fn metric_snapshots(&self) -> Vec<MetricSnapshot> {
349 self.metrics.iter().map(MetricHandle::snapshot).collect()
350 }
351 fn validate_finish(&self) -> Result<(), CompletionError> {
353 for metric in &self.metrics {
354 let snapshot = metric.snapshot();
355 if snapshot.active() != 0 {
356 return Err(CompletionError::ActiveWork {
357 metric_id: snapshot.id().to_owned(),
358 active: snapshot.active(),
359 });
360 }
361 if let Some(total) = snapshot.total()
362 && snapshot.completed() != total
363 {
364 return Err(CompletionError::IncompleteTotal {
365 metric_id: snapshot.id().to_owned(),
366 completed: snapshot.completed(),
367 total,
368 });
369 }
370 }
371 Ok(())
372 }
373 fn emit(
375 &mut self,
376 phase: Phase,
377 metrics: Vec<MetricSnapshot>,
378 elapsed: Duration,
379 ) -> Result<(), EmissionError> {
380 let operation_id =
381 self.operation_id.ok_or(EmissionError::SequenceExhausted)?;
382 let sequence = self.next_sequence;
383 self.next_sequence = sequence
384 .checked_add(1)
385 .ok_or(EmissionError::SequenceExhausted)?;
386 let event = Event::new(
387 operation_id,
388 sequence,
389 phase,
390 self.stage.clone(),
391 Arc::clone(&self.attributes),
392 metrics,
393 elapsed,
394 );
395 match self.reporter.as_reporter().report(&event) {
396 Ok(()) => Ok(()),
397 Err(source) => {
398 Err(EmissionError::Delivery(DeliveryError::new(event, source)))
399 }
400 }
401 }
402 fn is_due(&self) -> bool {
404 self.interval.is_zero()
405 || self
406 .next_due_elapsed
407 .is_some_and(|deadline| self.elapsed() >= deadline)
408 }
409 pub(crate) const fn report_interval(&self) -> Duration {
411 self.interval
412 }
413 pub(crate) fn time_until_due(&self) -> Duration {
415 self.next_due_elapsed
416 .map(|deadline| deadline.saturating_sub(self.elapsed()))
417 .unwrap_or(Duration::MAX)
418 }
419 fn reset_deadline(&mut self) {
421 self.next_due_elapsed = self.elapsed().checked_add(self.interval);
422 }
423 #[inline(never)]
425 fn terminal(mut self, phase: Phase) -> Result<Duration, TerminalError> {
426 let elapsed = self.elapsed();
427 let finish_guard = self.operation_state.begin_finish();
428 finish_guard.close();
429 if !self.enabled {
430 return Ok(elapsed);
431 }
432 self.emit(phase, self.metric_snapshots(), elapsed)
433 .map(|()| elapsed)
434 .map_err(|source| TerminalError::new(elapsed, source))
435 }
436}
437
438impl Drop for Progress<'_> {
439 #[inline(never)]
441 fn drop(&mut self) {
442 self.operation_state.close();
443 }
444}
445
446#[inline(never)]
448fn allocate_operation_id() -> Result<u64, StartError> {
449 loop {
450 let current = NEXT_OPERATION_ID.load(Ordering::Relaxed);
451 if current == 0 {
452 return Err(StartError::OperationIdExhausted);
453 }
454 let next = current.checked_add(1).unwrap_or(0);
455 if NEXT_OPERATION_ID
456 .compare_exchange_weak(
457 current,
458 next,
459 Ordering::Relaxed,
460 Ordering::Relaxed,
461 )
462 .is_ok()
463 {
464 return Ok(current);
465 }
466 }
467}
468
469#[cfg(coverage)]
471#[doc(hidden)]
472pub fn __coverage_progress_edges() {
473 let previous = NEXT_OPERATION_ID.swap(0, Ordering::Relaxed);
474 assert!(matches!(
475 allocate_operation_id(),
476 Err(StartError::OperationIdExhausted)
477 ));
478 NEXT_OPERATION_ID.store(previous.max(1), Ordering::Relaxed);
479
480 let progress = Progress::builder(&NoopReporter)
481 .metric(Metric::new("coverage", "Coverage"))
482 .start()
483 .expect("coverage progress must start");
484 progress.cancel().expect("coverage progress must cancel");
485
486 let reporter = CoverageTerminalReporter {
487 attempts: AtomicU64::new(0),
488 };
489 let progress = Progress::builder_arc(Arc::new(reporter))
490 .metric(Metric::new("coverage-terminal", "Coverage terminal"))
491 .start()
492 .expect("coverage terminal progress must start");
493 assert!(progress.cancel().is_err());
494}