Mountain/Vine/Error.rs
1//! # VineError
2//!
3//! Defines the specific, structured error types for all operations within the
4//! Vine gRPC Inter-Process Communication (IPC) system.
5//!
6//! ## Error Categories
7//!
8//! ### Connection Errors
9//! - `ClientNotConnected`: Sidecar not in connection pool
10//! - `ConnectionFailed`: Unable to establish connection
11//! - `ConnectionLost`: Established connection was lost
12//!
13//! ### RPC Errors
14//! - `RPCError`: Generic gRPC status error
15//! - `RequestTimeout`: Request exceeded configured timeout
16//! - `RequestCanceled`: Request was explicitly canceled
17//!
18//! ### Serialization Errors
19//! - `SerializationError`: JSON serialization/deserialization failure
20//! - `MessageTooLarge`: Message exceeds size limits
21//! - `InvalidMessageFormat`: Message format validation failed
22//!
23//! ### Transport Errors
24//! - `TonicTransportError`: Low-level tonic transport failure
25//! - `InvalidUri`: Invalid URI format
26//! - `AddressParseError`: Invalid socket address format
27//!
28//! ### Internal Errors
29//! - `InternalLockError`: Mutex poisoned (panic in another thread)
30//! - `InvalidState`: Invalid internal state detected
31//!
32//! ## Error Conversion
33//!
34//! The module provides `From` implementations for seamless conversion from:
35//! - `serde_json::Error` → `SerializationError`
36//! - `tonic::transport::Error` → `TonicTransportError`
37//! - `tonic::Status` → `RPCError`
38//! - `InvalidUri` → `InvalidUri`
39//! - `AddrParseError` → `AddressParseError`
40//! - `PoisonError<T>` → `InternalLockError`
41//!
42//! ## Usage Example
43//!
44//! ```text
45//! # use Vine::Error::VineError;
46//! fn handle_error(error:VineError) {
47//! match error {
48//! VineError::RequestTimeout { SideCarIdentifier, MethodName, TimeoutMilliseconds } => {
49//! eprintln!(
50//! "Request to {} method '{}' timed out after {}ms",
51//! SideCarIdentifier, MethodName, TimeoutMilliseconds
52//! );
53//! // Implement retry logic or fallback behavior
54//! },
55//! VineError::ClientNotConnected(id) => {
56//! eprintln!("Sidecar '{}' not connected, attempting reconnection...", id);
57//! // Attempt to reconnect
58//! },
59//! _ => eprintln!("Vine error: {}", error),
60//! }
61//! }
62//! ```
63
64use std::{
65 net::AddrParseError,
66 sync::{MutexGuard, PoisonError},
67};
68
69use http::uri::InvalidUri;
70use thiserror::Error;
71
72/// A comprehensive error enum for the Vine IPC layer.
73///
74/// Each variant provides detailed context about the failure, enabling
75/// precise error handling and recovery strategies.
76#[derive(Debug, Error)]
77pub enum VineError {
78 /// A gRPC client channel for the specified sidecar could not be found or
79 /// is not ready in the connection pool.
80 ///
81 /// This typically occurs when trying to send a request to a sidecar
82 /// that was never connected or has been disconnected.
83 #[error("SideCar '{0}' not found or its gRPC client channel is not ready.")]
84 ClientNotConnected(String),
85
86 /// Failed to establish a connection to the specified sidecar.
87 ///
88 /// This indicates that connection attempts failed, possibly due to
89 /// network issues, incorrect address, or the sidecar being unavailable.
90 #[error("Failed to connect to sidecar '{SideCarIdentifier}' at '{Address}': {Reason}")]
91 ConnectionFailed { SideCarIdentifier:String, Address:String, Reason:String },
92
93 /// An established connection to the sidecar was lost.
94 ///
95 /// This occurs when an active connection fails during communication.
96 #[error("Connection to sidecar '{0}' was lost")]
97 ConnectionLost(String),
98
99 /// An RPC call to a sidecar failed with a specific gRPC status.
100 ///
101 /// This wraps tonic::Status errors with more context about what went wrong.
102 #[error("gRPC call failed: {0}")]
103 RPCError(String),
104
105 /// A request did not receive a response within the specified timeout.
106 ///
107 /// Includes the sidecar identifier, method name, and timeout duration
108 /// for debugging and retry logic.
109 #[error(
110 "Request to sidecar '{SideCarIdentifier}' (method: '{MethodName}') timed out after {TimeoutMilliseconds}ms"
111 )]
112 RequestTimeout { SideCarIdentifier:String, MethodName:String, TimeoutMilliseconds:u64 },
113
114 /// A request was explicitly canceled before completion.
115 #[error("Request to sidecar '{SideCarIdentifier}' (method: '{MethodName}') was canceled")]
116 RequestCanceled { SideCarIdentifier:String, MethodName:String },
117
118 /// An error occurred while serializing or deserializing a JSON payload.
119 ///
120 /// This is automatically converted from serde_json::Error when using
121 /// the ? operator.
122 #[error("JSON serialization error for gRPC payload: {0}")]
123 SerializationError(#[from] serde_json::Error),
124
125 /// Message exceeded maximum allowed size.
126 ///
127 /// This prevents denial-of-service attacks via oversized messages.
128 #[error("Message size {ActualSize} bytes exceeds maximum allowed size {MaxSize} bytes")]
129 MessageTooLarge { ActualSize:usize, MaxSize:usize },
130
131 /// Message format validation failed.
132 ///
133 /// This occurs when a message doesn't conform to expected structure.
134 #[error("Invalid message format: {0}")]
135 InvalidMessageFormat(String),
136
137 /// A low-level error occurred in the `tonic` gRPC transport layer.
138 ///
139 /// This is automatically converted from tonic::transport::Error.
140 #[error("Tonic transport error: {0}")]
141 TonicTransportError(#[from] tonic::transport::Error),
142
143 /// A shared state mutex was \"poisoned,\" indicating a panic.
144 ///
145 /// This is a critical error indicating that a thread panicked while
146 /// holding a lock, leaving the shared state in an inconsistent state.
147 #[error("Internal state lock poisoned: {0}")]
148 InternalLockError(String),
149
150 /// Invalid internal state detected.
151 ///
152 /// This occurs when the system is in an unexpected state that should
153 /// never happen during normal operation.
154 #[error("Invalid internal state detected: {0}")]
155 InvalidState(String),
156
157 /// An error occurred from an invalid URI.
158 ///
159 /// This is automatically converted from http::uri::InvalidUri.
160 #[error("Invalid URI: {0}")]
161 InvalidUri(#[from] InvalidUri),
162
163 /// An error occurred while parsing a socket address.
164 ///
165 /// This is automatically converted from std::net::AddrParseError.
166 #[error("Invalid Socket Address: {0}")]
167 AddressParseError(#[from] AddrParseError),
168}
169
170impl VineError {
171 /// Checks if this error is recoverable (can retry the operation).
172 ///
173 /// Recoverable errors include timeouts, connection issues, and temporary
174 /// failures. Non-recoverable errors include serialization errors and
175 /// invalid state.
176 pub fn IsRecoverable(&self) -> bool {
177 matches!(
178 self,
179 Self::RequestTimeout { .. }
180 | Self::ConnectionFailed { .. }
181 | Self::ConnectionLost(_)
182 | Self::TonicTransportError(_)
183 )
184 }
185
186 /// Converts the error to a tonic::Status for gRPC error responses.
187 ///
188 /// This maps VineError variants to appropriate gRPC status codes:
189 /// - RequestTimeout → DeadlineExceeded
190 /// - ClientNotConnected → Unavailable
191 /// - SerializationError → Internal
192 /// - etc.
193 pub fn ToTonicStatus(&self) -> tonic::Status {
194 match self {
195 Self::RequestTimeout { .. } => tonic::Status::deadline_exceeded(self.to_string()),
196 Self::ClientNotConnected(_) | Self::ConnectionFailed { .. } => tonic::Status::unavailable(self.to_string()),
197 Self::SerializationError(_) | Self::InternalLockError(_) | Self::InvalidState(_) => {
198 tonic::Status::internal(self.to_string())
199 },
200 Self::MessageTooLarge { .. } => tonic::Status::resource_exhausted(self.to_string()),
201 Self::InvalidMessageFormat(_) | Self::InvalidUri(_) | Self::AddressParseError(_) => {
202 tonic::Status::invalid_argument(self.to_string())
203 },
204 Self::RequestCanceled { .. } => tonic::Status::cancelled(self.to_string()),
205 Self::RPCError(msg) => tonic::Status::unknown(msg.clone()),
206 Self::ConnectionLost(_) => tonic::Status::aborted(self.to_string()),
207 Self::TonicTransportError(_) => tonic::Status::unavailable(self.to_string()),
208 }
209 }
210}
211
212impl<T> From<PoisonError<MutexGuard<'_, T>>> for VineError {
213 fn from(Error:PoisonError<MutexGuard<'_, T>>) -> Self {
214 VineError::InternalLockError(format!("Shared state lock poisoned: {}", Error))
215 }
216}
217
218impl From<tonic::Status> for VineError {
219 fn from(status:tonic::Status) -> Self {
220 // Map gRPC status codes to appropriate VineError variants
221 match status.code() {
222 tonic::Code::DeadlineExceeded => VineError::RPCError(format!("Timeout: {}", status.message())),
223 tonic::Code::NotFound => VineError::ClientNotConnected(status.message().to_string()),
224 tonic::Code::AlreadyExists | tonic::Code::InvalidArgument | tonic::Code::OutOfRange => {
225 VineError::InvalidMessageFormat(status.message().to_string())
226 },
227 tonic::Code::FailedPrecondition | tonic::Code::Aborted => {
228 VineError::ConnectionLost(status.message().to_string())
229 },
230 tonic::Code::ResourceExhausted => VineError::MessageTooLarge { ActualSize:0, MaxSize:4 * 1024 * 1024 },
231 tonic::Code::Cancelled => {
232 VineError::RequestCanceled { SideCarIdentifier:"unknown".to_string(), MethodName:"unknown".to_string() }
233 },
234 tonic::Code::Unavailable => {
235 VineError::ConnectionFailed {
236 SideCarIdentifier:"unknown".to_string(),
237 Address:"unknown".to_string(),
238 Reason:status.message().to_string(),
239 }
240 },
241 _ => VineError::RPCError(status.to_string()),
242 }
243 }
244}