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