rust-job-queue-api-worker-system 0.1.0

A production-shaped Rust job queue: Axum API + async workers + Postgres SKIP LOCKED dequeue, retries with decorrelated jitter, idempotency, cooperative cancellation, OpenAPI, Prometheus metrics.
//! [`JobId`]: the strongly-typed identifier for a row in the `jobs`
//! table.
//!
//! Why a newtype wrapping `Uuid` rather than `Uuid` directly:
//!
//! - **Type safety**: a function expecting a `JobId` cannot be called
//!   with an arbitrary `Uuid` (e.g., a user-id) without an explicit
//!   conversion. This catches a real class of bugs at compile time.
//! - **One place to change the id generator**: if we ever want to move
//!   from `Uuid::now_v7` to a snowflake id or a u128, only this file
//!   changes.
//! - **Layered representations**: the `sqlx(transparent)` and
//!   `serde(transparent)` attributes mean the type behaves identically
//!   to the wrapped `Uuid` over the wire and in the database, so the
//!   wrapper is invisible to consumers but visible to the type checker.
//!
//! Why Uuid v7 specifically: see the "Design decisions" section of
//! `docs/architecture.md`. The short version is that v7 is time-ordered
//! so insertions stay localised to the rightmost BTree pages, while v4
//! would scatter writes across the entire index.

use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;

/// A typed identifier for a [`crate::Job`]. Wraps a `Uuid` (v7) with
/// transparent encoding so the JSON/DB representation is just a UUID
/// string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(transparent)]
#[schema(value_type = String, format = "uuid")]
pub struct JobId(pub Uuid);

impl JobId {
    /// Generate a new, time-ordered id.
    ///
    /// Uses [`Uuid::now_v7`]: the first 48 bits are the Unix timestamp
    /// in milliseconds, the remaining bits are random. Time-ordering
    /// means consecutive inserts tend to hit adjacent pages in the
    /// primary-key BTree instead of scattering across the index as v4
    /// UUIDs do.
    pub fn new() -> Self {
        Self(Uuid::now_v7())
    }

    /// Wrap an existing `Uuid`. Useful when receiving an id from
    /// outside the crate (e.g., parsing a URL path parameter) and
    /// re-attaching the type.
    pub fn from_uuid(u: Uuid) -> Self {
        Self(u)
    }

    /// Unwrap to a raw `Uuid`. Useful at the SQL boundary where we bind
    /// the value into a query; the transparent newtype stays out of the
    /// database driver's type inference path.
    pub fn as_uuid(&self) -> Uuid {
        self.0
    }
}

impl Default for JobId {
    /// `Default` returns a fresh id, not the nil UUID. This matches
    /// every caller's expectation: a `JobId::default()` should be a
    /// usable id, not a placeholder.
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Display for JobId {
    /// Formats as the standard hyphenated 36-char UUID representation.
    /// Used in log lines, error messages, and OpenAPI examples.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl std::str::FromStr for JobId {
    type Err = uuid::Error;

    /// Parse a hyphenated UUID string into a `JobId`. Useful for URL
    /// path parameters; Axum's `Path<Uuid>` extractor uses this when
    /// the handler asks for a `Path<JobId>` instead.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Uuid::parse_str(s).map(Self)
    }
}