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}