neo3/neo_clients/
production_client.rs

1use crate::{
2	neo_clients::{
3		APITrait, Cache, CacheConfig, CircuitBreaker, CircuitBreakerConfig, ConnectionPool,
4		HttpProvider, PoolConfig, RpcCache, RpcClient,
5	},
6	neo_error::{Neo3Error, Neo3Result},
7};
8use serde_json::Value;
9use std::{sync::Arc, time::Duration};
10use tokio::sync::RwLock;
11
12/// Production-ready RPC client with connection pooling, caching, and circuit breaker
13pub struct ProductionRpcClient {
14	pool: ConnectionPool,
15	cache: RpcCache,
16	circuit_breaker: CircuitBreaker,
17	config: ProductionClientConfig,
18	stats: Arc<RwLock<ProductionClientStats>>,
19}
20
21/// Configuration for production RPC client
22#[derive(Debug, Clone)]
23pub struct ProductionClientConfig {
24	/// Connection pool configuration
25	pub pool_config: PoolConfig,
26	/// Cache configuration
27	pub cache_config: CacheConfig,
28	/// Circuit breaker configuration
29	pub circuit_breaker_config: CircuitBreakerConfig,
30	/// Enable request/response logging
31	pub enable_logging: bool,
32	/// Enable metrics collection
33	pub enable_metrics: bool,
34}
35
36impl Default for ProductionClientConfig {
37	fn default() -> Self {
38		Self {
39			pool_config: PoolConfig {
40				max_connections: 20,
41				min_idle: 5,
42				max_idle_time: Duration::from_secs(300),
43				connection_timeout: Duration::from_secs(30),
44				request_timeout: Duration::from_secs(60),
45				max_retries: 3,
46				retry_delay: Duration::from_millis(1000),
47			},
48			cache_config: CacheConfig {
49				max_entries: 10000,
50				default_ttl: Duration::from_secs(30),
51				cleanup_interval: Duration::from_secs(60),
52				enable_lru: true,
53			},
54			circuit_breaker_config: CircuitBreakerConfig {
55				failure_threshold: 5,
56				timeout: Duration::from_secs(60),
57				success_threshold: 3,
58				failure_window: Duration::from_secs(60),
59				half_open_max_requests: 3,
60			},
61			enable_logging: true,
62			enable_metrics: true,
63		}
64	}
65}
66
67/// Production client statistics
68#[derive(Debug, Default)]
69pub struct ProductionClientStats {
70	pub total_requests: u64,
71	pub cache_hits: u64,
72	pub cache_misses: u64,
73	pub circuit_breaker_rejections: u64,
74	pub successful_requests: u64,
75	pub failed_requests: u64,
76	pub average_response_time_ms: f64,
77}
78
79impl ProductionRpcClient {
80	/// Create a new production RPC client
81	pub fn new(endpoint: String, config: ProductionClientConfig) -> Self {
82		let pool = ConnectionPool::new(endpoint, config.pool_config.clone());
83		let cache = Cache::new(config.cache_config.clone());
84		let circuit_breaker = CircuitBreaker::new(config.circuit_breaker_config.clone());
85
86		Self {
87			pool,
88			cache,
89			circuit_breaker,
90			config,
91			stats: Arc::new(RwLock::new(ProductionClientStats::default())),
92		}
93	}
94
95	/// Execute an RPC call with full production features
96	pub async fn call(&self, method: &str, params: Vec<Value>) -> Neo3Result<Value> {
97		let start_time = std::time::Instant::now();
98
99		// Update total requests
100		{
101			let mut stats = self.stats.write().await;
102			stats.total_requests += 1;
103		}
104
105		// Create cache key
106		let cache_key = self.create_cache_key(method, &params);
107
108		// Check cache first for idempotent operations
109		if self.is_cacheable_method(method) {
110			if let Some(cached_result) = self.cache.get(&cache_key).await {
111				let mut stats = self.stats.write().await;
112				stats.cache_hits += 1;
113				return Ok(cached_result);
114			} else {
115				let mut stats = self.stats.write().await;
116				stats.cache_misses += 1;
117			}
118		}
119
120		// Clone params for the closure
121		let params_clone = params.clone();
122		let method_clone = method.to_string();
123
124		// Execute through circuit breaker
125		let result: Neo3Result<Value> = self
126			.circuit_breaker
127			.call(async move {
128				self.pool
129					.execute(move |client| {
130						let params_inner = params_clone.clone();
131						let method_inner = method_clone.clone();
132						Box::pin(async move {
133							client.request(&method_inner, params_inner).await.map_err(|e| {
134								Neo3Error::Network(crate::neo_error::NetworkError::RpcError {
135									code: -1,
136									message: e.to_string(),
137								})
138							})
139						})
140					})
141					.await
142			})
143			.await;
144
145		// Update statistics
146		let elapsed = start_time.elapsed();
147		let mut stats = self.stats.write().await;
148
149		match &result {
150			Ok(value) => {
151				stats.successful_requests += 1;
152
153				// Cache successful results for cacheable methods
154				if self.is_cacheable_method(method) {
155					let ttl = self.get_cache_ttl(method);
156					drop(stats);
157					self.cache.insert_with_ttl(cache_key, value.clone(), ttl).await;
158					stats = self.stats.write().await;
159				}
160			},
161			Err(_) => {
162				stats.failed_requests += 1;
163			},
164		}
165
166		// Update average response time
167		let total_requests = stats.successful_requests + stats.failed_requests;
168		if total_requests > 0 {
169			stats.average_response_time_ms = (stats.average_response_time_ms
170				* (total_requests - 1) as f64
171				+ elapsed.as_millis() as f64)
172				/ total_requests as f64;
173		}
174
175		if self.config.enable_logging {
176			match &result {
177				Ok(_) => {
178					tracing::info!(
179						method = method,
180						duration_ms = elapsed.as_millis(),
181						"RPC call successful"
182					);
183				},
184				Err(e) => {
185					tracing::error!(
186						method = method,
187						duration_ms = elapsed.as_millis(),
188						error = %e,
189						"RPC call failed"
190					);
191				},
192			}
193		}
194
195		result
196	}
197
198	/// Get current block count with caching
199	pub async fn get_block_count(&self) -> Neo3Result<u64> {
200		let result = self.call("getblockcount", vec![]).await?;
201		result.as_u64().ok_or_else(|| {
202			Neo3Error::Serialization(crate::neo_error::SerializationError::InvalidFormat(
203				"Invalid block count format".to_string(),
204			))
205		})
206	}
207
208	/// Get block by hash or index with long-term caching
209	pub async fn get_block(&self, identifier: Value) -> Neo3Result<Value> {
210		let result = self.call("getblock", vec![identifier.clone(), Value::Bool(true)]).await?;
211
212		// Cache blocks for longer since they're immutable
213		let cache_key = format!("block:{}", identifier);
214		self.cache
215			.insert_with_ttl(cache_key, result.clone(), Duration::from_secs(3600))
216			.await;
217
218		Ok(result)
219	}
220
221	/// Get transaction with long-term caching
222	pub async fn get_transaction(&self, tx_hash: String) -> Neo3Result<Value> {
223		let result = self
224			.call("getrawtransaction", vec![Value::String(tx_hash.clone()), Value::Bool(true)])
225			.await?;
226
227		// Cache transactions for longer since they're immutable
228		let cache_key = format!("tx:{}", tx_hash);
229		self.cache
230			.insert_with_ttl(cache_key, result.clone(), Duration::from_secs(3600))
231			.await;
232
233		Ok(result)
234	}
235
236	/// Get contract state with short-term caching
237	pub async fn get_contract_state(&self, contract_hash: String) -> Neo3Result<Value> {
238		let result = self
239			.call("getcontractstate", vec![Value::String(contract_hash.clone())])
240			.await?;
241
242		// Cache contract state for shorter time since it can change
243		let cache_key = format!("contract:{}", contract_hash);
244		self.cache
245			.insert_with_ttl(cache_key, result.clone(), Duration::from_secs(60))
246			.await;
247
248		Ok(result)
249	}
250
251	/// Get balance with very short-term caching
252	pub async fn get_nep17_balances(&self, address: String) -> Neo3Result<Value> {
253		let result = self.call("getnep17balances", vec![Value::String(address.clone())]).await?;
254
255		// Cache balances for very short time since they change frequently
256		let cache_key = format!("balance:{}", address);
257		self.cache
258			.insert_with_ttl(cache_key, result.clone(), Duration::from_secs(10))
259			.await;
260
261		Ok(result)
262	}
263
264	/// Send raw transaction (not cached)
265	pub async fn send_raw_transaction(&self, transaction_hex: String) -> Neo3Result<Value> {
266		self.call("sendrawtransaction", vec![Value::String(transaction_hex)]).await
267	}
268
269	/// Get production client statistics
270	pub async fn get_stats(&self) -> ProductionClientStats {
271		let stats = self.stats.read().await;
272		ProductionClientStats {
273			total_requests: stats.total_requests,
274			cache_hits: stats.cache_hits,
275			cache_misses: stats.cache_misses,
276			circuit_breaker_rejections: stats.circuit_breaker_rejections,
277			successful_requests: stats.successful_requests,
278			failed_requests: stats.failed_requests,
279			average_response_time_ms: stats.average_response_time_ms,
280		}
281	}
282
283	/// Get detailed health information
284	pub async fn get_health(&self) -> serde_json::Value {
285		let stats = self.get_stats().await;
286		let pool_stats = self.pool.get_stats().await;
287		let cache_stats = self.cache.stats().await;
288		let cb_stats = self.circuit_breaker.get_stats().await;
289
290		serde_json::json!({
291			"status": if cb_stats.current_state == crate::neo_clients::CircuitState::Open { "unhealthy" } else { "healthy" },
292			"timestamp": chrono::Utc::now().to_rfc3339(),
293			"stats": {
294				"total_requests": stats.total_requests,
295				"success_rate": if stats.total_requests > 0 {
296					stats.successful_requests as f64 / stats.total_requests as f64
297				} else { 0.0 },
298				"cache_hit_rate": cache_stats.hit_rate(),
299				"average_response_time_ms": stats.average_response_time_ms,
300				"circuit_breaker_state": format!("{:?}", cb_stats.current_state),
301				"pool": {
302					"active_connections": pool_stats.current_active_connections,
303					"idle_connections": pool_stats.current_idle_connections,
304					"total_created": pool_stats.total_connections_created
305				}
306			}
307		})
308	}
309
310	/// Perform health check by calling a simple RPC method
311	pub async fn health_check(&self) -> Neo3Result<bool> {
312		match self.call("getversion", vec![]).await {
313			Ok(_) => Ok(true),
314			Err(_) => Ok(false),
315		}
316	}
317
318	/// Create cache key for method and parameters
319	fn create_cache_key(&self, method: &str, params: &[Value]) -> String {
320		let params_str = serde_json::to_string(params).unwrap_or_default();
321		format!("{}:{}", method, params_str)
322	}
323
324	/// Check if a method should be cached
325	fn is_cacheable_method(&self, method: &str) -> bool {
326		matches!(
327			method,
328			"getblock"
329				| "getrawtransaction"
330				| "getcontractstate"
331				| "getnep17balances"
332				| "getblockcount"
333				| "getversion"
334				| "getpeers" | "getconnectioncount"
335		)
336	}
337
338	/// Get appropriate cache TTL for different methods
339	fn get_cache_ttl(&self, method: &str) -> Duration {
340		match method {
341			"getblock" | "getrawtransaction" => Duration::from_secs(3600), // 1 hour - immutable
342			"getcontractstate" => Duration::from_secs(60),                 // 1 minute - can change
343			"getnep17balances" => Duration::from_secs(10),                 // 10 seconds - changes frequently
344			"getblockcount" => Duration::from_secs(5), // 5 seconds - changes every ~15 seconds
345			_ => self.config.cache_config.default_ttl,
346		}
347	}
348}
349
350#[cfg(test)]
351mod tests {
352	use super::*;
353
354	#[tokio::test]
355	async fn test_production_client_creation() {
356		let config = ProductionClientConfig::default();
357		let client = ProductionRpcClient::new("https://testnet.neo.org:443".to_string(), config);
358
359		let stats = client.get_stats().await;
360		assert_eq!(stats.total_requests, 0);
361	}
362
363	#[tokio::test]
364	async fn test_cache_key_generation() {
365		let config = ProductionClientConfig::default();
366		let client = ProductionRpcClient::new("https://testnet.neo.org:443".to_string(), config);
367
368		let key1 = client.create_cache_key("getblock", &[Value::String("hash1".to_string())]);
369		let key2 = client.create_cache_key("getblock", &[Value::String("hash2".to_string())]);
370
371		assert_ne!(key1, key2);
372		assert!(key1.contains("getblock"));
373	}
374
375	#[tokio::test]
376	async fn test_cacheable_methods() {
377		let config = ProductionClientConfig::default();
378		let client = ProductionRpcClient::new("https://testnet.neo.org:443".to_string(), config);
379
380		assert!(client.is_cacheable_method("getblock"));
381		assert!(client.is_cacheable_method("getrawtransaction"));
382		assert!(!client.is_cacheable_method("sendrawtransaction"));
383	}
384}