Skip to main content

Vine/
Error.rs

1//! # Vine::Error
2//!
3//! Canonical, structured error types for every operation that flows through
4//! Vine - the gRPC IPC layer that connects Mountain, Cocoon, and Air.
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
32use std::{
33	net::AddrParseError,
34	sync::{MutexGuard, PoisonError},
35};
36
37use http::uri::InvalidUri;
38use thiserror::Error;
39
40/// A comprehensive error enum for the Vine IPC layer.
41///
42/// Each variant carries detailed context so callers can choose between retry,
43/// fallback, and surface-to-user strategies.
44#[derive(Debug, Error)]
45pub enum VineError {
46	/// A gRPC client channel for the specified sidecar could not be found or
47	/// is not ready in the connection pool.
48	#[error("SideCar '{0}' not found or its gRPC client channel is not ready.")]
49	ClientNotConnected(String),
50
51	/// Failed to establish a connection to the specified sidecar.
52	#[error("Failed to connect to sidecar '{SideCarIdentifier}' at '{Address}': {Reason}")]
53	ConnectionFailed { SideCarIdentifier:String, Address:String, Reason:String },
54
55	/// An established connection to the sidecar was lost.
56	#[error("Connection to sidecar '{0}' was lost")]
57	ConnectionLost(String),
58
59	/// An RPC call to a sidecar failed with a specific gRPC status.
60	#[error("gRPC call failed: {0}")]
61	RPCError(String),
62
63	/// A request did not receive a response within the configured timeout.
64	#[error(
65		"Request to sidecar '{SideCarIdentifier}' (method: '{MethodName}') timed out after {TimeoutMilliseconds}ms"
66	)]
67	RequestTimeout { SideCarIdentifier:String, MethodName:String, TimeoutMilliseconds:u64 },
68
69	/// A request was explicitly cancelled before completion.
70	#[error("Request to sidecar '{SideCarIdentifier}' (method: '{MethodName}') was canceled")]
71	RequestCanceled { SideCarIdentifier:String, MethodName:String },
72
73	/// An error occurred while serializing or deserializing a JSON payload.
74	#[error("JSON serialization error for gRPC payload: {0}")]
75	SerializationError(#[from] serde_json::Error),
76
77	/// Message exceeded the maximum allowed size.
78	#[error("Message size {ActualSize} bytes exceeds maximum allowed size {MaxSize} bytes")]
79	MessageTooLarge { ActualSize:usize, MaxSize:usize },
80
81	/// Message format validation failed.
82	#[error("Invalid message format: {0}")]
83	InvalidMessageFormat(String),
84
85	/// A low-level error occurred in the `tonic` gRPC transport layer.
86	#[error("Tonic transport error: {0}")]
87	TonicTransportError(#[from] tonic::transport::Error),
88
89	/// A shared state mutex was "poisoned," indicating a panic in another
90	/// thread while holding the lock.
91	#[error("Internal state lock poisoned: {0}")]
92	InternalLockError(String),
93
94	/// Invalid internal state detected - the system reached an unexpected
95	/// state that should never happen during normal operation.
96	#[error("Invalid internal state detected: {0}")]
97	InvalidState(String),
98
99	/// An error occurred from an invalid URI.
100	#[error("Invalid URI: {0}")]
101	InvalidUri(#[from] InvalidUri),
102
103	/// An error occurred while parsing a socket address.
104	#[error("Invalid Socket Address: {0}")]
105	AddressParseError(#[from] AddrParseError),
106}
107
108impl VineError {
109	/// Returns `true` when the error is recoverable (the caller can sensibly
110	/// retry the operation).
111	pub fn IsRecoverable(&self) -> bool {
112		matches!(
113			self,
114			Self::RequestTimeout { .. }
115				| Self::ConnectionFailed { .. }
116				| Self::ConnectionLost(_)
117				| Self::TonicTransportError(_)
118		)
119	}
120
121	/// Maps the error to a `tonic::Status` suitable for a gRPC error response.
122	pub fn ToTonicStatus(&self) -> tonic::Status {
123		match self {
124			Self::RequestTimeout { .. } => tonic::Status::deadline_exceeded(self.to_string()),
125
126			Self::ClientNotConnected(_) | Self::ConnectionFailed { .. } => tonic::Status::unavailable(self.to_string()),
127
128			Self::SerializationError(_) | Self::InternalLockError(_) | Self::InvalidState(_) => {
129				tonic::Status::internal(self.to_string())
130			},
131
132			Self::MessageTooLarge { .. } => tonic::Status::resource_exhausted(self.to_string()),
133
134			Self::InvalidMessageFormat(_) | Self::InvalidUri(_) | Self::AddressParseError(_) => {
135				tonic::Status::invalid_argument(self.to_string())
136			},
137
138			Self::RequestCanceled { .. } => tonic::Status::cancelled(self.to_string()),
139
140			Self::RPCError(msg) => tonic::Status::unknown(msg.clone()),
141
142			Self::ConnectionLost(_) => tonic::Status::aborted(self.to_string()),
143
144			Self::TonicTransportError(_) => tonic::Status::unavailable(self.to_string()),
145		}
146	}
147}
148
149impl<T> From<PoisonError<MutexGuard<'_, T>>> for VineError {
150	fn from(Error:PoisonError<MutexGuard<'_, T>>) -> Self {
151		VineError::InternalLockError(format!("Shared state lock poisoned: {}", Error))
152	}
153}
154
155impl From<tonic::Status> for VineError {
156	fn from(Status:tonic::Status) -> Self {
157		match Status.code() {
158			tonic::Code::DeadlineExceeded => VineError::RPCError(format!("Timeout: {}", Status.message())),
159
160			tonic::Code::NotFound => VineError::ClientNotConnected(Status.message().to_string()),
161
162			tonic::Code::AlreadyExists | tonic::Code::InvalidArgument | tonic::Code::OutOfRange => {
163				VineError::InvalidMessageFormat(Status.message().to_string())
164			},
165
166			tonic::Code::FailedPrecondition | tonic::Code::Aborted => {
167				VineError::ConnectionLost(Status.message().to_string())
168			},
169
170			tonic::Code::ResourceExhausted => VineError::MessageTooLarge { ActualSize:0, MaxSize:4 * 1024 * 1024 },
171
172			tonic::Code::Cancelled => {
173				VineError::RequestCanceled { SideCarIdentifier:"unknown".to_string(), MethodName:"unknown".to_string() }
174			},
175
176			tonic::Code::Unavailable => {
177				VineError::ConnectionFailed {
178					SideCarIdentifier:"unknown".to_string(),
179
180					Address:"unknown".to_string(),
181
182					Reason:Status.message().to_string(),
183				}
184			},
185
186			_ => VineError::RPCError(Status.to_string()),
187		}
188	}
189}
190
191/// Convenience `Result` alias for Vine operations.
192pub type Result<T> = std::result::Result<T, VineError>;