Skip to main content

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}