Skip to main content

qubit_progress/
metric.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Metric configuration and immutable metric snapshots.
9// qubit-style: allow multiple-public-types
10
11use std::{
12    hint::spin_loop,
13    sync::{
14        Arc,
15        atomic::{
16            AtomicU64,
17            Ordering,
18        },
19    },
20    thread,
21};
22
23use qubit_fast_cas::CasCell;
24
25#[cfg(feature = "serde")]
26use serde::{
27    Deserialize,
28    Deserializer,
29};
30
31#[cfg(feature = "serde")]
32use crate::validation::{
33    validate_metrics,
34    validate_snapshot_counts,
35};
36use crate::{
37    MetricError,
38    internal::OperationState,
39};
40
41/// Stable metadata for one metric in a progress operation.
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct Metric {
44    /// Machine-readable identifier.
45    pub(crate) id: Arc<str>,
46    /// Human-readable name.
47    pub(crate) name: Arc<str>,
48    /// Optional configured total.
49    pub(crate) total: Option<u64>,
50}
51
52impl Metric {
53    /// Creates metric metadata without a known total.
54    ///
55    /// The ID and name are validated when the enclosing progress operation is
56    /// started, so this constructor never panics.
57    #[must_use]
58    pub fn new(id: &str, name: &str) -> Self {
59        Self {
60            id: Arc::from(id),
61            name: Arc::from(name),
62            total: None,
63        }
64    }
65
66    /// Records the total work for this metric.
67    ///
68    /// The value is carried automatically by all future events from the
69    /// operation that owns this metric.
70    #[must_use]
71    pub const fn total(mut self, total: u64) -> Self {
72        self.total = Some(total);
73        self
74    }
75
76    /// Returns the metric's stable ID.
77    #[must_use]
78    pub fn id(&self) -> &str {
79        &self.id
80    }
81
82    /// Returns the metric's display name.
83    #[must_use]
84    pub fn name(&self) -> &str {
85        &self.name
86    }
87
88    /// Returns the configured total, if it is known.
89    #[must_use]
90    pub const fn configured_total(&self) -> Option<u64> {
91        self.total
92    }
93}
94
95/// One atomic batch of additive metric lifecycle changes.
96#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
97pub struct MetricDelta {
98    /// Work moving from not-started to active.
99    started: u64,
100    /// Active work becoming completed without an outcome classification.
101    unclassified: u64,
102    /// Active work becoming successful.
103    succeeded: u64,
104    /// Active work becoming failed.
105    failed: u64,
106    /// Active work becoming cancelled.
107    cancelled: u64,
108}
109
110impl MetricDelta {
111    /// Creates a zero delta.
112    #[must_use]
113    pub const fn new() -> Self {
114        Self {
115            started: 0,
116            unclassified: 0,
117            succeeded: 0,
118            failed: 0,
119            cancelled: 0,
120        }
121    }
122
123    /// Sets the number of work items entering the active state.
124    #[must_use]
125    pub const fn started(mut self, count: u64) -> Self {
126        self.started = count;
127        self
128    }
129
130    /// Sets the number of work items completing without classification.
131    #[must_use]
132    pub const fn unclassified(mut self, count: u64) -> Self {
133        self.unclassified = count;
134        self
135    }
136
137    /// Sets the number of work items completing successfully.
138    #[must_use]
139    pub const fn succeeded(mut self, count: u64) -> Self {
140        self.succeeded = count;
141        self
142    }
143
144    /// Sets the number of work items completing with failure.
145    #[must_use]
146    pub const fn failed(mut self, count: u64) -> Self {
147        self.failed = count;
148        self
149    }
150
151    /// Sets the number of work items completing by cancellation.
152    #[must_use]
153    pub const fn cancelled(mut self, count: u64) -> Self {
154        self.cancelled = count;
155        self
156    }
157}
158
159/// Cloneable capability for one live metric owned by a progress operation.
160///
161/// All mutation methods are serialized by one CAS gate critical section.
162/// A handle remains readable after its progress operation closes, but rejects
163/// all later mutations.
164#[derive(Clone)]
165pub struct MetricHandle {
166    /// Shared metadata and mutable count state.
167    inner: Arc<MetricInner>,
168    /// Shared lifecycle gate owned by the enclosing progress operation.
169    operation_state: Arc<OperationState>,
170}
171
172impl MetricHandle {
173    /// Creates one live handle from validated metric metadata.
174    pub(crate) fn new(
175        metric: Metric,
176        operation_state: Arc<OperationState>,
177    ) -> Self {
178        Self {
179            inner: Arc::new(MetricInner::new(metric)),
180            operation_state,
181        }
182    }
183
184    /// Returns the stable metric ID.
185    #[must_use]
186    pub fn id(&self) -> &str {
187        self.inner.metric.id()
188    }
189
190    /// Returns the stable metric display name.
191    #[must_use]
192    pub fn name(&self) -> &str {
193        self.inner.metric.name()
194    }
195
196    /// Moves work from the not-started state to the active state.
197    ///
198    /// # Errors
199    ///
200    /// Returns a metric error when the transition violates aggregate state
201    /// invariants or when the owning operation is closed.
202    pub fn start(&self, count: u64) -> Result<(), MetricError> {
203        self.apply_delta(MetricDelta::new().started(count))
204    }
205
206    /// Moves work from the active state to unclassified completion.
207    ///
208    /// # Errors
209    ///
210    /// Returns a metric error when the transition violates aggregate state
211    /// invariants or when the owning operation is closed.
212    pub fn complete(&self, count: u64) -> Result<(), MetricError> {
213        self.apply_delta(MetricDelta::new().unclassified(count))
214    }
215
216    /// Moves work from the active state to the succeeded state.
217    ///
218    /// # Errors
219    ///
220    /// Returns a metric error when the transition violates aggregate state
221    /// invariants or when the owning operation is closed.
222    pub fn succeed(&self, count: u64) -> Result<(), MetricError> {
223        self.apply_delta(MetricDelta::new().succeeded(count))
224    }
225
226    /// Moves work from the active state to the failed state.
227    ///
228    /// # Errors
229    ///
230    /// Returns a metric error when the transition violates aggregate state
231    /// invariants or when the owning operation is closed.
232    pub fn fail(&self, count: u64) -> Result<(), MetricError> {
233        self.apply_delta(MetricDelta::new().failed(count))
234    }
235
236    /// Moves work from the active state to the cancelled state.
237    ///
238    /// # Errors
239    ///
240    /// Returns a metric error when the transition violates aggregate state
241    /// invariants or when the owning operation is closed.
242    pub fn cancel(&self, count: u64) -> Result<(), MetricError> {
243        self.apply_delta(MetricDelta::new().cancelled(count))
244    }
245
246    /// Applies one atomic additive batch of lifecycle changes.
247    ///
248    /// # Errors
249    ///
250    /// Returns a metric error when the delta exceeds active work, violates a
251    /// configured total, overflows, or the owning operation is closed. The
252    /// metric is unchanged whenever an error is returned.
253    pub fn apply_delta(&self, delta: MetricDelta) -> Result<(), MetricError> {
254        let metric_id = self.id();
255        let total = self.inner.metric.configured_total();
256        let _update_guard = self.operation_state.enter_update(metric_id)?;
257
258        self.inner.with_update(|counts| {
259            let mut next = *counts;
260            apply_delta_to_counts(&mut next, delta, metric_id)?;
261            next.validate(metric_id, total)?;
262            *counts = next;
263            Ok(())
264        })
265    }
266
267    /// Returns one internally consistent immutable metric snapshot.
268    ///
269    /// This read remains available after the owning operation closes.
270    #[must_use]
271    pub fn snapshot(&self) -> MetricSnapshot {
272        let counts = self.inner.snapshot_counts();
273        MetricSnapshot::from_counts(&self.inner.metric, counts)
274    }
275}
276
277/// Immutable metadata and atomic dynamic state for one handle.
278struct MetricInner {
279    /// Fixed metric definition supplied to the progress builder.
280    metric: Metric,
281    /// Dynamic updates are serialized by this gate.
282    gate: CasCell,
283    /// Work that has started but is not terminal.
284    active: AtomicU64,
285    /// Terminal work without explicit success, failure, or cancellation.
286    completed_unclassified: AtomicU64,
287    /// Terminal work classified as successful.
288    succeeded: AtomicU64,
289    /// Terminal work classified as failed.
290    failed: AtomicU64,
291    /// Terminal work classified as cancelled.
292    cancelled: AtomicU64,
293}
294
295impl MetricInner {
296    /// Builds one live metric inner state with zeroed counters.
297    fn new(metric: Metric) -> Self {
298        Self {
299            metric,
300            gate: CasCell::new(0),
301            active: AtomicU64::new(0),
302            completed_unclassified: AtomicU64::new(0),
303            succeeded: AtomicU64::new(0),
304            failed: AtomicU64::new(0),
305            cancelled: AtomicU64::new(0),
306        }
307    }
308
309    /// Runs one validated update while exclusively holding the gate.
310    fn with_update<R, F>(&self, mut update: F) -> Result<R, MetricError>
311    where
312        F: FnMut(&mut MetricCounts) -> Result<R, MetricError>,
313    {
314        let mut attempts = 0;
315        loop {
316            let version = self.gate.load();
317            if version & 1 != 0 {
318                wait_for_contention(attempts);
319                attempts += 1;
320                continue;
321            }
322
323            match self.gate.compare_set(version, version.wrapping_add(1)) {
324                Ok(()) => {
325                    let _guard = MetricGateGuard::new(
326                        &self.gate,
327                        version.wrapping_add(2),
328                    );
329                    let mut counts = self.read_counts();
330                    let result = update(&mut counts);
331                    if result.is_ok() {
332                        self.write_counts(&counts);
333                    }
334                    return result;
335                }
336                Err(_) => {
337                    wait_for_contention(attempts);
338                    attempts += 1;
339                }
340            }
341        }
342    }
343
344    /// Reads all counter fields with acquire order and copies them by value.
345    fn read_counts(&self) -> MetricCounts {
346        MetricCounts {
347            active: self.active.load(Ordering::Acquire),
348            completed_unclassified: self
349                .completed_unclassified
350                .load(Ordering::Acquire),
351            succeeded: self.succeeded.load(Ordering::Acquire),
352            failed: self.failed.load(Ordering::Acquire),
353            cancelled: self.cancelled.load(Ordering::Acquire),
354        }
355    }
356
357    /// Writes all counter fields after successful validation.
358    fn write_counts(&self, counts: &MetricCounts) {
359        self.active.store(counts.active, Ordering::Release);
360        self.completed_unclassified
361            .store(counts.completed_unclassified, Ordering::Release);
362        self.succeeded.store(counts.succeeded, Ordering::Release);
363        self.failed.store(counts.failed, Ordering::Release);
364        self.cancelled.store(counts.cancelled, Ordering::Release);
365    }
366
367    /// Repeatedly reads counts and validates version stability.
368    fn snapshot_counts(&self) -> MetricCounts {
369        let mut attempts = 0;
370        loop {
371            let start = self.gate.load();
372            if start & 1 != 0 {
373                wait_for_contention(attempts);
374                attempts += 1;
375                continue;
376            }
377
378            let counts = self.read_counts();
379            if start == self.gate.load() {
380                return counts;
381            }
382
383            wait_for_contention(attempts);
384            attempts += 1;
385        }
386    }
387}
388
389/// Dynamic metric counts for a CAS transaction.
390#[derive(Clone, Copy)]
391struct MetricCounts {
392    /// Work that has started but is not terminal.
393    active: u64,
394    /// Terminal work without explicit success, failure, or cancellation.
395    completed_unclassified: u64,
396    /// Terminal work classified as successful.
397    succeeded: u64,
398    /// Terminal work classified as failed.
399    failed: u64,
400    /// Terminal work classified as cancelled.
401    cancelled: u64,
402}
403
404impl MetricCounts {
405    /// Returns the derived public completed count.
406    ///
407    /// Every transition conserves the total count, which cannot exceed `u64`.
408    fn completed(self) -> Option<u64> {
409        self.completed_unclassified
410            .checked_add(self.succeeded)?
411            .checked_add(self.failed)?
412            .checked_add(self.cancelled)
413    }
414
415    /// Returns active plus completed work.
416    fn occupied(self) -> Option<u64> {
417        self.completed()?.checked_add(self.active)
418    }
419
420    /// Validates the aggregate conservation invariants for one pending state.
421    fn validate(
422        self,
423        metric_id: &str,
424        total: Option<u64>,
425    ) -> Result<(), MetricError> {
426        let occupied =
427            self.occupied().ok_or_else(|| MetricError::CountOverflow {
428                metric_id: metric_id.into(),
429            })?;
430        if let Some(total) = total
431            && occupied > total
432        {
433            return Err(MetricError::TotalExceeded {
434                metric_id: metric_id.into(),
435                total,
436                attempted: occupied,
437            });
438        }
439        Ok(())
440    }
441}
442
443/// Applies one validated additive delta to dynamic metric counts.
444fn apply_delta_to_counts(
445    counts: &mut MetricCounts,
446    delta: MetricDelta,
447    metric_id: &str,
448) -> Result<(), MetricError> {
449    let terminal_delta = delta
450        .unclassified
451        .checked_add(delta.succeeded)
452        .and_then(|value| value.checked_add(delta.failed))
453        .and_then(|value| value.checked_add(delta.cancelled))
454        .ok_or_else(|| MetricError::CountOverflow {
455            metric_id: metric_id.into(),
456        })?;
457    let available_active = counts
458        .active
459        .checked_add(delta.started)
460        .ok_or_else(|| MetricError::CountOverflow {
461            metric_id: metric_id.into(),
462        })?;
463    if terminal_delta > available_active {
464        return Err(MetricError::InsufficientActive {
465            metric_id: metric_id.into(),
466            requested: terminal_delta,
467            available: available_active,
468        });
469    }
470
471    counts.active = available_active - terminal_delta;
472    counts.completed_unclassified = counts
473        .completed_unclassified
474        .checked_add(delta.unclassified)
475        .ok_or_else(|| MetricError::CountOverflow {
476            metric_id: metric_id.into(),
477        })?;
478    counts.succeeded = counts
479        .succeeded
480        .checked_add(delta.succeeded)
481        .ok_or_else(|| MetricError::CountOverflow {
482            metric_id: metric_id.into(),
483        })?;
484    counts.failed =
485        counts.failed.checked_add(delta.failed).ok_or_else(|| {
486            MetricError::CountOverflow {
487                metric_id: metric_id.into(),
488            }
489        })?;
490    counts.cancelled = counts
491        .cancelled
492        .checked_add(delta.cancelled)
493        .ok_or_else(|| MetricError::CountOverflow {
494            metric_id: metric_id.into(),
495        })?;
496    Ok(())
497}
498
499/// RAII wrapper that always releases a locked gate.
500struct MetricGateGuard<'gate> {
501    gate: &'gate CasCell,
502    next_version: u64,
503}
504
505impl<'gate> MetricGateGuard<'gate> {
506    fn new(gate: &'gate CasCell, next_version: u64) -> Self {
507        Self { gate, next_version }
508    }
509}
510
511impl Drop for MetricGateGuard<'_> {
512    fn drop(&mut self) {
513        self.gate.store(self.next_version);
514    }
515}
516
517/// Immutable complete state for one metric in an emitted event.
518#[cfg_attr(feature = "serde", derive(serde::Serialize))]
519#[derive(Clone, Debug, Eq, PartialEq)]
520pub struct MetricSnapshot {
521    /// Machine-readable metric ID.
522    id: Arc<str>,
523    /// Human-readable metric name.
524    name: Arc<str>,
525    /// Configured total, if known.
526    total: Option<u64>,
527    /// Completed count.
528    completed: u64,
529    /// Active count.
530    active: u64,
531    /// Succeeded count.
532    succeeded: u64,
533    /// Failed count.
534    failed: u64,
535    /// Cancelled count.
536    cancelled: u64,
537}
538
539impl MetricSnapshot {
540    /// Builds an immutable snapshot from one internally validated metric state.
541    fn from_counts(metric: &Metric, counts: MetricCounts) -> Self {
542        Self {
543            id: Arc::clone(&metric.id),
544            name: Arc::clone(&metric.name),
545            total: metric.total,
546            completed: counts
547                .completed()
548                .expect("validated metric counts must fit in u64"),
549            active: counts.active,
550            succeeded: counts.succeeded,
551            failed: counts.failed,
552            cancelled: counts.cancelled,
553        }
554    }
555    /// Returns the metric's stable ID.
556    #[must_use]
557    pub fn id(&self) -> &str {
558        &self.id
559    }
560    /// Returns the metric's display name.
561    #[must_use]
562    pub fn name(&self) -> &str {
563        &self.name
564    }
565    /// Returns the total configured for this event's metric.
566    #[must_use]
567    pub const fn total(&self) -> Option<u64> {
568        self.total
569    }
570    /// Returns the number of completed work items.
571    #[must_use]
572    pub const fn completed(&self) -> u64 {
573        self.completed
574    }
575    /// Returns completed work without an explicit outcome classification.
576    #[must_use]
577    pub const fn unclassified(&self) -> u64 {
578        let classified = self
579            .succeeded
580            .saturating_add(self.failed)
581            .saturating_add(self.cancelled);
582        self.completed.saturating_sub(classified)
583    }
584    /// Returns the number of active work items.
585    #[must_use]
586    pub const fn active(&self) -> u64 {
587        self.active
588    }
589    /// Returns the number of explicitly successful work items.
590    #[must_use]
591    pub const fn succeeded(&self) -> u64 {
592        self.succeeded
593    }
594    /// Returns the number of explicitly failed work items.
595    #[must_use]
596    pub const fn failed(&self) -> u64 {
597        self.failed
598    }
599    /// Returns the number of explicitly cancelled work items.
600    #[must_use]
601    pub const fn cancelled(&self) -> u64 {
602        self.cancelled
603    }
604    /// Returns the completed fraction when the total is positive and known.
605    #[must_use]
606    pub fn completion_fraction(&self) -> Option<f64> {
607        self.total
608            .filter(|total| *total > 0)
609            .map(|total| self.completed as f64 / total as f64)
610    }
611}
612
613/// Serializable wire representation used to validate standalone snapshots.
614#[cfg(feature = "serde")]
615#[derive(Deserialize)]
616struct MetricSnapshotWire {
617    /// Machine-readable metric ID.
618    id: Arc<str>,
619    /// Human-readable metric name.
620    name: Arc<str>,
621    /// Configured total, if known.
622    total: Option<u64>,
623    /// Completed count.
624    completed: u64,
625    /// Active count.
626    active: u64,
627    /// Succeeded count.
628    succeeded: u64,
629    /// Failed count.
630    failed: u64,
631    /// Cancelled count.
632    cancelled: u64,
633}
634
635#[cfg(feature = "serde")]
636impl<'de> Deserialize<'de> for MetricSnapshot {
637    /// Deserializes and validates one standalone metric snapshot.
638    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
639    where
640        D: Deserializer<'de>,
641    {
642        let wire = MetricSnapshotWire::deserialize(deserializer)?;
643        let snapshot = Self {
644            id: wire.id,
645            name: wire.name,
646            total: wire.total,
647            completed: wire.completed,
648            active: wire.active,
649            succeeded: wire.succeeded,
650            failed: wire.failed,
651            cancelled: wire.cancelled,
652        };
653        let metric = Metric {
654            id: Arc::clone(&snapshot.id),
655            name: Arc::clone(&snapshot.name),
656            total: snapshot.total,
657        };
658        validate_metrics(std::slice::from_ref(&metric))
659            .map_err(serde::de::Error::custom)?;
660        validate_snapshot_counts(&snapshot)
661            .map_err(serde::de::Error::custom)?;
662        Ok(snapshot)
663    }
664}
665
666/// Busy-wait helper for writer contention and snapshot retries.
667#[inline]
668fn wait_for_contention(attempts: usize) {
669    if attempts > 0 && attempts.is_multiple_of(16) {
670        thread::yield_now();
671    } else {
672        spin_loop();
673    }
674}