trusty-common 0.49.0

Shared utilities and provider-agnostic streaming chat (ChatProvider, OllamaProvider, OpenRouter, tool-use) for trusty-* projects
Documentation
//! Failures [`super::UdsServiceSupervisor`] reports to its caller (#5089).
//!
//! Why: `Bm25Supervisor` returned `anyhow::Result` because it lived in a
//! binary-adjacent crate. Promoted into `trusty-common` the same failures cross
//! a library boundary, where this workspace's convention is a structured
//! `thiserror` enum — and where a caller deciding whether to retry, degrade, or
//! surface a 5xx needs to tell "the binary is missing" from "it bound too
//! slowly" from "something untrusted is already on that path".
//! What: one variant per way `ensure_running` can decline to hand back a socket.
//! Test: exercised through `tests.rs`'s spawn-failure and untrusted-socket
//! cases; the `Display` strings are the operator-facing contract.

use std::path::PathBuf;
use std::time::Duration;

/// Render a captured stderr tail as a suffix, or nothing when there is none.
///
/// Why: `thiserror` formats one string per variant, and a bare "last stderr: "
/// on a detached child — whose stderr is never captured — would read as a child
/// that printed nothing rather than one nobody was listening to.
fn format_stderr_tail(lines: &[String]) -> String {
    if lines.is_empty() {
        String::new()
    } else {
        format!(" — last stderr: {}", lines.join(" | "))
    }
}

/// Why a supervised service could not be made available.
///
/// `#[non_exhaustive]`: on an ENUM the attribute constrains *matching* — an
/// external crate needs a wildcard arm (E0004) — which is the intended cost of
/// keeping variant additions non-breaking once `trusty-common` publishes.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SupervisorError {
    /// The caller's spawn-spec closure failed — typically because the service's
    /// binary could not be located.
    #[error("resolve the spawn command for {service} instance {key}: {source}")]
    SpawnSpec {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// Underlying failure.
        #[source]
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },

    /// A directory the spec asked for could not be created.
    #[error("create directory {path} for {service} instance {key}: {source}")]
    CreateDir {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// Directory that could not be created.
        path: PathBuf,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// `Command::spawn` failed.
    #[error("spawn {program} for {service} instance {key}: {source}")]
    Spawn {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// Binary that could not be executed.
        program: PathBuf,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// The child started but never bound its socket inside the service's
    /// `spawn_probe` budget.
    ///
    /// #6600 review: this carries the child's stderr tail for the same reason
    /// [`SupervisorError::ChildExited`] does. "Its `spawn_probe` is too small"
    /// is the message's guess, and the child's own last lines are what say
    /// whether that guess is right — a model still loading and a child blocked
    /// on a lock it will never get produce the same timeout and different logs.
    #[error(
        "{service} instance {key} did not bind {socket} within {budget:?} — \
         if this service loads a model at startup, its ServiceTimeouts::spawn_probe \
         is too small{}",
        format_stderr_tail(.stderr)
    )]
    SpawnTimeout {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// Socket the child was expected to bind.
        socket: PathBuf,
        /// The budget that elapsed.
        budget: Duration,
        /// Last lines the child wrote to stderr before it was killed. EMPTY for
        /// a detached child, whose stderr is inherited rather than captured —
        /// see `child::spawn_child` for why capturing it would kill the child.
        stderr: Vec<String>,
    },

    /// The child exited before it bound its socket (#6600).
    ///
    /// Distinct from [`SupervisorError::SpawnTimeout`] because the remedies do
    /// not overlap: a timeout points at the service's `spawn_probe` budget,
    /// while this points at whatever the child itself refused to do. The
    /// supervisor returns it within one probe interval rather than at the
    /// budget boundary, so a child that dies in 100 ms is not reported 20 s
    /// later as a slow bind.
    #[error(
        "{service} instance {key} exited before binding {socket}: {status}{}",
        format_stderr_tail(.stderr)
    )]
    ChildExited {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// Socket the child was expected to bind.
        socket: PathBuf,
        /// How the child exited.
        status: std::process::ExitStatus,
        /// Last lines the child wrote to stderr. EMPTY for a detached child,
        /// whose stderr is inherited rather than captured — see
        /// `child::spawn_child` for why capturing it would kill the child.
        stderr: Vec<String>,
    },

    /// Something is serving the socket, but it does not pass the
    /// [`crate::uds::verify_socket_for_connect`] check — so adopting it would
    /// mean trusting a socket whose permissions are not the credential
    /// ADR-0034 §3 says they are.
    #[error("refusing to adopt {socket} for {service} instance {key}: {source}")]
    UntrustedSocket {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// The socket that was refused.
        socket: PathBuf,
        /// Which property failed. Boxed: `UdsSecurityError` is the largest
        /// payload in this enum, and unboxed it pushes `SupervisorError` past
        /// clippy's `result_large_err` threshold for every fallible non-async
        /// caller.
        #[source]
        source: Box<crate::uds::UdsSecurityError>,
    },

    /// A runtime-derived [`crate::uds::ServiceTimeouts`] pair fails the one
    /// relation the type enforces. Only [`crate::uds::ServiceTimeouts::try_new`]
    /// produces this — the `const fn` constructor turns the same condition into
    /// a build error.
    #[error(
        "SIGTERM patience {sigterm_patience:?} must strictly exceed the supervised \
         child's shutdown-flush budget {shutdown_flush:?}, or the SIGKILL lands \
         mid-flush and discards acked writes"
    )]
    InvalidTimeouts {
        /// The patience that was too short.
        sigterm_patience: Duration,
        /// The child's declared flush budget.
        shutdown_flush: Duration,
    },

    /// The socket path does not fit the kernel's address buffer, so no child
    /// could ever bind it.
    #[error("socket path for {service} instance {key} is unusable: {source}")]
    SocketPath {
        /// Service label.
        service: String,
        /// Instance key.
        key: String,
        /// The underlying budget failure. Boxed for the same reason as
        /// [`SupervisorError::UntrustedSocket`]'s.
        #[source]
        source: Box<crate::uds::UdsSecurityError>,
    },
}