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}