Skip to main content

ppoppo_infra/
error.rs

1//! Backend-agnostic error types for infrastructure operations.
2
3use thiserror::Error;
4use time::OffsetDateTime;
5
6/// Result type alias for infrastructure operations.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Errors that can occur during infrastructure operations.
10///
11/// Backend-specific errors (sqlx, redis, etc.) are wrapped in [`Error::Backend`].
12#[derive(Debug, Error)]
13pub enum Error {
14    /// Backend-specific error (sqlx::Error, redis::Error, etc.).
15    #[error("backend error: {0}")]
16    Backend(Box<dyn std::error::Error + Send + Sync>),
17
18    /// JSON serialization/deserialization error.
19    #[error("json error: {0}")]
20    Json(#[from] serde_json::Error),
21
22    /// Key not found in cache or counter.
23    #[error("key not found: {0}")]
24    NotFound(String),
25
26    /// Job not found in queue.
27    ///
28    /// After the `PARTITION BY LIST (queue_name)` migration, every id-based
29    /// JobQueue operation scopes the lookup to a specific partition via
30    /// `queue_name`. A `JobNotFound` can therefore mean either "no job with
31    /// this id in this queue" (the genuine case) or "the caller passed the
32    /// wrong queue_name for a real id" (a programming bug). Carrying both
33    /// fields lets operators distinguish the two in logs.
34    #[error("job not found: queue={queue_name}, id={id}")]
35    JobNotFound {
36        /// The queue the caller asked for.
37        queue_name: String,
38        /// The job id the caller asked for.
39        id: String,
40    },
41
42    /// Rate limit exceeded.
43    #[error("rate limit exceeded: {current} requests (limit: {limit})")]
44    RateLimitExceeded {
45        /// Current weighted request count.
46        current: f64,
47        /// Maximum allowed requests.
48        limit: i32,
49        /// When the rate limit resets.
50        reset_at: OffsetDateTime,
51    },
52
53    /// Lock acquisition failed (already held by another session).
54    #[error("failed to acquire lock: {0}")]
55    LockFailed(String),
56
57    /// Lock acquisition timed out.
58    #[error("lock acquisition timed out after {0}ms")]
59    LockTimeout(i32),
60
61    /// Payload too large for the backend's notification limit.
62    #[error("payload too large: {size} bytes (max: {max_size})")]
63    PayloadTooLarge {
64        /// Actual payload size in bytes.
65        size: usize,
66        /// Maximum allowed size in bytes.
67        max_size: usize,
68    },
69
70    /// Internal channel closed (background task exited).
71    #[error("channel closed: {0}")]
72    ChannelClosed(String),
73}
74
75impl Error {
76    /// Wrap a backend-specific error.
77    pub fn backend(err: impl std::error::Error + Send + Sync + 'static) -> Self {
78        Self::Backend(Box::new(err))
79    }
80
81    /// Returns true if this is a rate limit exceeded error.
82    pub fn is_rate_limited(&self) -> bool {
83        matches!(self, Error::RateLimitExceeded { .. })
84    }
85
86    /// Returns true if this is a not found error.
87    pub fn is_not_found(&self) -> bool {
88        matches!(self, Error::NotFound(_) | Error::JobNotFound { .. })
89    }
90
91    /// Returns true if this is a lock-related error.
92    pub fn is_lock_error(&self) -> bool {
93        matches!(self, Error::LockFailed(_) | Error::LockTimeout(_))
94    }
95
96    /// Extract rate limit reset time if this is a rate limit error.
97    pub fn reset_at(&self) -> Option<OffsetDateTime> {
98        match self {
99            Error::RateLimitExceeded { reset_at, .. } => Some(*reset_at),
100            _ => None,
101        }
102    }
103}