reliar-outbox 0.9.0

Storage-agnostic transactional outbox: OutboxStore/Publisher contracts, retry policy, settings and dispatcher (no storage or transport dependency).
Documentation
//! Static-dispatch metrics hook.

use std::time::Duration;

use reliar_core::{FailureKind, MessageType};

use crate::store::DeadReason;

/// A metrics hook with no-op defaults, so it costs nothing unused and adding an instrument
/// later is not a breaking change. No library crate depends on a metrics exporter — the host
/// wires one behind an implementation of this trait.
///
/// Labels are bounded to `message_type`, `kind`, `reason` and similar low-cardinality values.
/// `message_id`, `correlation_id`, `tenant_id`, `worker_id` and `last_error` **SHALL NEVER** be
/// metric labels.
///
/// A host implements the trait over whatever exporter it uses — here a tiny local counter stands
/// in for one, which documents what a host actually has to write:
///
/// ```
/// use reliar_outbox::OutboxMetrics;
/// use std::sync::atomic::{AtomicUsize, Ordering};
///
/// #[derive(Default)]
/// struct CountingMetrics {
///     claimed: AtomicUsize,
/// }
///
/// impl OutboxMetrics for CountingMetrics {
///     fn claimed(&self, n: usize) {
///         self.claimed.fetch_add(n, Ordering::Relaxed);
///     }
/// }
///
/// let metrics = CountingMetrics::default();
/// OutboxMetrics::claimed(&metrics, 3);
/// assert_eq!(metrics.claimed.load(Ordering::Relaxed), 3);
/// ```
pub trait OutboxMetrics: Send + Sync {
    /// Called once per `acquire` with the number of rows claimed (which may be zero).
    ///
    /// ```
    /// use reliar_outbox::OutboxMetrics;
    /// use std::sync::atomic::{AtomicUsize, Ordering};
    ///
    /// #[derive(Default)]
    /// struct CountingMetrics {
    ///     claimed: AtomicUsize,
    /// }
    ///
    /// impl OutboxMetrics for CountingMetrics {
    ///     fn claimed(&self, n: usize) {
    ///         self.claimed.fetch_add(n, Ordering::Relaxed);
    ///     }
    /// }
    ///
    /// let metrics = CountingMetrics::default();
    /// OutboxMetrics::claimed(&metrics, 3);
    /// assert_eq!(metrics.claimed.load(Ordering::Relaxed), 3);
    /// ```
    fn claimed(&self, _n: usize) {}
    /// Called once per publish outcome that succeeded.
    ///
    /// ```
    /// use reliar_core::MessageType;
    /// use reliar_outbox::{NoopMetrics, OutboxMetrics};
    /// NoopMetrics.published(1, &MessageType::new("orders.created", 1));
    /// ```
    fn published(&self, _n: usize, _message_type: &MessageType) {}
    /// Called once per publish outcome resolved as [`crate::FailureOutcome::Retry`].
    ///
    /// ```
    /// use reliar_core::FailureKind;
    /// use reliar_outbox::{NoopMetrics, OutboxMetrics};
    /// NoopMetrics.retried(1, FailureKind::Transient);
    /// ```
    fn retried(&self, _n: usize, _kind: FailureKind) {}
    /// Called once per row moved to dead.
    ///
    /// ```
    /// use reliar_outbox::{DeadReason, OutboxMetrics};
    /// use std::sync::Mutex;
    ///
    /// #[derive(Default)]
    /// struct CountingMetrics {
    ///     dead: Mutex<Vec<DeadReason>>,
    /// }
    ///
    /// impl OutboxMetrics for CountingMetrics {
    ///     fn dead(&self, n: usize, reason: DeadReason) {
    ///         self.dead.lock().unwrap().extend(std::iter::repeat_n(reason, n));
    ///     }
    /// }
    ///
    /// let metrics = CountingMetrics::default();
    /// OutboxMetrics::dead(&metrics, 1, DeadReason::AttemptsExhausted);
    /// assert_eq!(*metrics.dead.lock().unwrap(), vec![DeadReason::AttemptsExhausted]);
    /// ```
    fn dead(&self, _n: usize, _reason: DeadReason) {}
    /// Called once per publish attempt with its wall-clock duration.
    ///
    /// ```
    /// use reliar_core::MessageType;
    /// use reliar_outbox::{NoopMetrics, OutboxMetrics};
    /// use std::time::Duration;
    /// NoopMetrics.publish_duration(Duration::from_millis(12), &MessageType::new("orders.created", 1));
    /// ```
    fn publish_duration(&self, _d: Duration, _message_type: &MessageType) {}
    /// The claimable backlog, from the last [`crate::OutboxStore::stats`] poll.
    ///
    /// ```
    /// use reliar_outbox::OutboxMetrics;
    /// use std::sync::Mutex;
    ///
    /// #[derive(Default)]
    /// struct CountingMetrics {
    ///     pending: Mutex<Option<u64>>,
    /// }
    ///
    /// impl OutboxMetrics for CountingMetrics {
    ///     fn pending(&self, n: u64) {
    ///         *self.pending.lock().unwrap() = Some(n);
    ///     }
    /// }
    ///
    /// let metrics = CountingMetrics::default();
    /// OutboxMetrics::pending(&metrics, 42);
    /// assert_eq!(*metrics.pending.lock().unwrap(), Some(42));
    /// ```
    fn pending(&self, _n: u64) {}
    /// Pending rows past `expires_at` awaiting the next purge — counted separately so they can
    /// be alerted on without pinning [`Self::oldest_pending_age`].
    ///
    /// ```
    /// use reliar_outbox::OutboxMetrics;
    /// use std::sync::Mutex;
    ///
    /// #[derive(Default)]
    /// struct CountingMetrics {
    ///     expired_pending: Mutex<Option<u64>>,
    /// }
    ///
    /// impl OutboxMetrics for CountingMetrics {
    ///     fn expired_pending(&self, n: u64) {
    ///         *self.expired_pending.lock().unwrap() = Some(n);
    ///     }
    /// }
    ///
    /// let metrics = CountingMetrics::default();
    /// OutboxMetrics::expired_pending(&metrics, 0);
    /// assert_eq!(*metrics.expired_pending.lock().unwrap(), Some(0));
    /// ```
    fn expired_pending(&self, _n: u64) {}
    /// The outbox-lag alerting signal: age of the oldest claimable row, from
    /// [`crate::OutboxStats::lag`]. **Not called when the backlog is empty** — there is no
    /// oldest pending row to report an age for, and calling this with a made-up zero would read
    /// as "no lag" rather than "no data."
    ///
    /// ```
    /// use reliar_outbox::{NoopMetrics, OutboxMetrics};
    /// use std::time::Duration;
    /// NoopMetrics.oldest_pending_age(Duration::from_secs(5));
    /// ```
    fn oldest_pending_age(&self, _age: Duration) {}
}

/// The default [`OutboxMetrics`]: every hook is a no-op.
///
/// ```
/// use reliar_outbox::{NoopMetrics, OutboxMetrics};
/// // Costs nothing when a host never wires a real exporter.
/// NoopMetrics.claimed(1);
/// ```
#[derive(Clone, Copy, Debug, Default)]
pub struct NoopMetrics;

impl OutboxMetrics for NoopMetrics {}