fusillade_core/error.rs
1//! Error types for the batching system.
2
3use thiserror::Error;
4
5use crate::types::RequestId;
6
7/// Result type alias using the fusillade error type.
8pub type Result<T> = std::result::Result<T, FusilladeError>;
9
10/// Main error type for the batching system.
11#[derive(Error, Debug)]
12pub enum FusilladeError {
13 /// Request not found
14 #[error("Request not found: {0}")]
15 RequestNotFound(RequestId),
16
17 /// Request exists but is not in the expected state for the requested
18 /// operation (e.g., completing an already-completed or failed request).
19 ///
20 /// Distinct from [`RequestNotFound`] so callers can be properly idempotent
21 /// against concurrent writers — e.g. a complete-then-complete race where
22 /// the second caller should treat "already completed" as success rather
23 /// than synthesizing a new row.
24 #[error("Request {id} is in state '{current_state}', expected one of: {expected}")]
25 RequestStateConflict {
26 id: RequestId,
27 current_state: String,
28 expected: &'static str,
29 },
30
31 /// Cancelled request
32 #[error("Request cancelled: {0}")]
33 RequestCancelled(RequestId),
34
35 /// Daemon is shutting down
36 #[error("Daemon is shutting down")]
37 Shutdown,
38
39 /// Request is in an invalid state for the requested operation
40 #[error("Invalid state transition: request {0} is in state '{1}', expected '{2}'")]
41 InvalidState(RequestId, String, String),
42
43 /// Validation error (e.g., invalid file format, missing required fields)
44 #[error("Validation error: {0}")]
45 ValidationError(String),
46
47 /// HTTP client error.
48 #[error("HTTP request failed: {0}")]
49 HttpClient(String),
50
51 /// HTTP request builder error.
52 #[error("HTTP request builder failed: {0}")]
53 HttpRequestBuilder(String),
54
55 /// HTTP client timeout.
56 #[error("HTTP request timed out: {0}")]
57 HttpClientTimeout(String),
58
59 /// Timed out waiting for response headers + first body chunk (time-to-first-token).
60 /// Only used for streaming requests. Handles servers (like vLLM) that return
61 /// headers immediately but queue the request before producing tokens.
62 #[error("First chunk timeout: {0}")]
63 FirstChunkTimeout(String),
64
65 /// The request body upload made no progress for the configured stall window.
66 /// Distinguishes send-phase hangs (a wedged connection or a stalled write)
67 /// from a slow-to-respond upstream, which is governed by first_chunk_timeout:
68 /// uploading a request should take seconds even when the answer takes hours.
69 #[error("Upload stall timeout: {0}")]
70 UploadStallTimeout(String),
71
72 /// Timed out waiting for the next chunk of response body tokens (streaming only)
73 #[error("Tokens timeout: {0}")]
74 TokensTimeout(String),
75
76 /// Timed out waiting for the entire response body to complete (streaming only).
77 /// Fires when the total body read exceeds body_timeout.
78 #[error("Body timeout: {0}")]
79 BodyTimeout(String),
80
81 /// Serialization/deserialization error
82 #[error("Serialization error: {0}")]
83 Serialization(#[from] serde_json::Error),
84
85 /// General error from anyhow
86 #[error(transparent)]
87 Other(#[from] anyhow::Error),
88}
89
90/// Helper functions for serializing and deserializing errors to/from JSON.
91///
92/// These are used to store error information in the database in a structured format.
93/// TODO: What's the point of this module? Thisi s just serde logic right? Why can't we just use
94/// serde_json
95pub mod error_serialization {
96 use anyhow::Error;
97 use serde::{Deserialize, Serialize};
98
99 /// Serialized error format that preserves error message and source chain.
100 #[derive(Debug, Clone, Serialize, Deserialize)]
101 pub struct SerializedError {
102 /// The main error message
103 pub message: String,
104 /// Chain of source errors, if any
105 pub sources: Vec<String>,
106 }
107
108 /// Serializes an anyhow::Error to a JSON string.
109 ///
110 /// Preserves the error message and the chain of source errors.
111 pub fn serialize_error(error: &Error) -> String {
112 let serialized = SerializedError {
113 message: error.to_string(),
114 sources: error.chain().skip(1).map(|e| e.to_string()).collect(),
115 };
116 serde_json::to_string(&serialized).unwrap_or_else(|_| {
117 format!(
118 r#"{{"message":"{}","sources":[]}}"#,
119 error.to_string().replace('"', "\\\"")
120 )
121 })
122 }
123
124 /// Deserializes an error from a JSON string.
125 ///
126 /// Returns an anyhow::Error with the original message.
127 pub fn deserialize_error(json: &str) -> Error {
128 match serde_json::from_str::<SerializedError>(json) {
129 Ok(serialized) => {
130 let mut error_msg = serialized.message;
131 if !serialized.sources.is_empty() {
132 error_msg.push_str("\nCaused by:\n");
133 for (i, source) in serialized.sources.iter().enumerate() {
134 error_msg.push_str(&format!(" {}: {}\n", i + 1, source));
135 }
136 }
137 anyhow::anyhow!(error_msg)
138 }
139 Err(_) => {
140 // Fallback: treat the entire string as an error message
141 anyhow::anyhow!("Deserialization failed: {}", json)
142 }
143 }
144 }
145
146 #[cfg(test)]
147 mod tests {
148 use super::*;
149
150 #[test]
151 fn test_serialize_deserialize_simple_error() {
152 let error = anyhow::anyhow!("Test error");
153 let serialized = serialize_error(&error);
154 let deserialized = deserialize_error(&serialized);
155 assert_eq!(error.to_string(), deserialized.to_string());
156 }
157
158 #[test]
159 fn test_serialize_deserialize_with_context() {
160 let error = anyhow::anyhow!("Root cause")
161 .context("Middle context")
162 .context("Top context");
163 let serialized = serialize_error(&error);
164 let deserialized = deserialize_error(&serialized);
165 // The deserialized error should contain the full chain
166 assert!(deserialized.to_string().contains("Top context"));
167 assert!(deserialized.to_string().contains("Middle context"));
168 assert!(deserialized.to_string().contains("Root cause"));
169 }
170 }
171}