Mountain/Vine/
Client.rs

1//! # Vine Client
2//!
3//! Provides a simplified, thread-safe client for communicating with a `Cocoon`
4//! sidecar process via gRPC. It manages a shared pool of connections with
5//! robust error handling, automatic reconnection, health checks, and timeout
6//! management.
7//!
8//! ## Features
9//!
10//! - **Connection Pool**: Thread-safe HashMap of client connections by
11//!   identifier
12//! - **Health Checks**: Validates connection status before RPC calls
13//! - **Automatic Reconnection**: Retries failed connections with exponential
14//!   backoff
15//! - **Request Timeout**: Configurable timeout per RPC call
16//! - **Retry Logic**: Configurable retry attempts for transient failures
17//! - **Message Validation**: Size limits and format checking for all messages
18//! - **Graceful Degradation**: Handles Cocoon unavailability gracefully
19//!
20//! ## Usage Example
21//!
22//! ```rust,no_run
23//! use Vine::Client::{ConnectToSideCar, SendRequest};
24//! use serde_json::json;
25//!
26//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
27//! // Connect to Cocoon
28//! ConnectToSideCar("cocoon-main".to_string(), "127.0.0.1:50052".to_string()).await?;
29//!
30//! // Send request
31//! let result = SendRequest(
32//! 	"cocoon-main",
33//! 	"GetExtensions".to_string(),
34//! 	json!({}),
35//! 	5000, // 5 second timeout
36//! )
37//! .await?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ## Error Handling
43//!
44//! All operations return `Result<T, VineError>` with comprehensive error types:
45//! - ClientNotConnected: Sidecar not in connection pool
46//! - RequestTimeout: RPC call exceeded timeout
47//! - RPCError: gRPC transport or status error
48//! - SerializationError: JSON parsing/serialization failure
49
50use std::{
51	collections::{HashMap, hash_map::DefaultHasher},
52	sync::Arc,
53	time::{Duration, Instant},
54};
55
56use lazy_static::lazy_static;
57use log::{debug, error, info, warn};
58use parking_lot::Mutex;
59use serde_json::{Value, from_slice, to_vec};
60use tokio::time::timeout;
61
62use super::{
63	Error::VineError,
64	Generated::{GenericNotification, GenericRequest, cocoon_service_client::CocoonServiceClient},
65};
66
67/// Type alias for the Cocoon gRPC client with Channel transport
68type CocoonClient = CocoonServiceClient<tonic::transport::Channel>;
69
70/// Configuration constants for Vine client behavior
71mod Config {
72	/// Default timeout for RPC calls (5 seconds)
73	pub const DEFAULT_TIMEOUT_MS:u64 = 5000;
74
75	/// Maximum number of retry attempts for failed connections
76	pub const MAX_RETRY_ATTEMPTS:usize = 3;
77
78	/// Base delay between retry attempts (100ms)
79	pub const RETRY_BASE_DELAY_MS:u64 = 100;
80
81	/// Maximum message size for validation (4MB to match tonic default)
82	pub const MAX_MESSAGE_SIZE_BYTES:usize = 4 * 1024 * 1024;
83
84	/// Health check interval (30 seconds)
85	pub const HEALTH_CHECK_INTERVAL_MS:u64 = 30000;
86
87	/// Connection timeout (10 seconds)
88	pub const CONNECTION_TIMEOUT_MS:u64 = 10000;
89}
90
91/// Connection metadata tracking health and last activity
92struct ConnectionMetadata {
93	/// Timestamp of last successful communication
94	LastActivity:Instant,
95	/// Number of consecutive failures since last success
96	FailureCount:usize,
97	/// Whether the connection is currently marked healthy
98	IsHealthy:bool,
99}
100
101lazy_static! {
102	/// Thread-safe pool of Cocoon client connections indexed by identifier
103	static ref SIDECAR_CLIENTS: Arc<Mutex<HashMap<String, CocoonClient>>> = Arc::new(Mutex::new(HashMap::new()));
104
105	/// Thread-safe metadata for connection health tracking
106	static ref CONNECTION_METADATA: Arc<Mutex<HashMap<String, ConnectionMetadata>>> = Arc::new(Mutex::new(HashMap::new()));
107}
108
109/// Establishes a gRPC connection to a sidecar process with retry logic.
110///
111/// This function attempts to connect to a Cocoon sidecar at the specified
112/// address. It implements exponential backoff retry logic for transient
113/// failures and initializes connection metadata for health tracking.
114///
115/// # Parameters
116/// - `SideCarIdentifier`: Unique identifier for this sidecar connection
117/// - `Address`: Network address in format "host:port"
118///
119/// # Returns
120/// - `Ok(())`: Connection successfully established
121/// - `Err(VineError)`: Connection failed after all retry attempts
122///
123/// # Example
124/// ```rust,no_run
125/// # use Vine::Client::ConnectToSideCar;
126/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
127/// ConnectToSideCar("cocoon-main".to_string(), "127.0.0.1:50052".to_string()).await?;
128/// # Ok(())
129/// # }
130/// ```
131pub async fn ConnectToSideCar(SideCarIdentifier:String, Address:String) -> Result<(), VineError> {
132	info!("[VineClient] Connecting to sidecar '{}' at '{}'...", SideCarIdentifier, Address);
133
134	let endpoint = format!("http://{}", Address);
135
136	// Validate endpoint format
137	if endpoint.len() > 256 {
138		return Err(VineError::RPCError(format!("Invalid endpoint address: exceeds maximum length")));
139	}
140
141	// Attempt connection with retry logic
142	let mut last_error = None;
143
144	for attempt in 1..=Config::MAX_RETRY_ATTEMPTS {
145		let result = try_connect_single(&SideCarIdentifier, &endpoint).await;
146
147		if result.is_ok() {
148			// Initialize connection metadata
149			CONNECTION_METADATA.lock().insert(
150				SideCarIdentifier.clone(),
151				ConnectionMetadata { LastActivity:Instant::now(), FailureCount:0, IsHealthy:true },
152			);
153
154			info!("[VineClient] Successfully connected to sidecar '{}'", SideCarIdentifier);
155
156			return Ok(result?);
157		}
158
159		// Capture last error
160		last_error = Some(result.unwrap_err());
161
162		// Wait before retry (exponential backoff)
163		if attempt < Config::MAX_RETRY_ATTEMPTS {
164			let delay_ms = Config::RETRY_BASE_DELAY_MS * 2_u64.pow(attempt as u32);
165			tokio::time::sleep(Duration::from_millis(delay_ms)).await;
166		}
167	}
168
169	Err(last_error.unwrap_or_else(|| VineError::RPCError("Connection failed".to_string())))
170}
171
172/// Single connection attempt without retry logic
173async fn try_connect_single(_SideCarIdentifier:&str, endpoint:&str) -> Result<(), VineError> {
174	let endpoint_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
175		endpoint.to_string()
176	} else {
177		format!("http://{}", endpoint)
178	};
179
180	let channel = tonic::transport::Channel::from_shared(endpoint_url)
181		.map_err(|e| VineError::RPCError(format!("Failed to create channel: {}", e)))?
182		.connect()
183		.await
184		.map_err(|e| VineError::RPCError(format!("Failed to connect: {}", e)))?;
185
186	let client = CocoonClient::new(channel);
187
188	let mut clients = SIDECAR_CLIENTS.lock();
189	clients.insert(_SideCarIdentifier.to_string(), client);
190
191	Ok(())
192}
193
194/// Disconnects from a sidecar process and removes it from the connection pool.
195///
196/// This function removes the sidecar from both the connection pool and
197/// connection metadata tracking.
198///
199/// # Parameters
200/// - `SideCarIdentifier`: Unique identifier of the sidecar to disconnect
201///
202/// # Returns
203/// - `Ok(())`: Disconnection successful
204/// - `Err(VineError)`: Sidecar was not connected
205///
206/// # Example
207/// ```rust,no_run
208/// # use Vine::Client::DisconnectFromSideCar;
209/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
210/// DisconnectFromSideCar("cocoon-main".to_string())?;
211/// # Ok(())
212/// # }
213/// ```
214pub fn DisconnectFromSideCar(SideCarIdentifier:String) -> Result<(), VineError> {
215	let mut clients = SIDECAR_CLIENTS.lock();
216
217	if clients.remove(&SideCarIdentifier).is_some() {
218		CONNECTION_METADATA.lock().remove(&SideCarIdentifier);
219
220		info!("[VineClient] Disconnected from sidecar '{}'", SideCarIdentifier);
221
222		Ok(())
223	} else {
224		Err(VineError::ClientNotConnected(SideCarIdentifier))
225	}
226}
227
228/// Checks the health status of a connected sidecar.
229///
230/// Health is determined by:
231/// - Connection exists in the pool
232/// - Last activity within health check interval
233/// - Failure count below threshold
234///
235/// # Parameters
236/// - `SideCarIdentifier`: Unique identifier of the sidecar to check
237///
238/// # Returns
239/// - `Ok(true)`: Sidecar is healthy and responsive
240/// - `Ok(false)`: Sidecar exists but may have issues
241/// - `Err(VineError)`: Sidecar not connected
242///
243/// # Example
244/// ```rust,no_run
245/// # use Vine::Client::CheckSideCarHealth;
246/// # fn example() -> Result<bool, Box<dyn std::error::Error>> {
247/// let healthy = CheckSideCarHealth("cocoon-main")?;
248/// # Ok(healthy)
249/// # }
250/// ```
251pub fn CheckSideCarHealth(SideCarIdentifier:&str) -> Result<bool, VineError> {
252	let metadata = CONNECTION_METADATA.lock();
253
254	if let Some(conn) = metadata.get(SideCarIdentifier) {
255		let is_stale = conn.LastActivity.elapsed() > Duration::from_millis(Config::HEALTH_CHECK_INTERVAL_MS);
256		let has_many_failures = conn.FailureCount > Config::MAX_RETRY_ATTEMPTS;
257
258		Ok(conn.IsHealthy && !is_stale && !has_many_failures)
259	} else {
260		Err(VineError::ClientNotConnected(SideCarIdentifier.to_string()))
261	}
262}
263
264/// Records a failure for a sidecar connection.
265///
266/// Increments the failure count and marks the connection as unhealthy.
267///
268/// # Parameters
269/// - `SideCarIdentifier`: Unique identifier of the sidecar that failed
270fn RecordSideCarFailure(SideCarIdentifier:&str) {
271	let mut metadata = CONNECTION_METADATA.lock();
272
273	if let Some(conn) = metadata.get_mut(SideCarIdentifier) {
274		conn.FailureCount += 1;
275		conn.IsHealthy = false;
276	}
277}
278
279/// Updates the last activity timestamp for a sidecar.
280///
281/// Called after successful operations to track liveness.
282///
283/// # Parameters
284/// - `SideCarIdentifier`: Unique identifier of the sidecar
285fn UpdateSideCarActivity(SideCarIdentifier:&str) {
286	let mut metadata = CONNECTION_METADATA.lock();
287
288	if let Some(conn) = metadata.get_mut(SideCarIdentifier) {
289		conn.LastActivity = Instant::now();
290		conn.FailureCount = 0;
291		conn.IsHealthy = true;
292	}
293}
294
295/// Validates message size against maximum allowed.
296///
297/// Helps prevent denial-of-service attacks via overly large messages.
298///
299/// # Parameters
300/// - `data`: Raw byte slice to validate
301///
302/// # Returns
303/// - `Ok(())`: Message size is within limits
304/// - `Err(VineError::SerializationError)`: Message exceeds maximum size
305fn ValidateMessageSize(data:&[u8]) -> Result<(), VineError> {
306	if data.len() > Config::MAX_MESSAGE_SIZE_BYTES {
307		Err(VineError::MessageTooLarge { ActualSize:data.len(), MaxSize:Config::MAX_MESSAGE_SIZE_BYTES })
308	} else {
309		Ok(())
310	}
311}
312
313/// Sends a request to a sidecar and waits for a response.
314///
315/// This is the primary method for request-response communication with sidecars.
316/// It implements timeout handling and automatic connection validation.
317///
318/// # Parameters
319/// - `SideCarIdentifier`: Unique identifier of the target sidecar
320/// - `Method`: RPC method name to call
321/// - `Parameters`: JSON parameters for the RPC call
322/// - `TimeoutMilliseconds`: Maximum time to wait for response (default: 5000ms)
323///
324/// # Returns
325/// - `Ok(Value)`: JSON response from the sidecar
326/// - `Err(VineError)`: Request failed or timed out
327///
328/// # Example
329/// ```rust,no_run
330/// # use Vine::Client::SendRequest;
331/// use serde_json::json;
332/// # async fn example() -> Result<serde_json::Value, Box<dyn std::error::Error>> {
333/// let result =
334/// 	SendRequest("cocoon-main".to_string(), "GetExtensions".to_string(), json!({}), 5000)
335/// 		.await?;
336/// # Ok(result)
337/// # }
338/// ```
339pub async fn SendRequest(
340	SideCarIdentifier:&str,
341	Method:String,
342	Parameters:Value,
343	TimeoutMilliseconds:u64,
344) -> Result<Value, VineError> {
345	// Validate method name format
346	if Method.is_empty() || Method.len() > 128 {
347		return Err(VineError::RPCError(
348			"Method name must be between 1 and 128 characters".to_string(),
349		));
350	}
351
352	let timeout_duration = Duration::from_millis(if TimeoutMilliseconds > 0 {
353		TimeoutMilliseconds
354	} else {
355		Config::DEFAULT_TIMEOUT_MS
356	});
357
358	// Validate message size
359	let parameter_bytes =
360		to_vec(&Parameters).map_err(|e| VineError::RPCError(format!("Failed to serialize parameters: {}", e)))?;
361	ValidateMessageSize(&parameter_bytes)?;
362
363	let mut client = {
364		let guard = SIDECAR_CLIENTS.lock();
365		guard.get(SideCarIdentifier).cloned()
366	};
367
368	if client.is_none() {
369		return Err(VineError::ClientNotConnected(SideCarIdentifier.to_string()));
370	}
371
372	let mut client = client.unwrap();
373
374	let request_identifier = std::time::SystemTime::now()
375		.duration_since(std::time::UNIX_EPOCH)
376		.unwrap()
377		.as_nanos() as u64;
378	let method_clone = Method.clone();
379	let request = GenericRequest { request_identifier, method:Method, parameter:parameter_bytes };
380
381	let result = timeout(timeout_duration, client.process_mountain_request(request)).await;
382
383	match result {
384		Ok(Ok(response)) => {
385			UpdateSideCarActivity(SideCarIdentifier);
386			debug!(
387				"[VineClient] Request sent successfully to sidecar '{}': method='{}'",
388				SideCarIdentifier, method_clone
389			);
390
391			// Get the inner response message
392			let inner_response = response.into_inner();
393
394			// Parse response JSON
395			let result_bytes = inner_response.result;
396			let result_value:Value = from_slice(&result_bytes)
397				.map_err(|e| VineError::RPCError(format!("Failed to deserialize response: {}", e)))?;
398
399			// Check for RPC errors in response
400			if let Some(error_data) = inner_response.error {
401				return Err(VineError::RPCError(format!(
402					"RPC error from sidecar: code={}, message={}",
403					error_data.code, error_data.message
404				)));
405			}
406
407			Ok(result_value)
408		},
409		Ok(Err(status)) => {
410			RecordSideCarFailure(SideCarIdentifier);
411			return Err(VineError::RPCError(format!("gRPC error: {}", status)));
412		},
413		Err(_) => {
414			RecordSideCarFailure(SideCarIdentifier);
415			Err(VineError::RequestTimeout {
416				SideCarIdentifier:SideCarIdentifier.to_string(),
417				MethodName:method_clone,
418				TimeoutMilliseconds:timeout_duration.as_millis() as u64,
419			})
420		},
421	}
422}
423
424/// Sends a notification to a sidecar without waiting for a response.
425///
426/// Note: This does not include a timeout parameter (unlike `SendRequest`).
427/// Notifications are sent as fire-and-forget messages.
428///
429/// ```rust,no_run
430/// # use Vine::Client::SendNotification;
431/// use serde_json::json;
432/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
433/// SendNotification(
434///     "cocoon-main".to_string(),
435///     "UpdateTheme".to_string(),
436///     json!({"theme": "dark"}),
437/// ).await?;
438/// # Ok(())
439/// # }
440/// ```
441pub async fn SendNotification(SideCarIdentifier:String, Method:String, Parameters:Value) -> Result<(), VineError> {
442	// Validate method name format
443	if Method.is_empty() || Method.len() > 128 {
444		return Err(VineError::RPCError(
445			"Method name must be between 1 and 128 characters".to_string(),
446		));
447	}
448
449	let parameter_bytes = to_vec(&Parameters)?;
450	ValidateMessageSize(&parameter_bytes)?;
451
452	let mut client = {
453		let guard = SIDECAR_CLIENTS.lock();
454		guard.get(&SideCarIdentifier).cloned()
455	};
456
457	if let Some(ref mut client) = client {
458		let request = GenericNotification { method:Method, parameter:parameter_bytes };
459
460		match client.send_mountain_notification(request).await {
461			Ok(_) => {
462				UpdateSideCarActivity(&SideCarIdentifier);
463				debug!("[VineClient] Notification sent successfully to sidecar '{}'", SideCarIdentifier);
464				Ok(())
465			},
466			Err(status) => {
467				RecordSideCarFailure(&SideCarIdentifier);
468				error!(
469					"[VineClient] Failed to send notification to sidecar '{}': {}",
470					SideCarIdentifier, status
471				);
472				Err(VineError::from(status))
473			},
474		}
475	} else {
476		Err(VineError::ClientNotConnected(SideCarIdentifier))
477	}
478}