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