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    /// Timed out waiting for the next chunk of response body tokens (streaming only)
66    #[error("Tokens timeout: {0}")]
67    TokensTimeout(String),
68
69    /// Timed out waiting for the entire response body to complete (streaming only).
70    /// Fires when the total body read exceeds body_timeout.
71    #[error("Body timeout: {0}")]
72    BodyTimeout(String),
73
74    /// Serialization/deserialization error
75    #[error("Serialization error: {0}")]
76    Serialization(#[from] serde_json::Error),
77
78    /// General error from anyhow
79    #[error(transparent)]
80    Other(#[from] anyhow::Error),
81}
82
83/// Helper functions for serializing and deserializing errors to/from JSON.
84///
85/// These are used to store error information in the database in a structured format.
86/// TODO: What's the point of this module? Thisi s just serde logic right? Why can't we just use
87/// serde_json
88pub mod error_serialization {
89    use anyhow::Error;
90    use serde::{Deserialize, Serialize};
91
92    /// Serialized error format that preserves error message and source chain.
93    #[derive(Debug, Clone, Serialize, Deserialize)]
94    pub struct SerializedError {
95        /// The main error message
96        pub message: String,
97        /// Chain of source errors, if any
98        pub sources: Vec<String>,
99    }
100
101    /// Serializes an anyhow::Error to a JSON string.
102    ///
103    /// Preserves the error message and the chain of source errors.
104    pub fn serialize_error(error: &Error) -> String {
105        let serialized = SerializedError {
106            message: error.to_string(),
107            sources: error.chain().skip(1).map(|e| e.to_string()).collect(),
108        };
109        serde_json::to_string(&serialized).unwrap_or_else(|_| {
110            format!(
111                r#"{{"message":"{}","sources":[]}}"#,
112                error.to_string().replace('"', "\\\"")
113            )
114        })
115    }
116
117    /// Deserializes an error from a JSON string.
118    ///
119    /// Returns an anyhow::Error with the original message.
120    pub fn deserialize_error(json: &str) -> Error {
121        match serde_json::from_str::<SerializedError>(json) {
122            Ok(serialized) => {
123                let mut error_msg = serialized.message;
124                if !serialized.sources.is_empty() {
125                    error_msg.push_str("\nCaused by:\n");
126                    for (i, source) in serialized.sources.iter().enumerate() {
127                        error_msg.push_str(&format!("  {}: {}\n", i + 1, source));
128                    }
129                }
130                anyhow::anyhow!(error_msg)
131            }
132            Err(_) => {
133                // Fallback: treat the entire string as an error message
134                anyhow::anyhow!("Deserialization failed: {}", json)
135            }
136        }
137    }
138
139    #[cfg(test)]
140    mod tests {
141        use super::*;
142
143        #[test]
144        fn test_serialize_deserialize_simple_error() {
145            let error = anyhow::anyhow!("Test error");
146            let serialized = serialize_error(&error);
147            let deserialized = deserialize_error(&serialized);
148            assert_eq!(error.to_string(), deserialized.to_string());
149        }
150
151        #[test]
152        fn test_serialize_deserialize_with_context() {
153            let error = anyhow::anyhow!("Root cause")
154                .context("Middle context")
155                .context("Top context");
156            let serialized = serialize_error(&error);
157            let deserialized = deserialize_error(&serialized);
158            // The deserialized error should contain the full chain
159            assert!(deserialized.to_string().contains("Top context"));
160            assert!(deserialized.to_string().contains("Middle context"));
161            assert!(deserialized.to_string().contains("Root cause"));
162        }
163    }
164}