reliar-outbox 0.6.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; `reliar-outbox`'s own
/// `test-support` feature ships [`crate::RecordingMetrics`], a fake that remembers every call —
/// here it stands in for a real exporter:
#[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
#[cfg_attr(feature = "test-support", doc = "```")]
/// # use reliar_outbox::{OutboxMetrics, RecordingMetrics};
/// let metrics = RecordingMetrics::default();
/// OutboxMetrics::claimed(&metrics, 3);
/// assert_eq!(metrics.claimed(), 3);
/// ```
pub trait OutboxMetrics: Send + Sync {
    /// Called once per `acquire` with the number of rows claimed (which may be zero).
    ///
    #[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
    #[cfg_attr(feature = "test-support", doc = "```")]
    /// # use reliar_outbox::{OutboxMetrics, RecordingMetrics};
    /// let metrics = RecordingMetrics::default();
    /// OutboxMetrics::claimed(&metrics, 3);
    /// assert_eq!(metrics.claimed(), 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.
    ///
    #[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
    #[cfg_attr(feature = "test-support", doc = "```")]
    /// # use reliar_outbox::{DeadReason, OutboxMetrics, RecordingMetrics};
    /// let metrics = RecordingMetrics::default();
    /// OutboxMetrics::dead(&metrics, 1, DeadReason::AttemptsExhausted);
    /// assert_eq!(metrics.dead(), 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.
    ///
    #[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
    #[cfg_attr(feature = "test-support", doc = "```")]
    /// # use reliar_outbox::{OutboxMetrics, RecordingMetrics};
    /// let metrics = RecordingMetrics::default();
    /// OutboxMetrics::pending(&metrics, 42);
    /// assert_eq!(metrics.pending(), 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`].
    ///
    #[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
    #[cfg_attr(feature = "test-support", doc = "```")]
    /// # use reliar_outbox::{OutboxMetrics, RecordingMetrics};
    /// let metrics = RecordingMetrics::default();
    /// OutboxMetrics::expired_pending(&metrics, 0);
    /// assert_eq!(metrics.expired_pending(), 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 {}