openzeppelin_monitor/services/blockchain/
pool.rs

1//! Client pool for managing blockchain clients.
2//!
3//! This module provides a thread-safe client pooling system that:
4//! - Caches blockchain clients by network
5//! - Creates clients lazily on first use
6//! - Handles both EVM and Stellar clients
7//! - Provides type-safe access to clients
8//! - Manages client lifecycles automatically
9//!
10//! The pool uses a fast path for existing clients and a slow path for
11//! creating new ones, optimizing performance while maintaining safety.
12
13use crate::utils::client_storage::ClientStorage;
14use crate::{
15	models::{BlockChainType, Network},
16	services::blockchain::{
17		BlockChainClient, BlockFilterFactory, EVMTransportClient, EvmClient, EvmClientTrait,
18		StellarClient, StellarClientTrait, StellarTransportClient,
19	},
20};
21use anyhow::Context;
22use async_trait::async_trait;
23use futures::future::BoxFuture;
24use std::{any::Any, collections::HashMap, sync::Arc};
25
26/// Trait for the client pool.
27#[async_trait]
28pub trait ClientPoolTrait: Send + Sync {
29	type EvmClient: EvmClientTrait + BlockChainClient + BlockFilterFactory<Self::EvmClient>;
30	type StellarClient: StellarClientTrait
31		+ BlockChainClient
32		+ BlockFilterFactory<Self::StellarClient>;
33
34	async fn get_evm_client(
35		&self,
36		network: &Network,
37	) -> Result<Arc<Self::EvmClient>, anyhow::Error>;
38	async fn get_stellar_client(
39		&self,
40		network: &Network,
41	) -> Result<Arc<Self::StellarClient>, anyhow::Error>;
42}
43
44/// Main client pool manager that handles multiple blockchain types.
45///
46/// Provides type-safe access to cached blockchain clients. Clients are created
47/// on demand when first requested and then cached for future use. Uses RwLock
48/// for thread-safe access and Arc for shared ownership.
49pub struct ClientPool {
50	/// Map of client storages indexed by client type
51	pub storages: HashMap<BlockChainType, Box<dyn Any + Send + Sync>>,
52}
53
54impl ClientPool {
55	/// Creates a new empty client pool.
56	///
57	/// Initializes empty hashmaps for both EVM and Stellar clients.
58	pub fn new() -> Self {
59		let mut pool = Self {
60			storages: HashMap::new(),
61		};
62
63		// Register client types
64		pool.register_client_type::<EvmClient<EVMTransportClient>>(BlockChainType::EVM);
65		pool.register_client_type::<StellarClient<StellarTransportClient>>(BlockChainType::Stellar);
66
67		pool
68	}
69
70	fn register_client_type<T: 'static + Send + Sync>(&mut self, client_type: BlockChainType) {
71		self.storages
72			.insert(client_type, Box::new(ClientStorage::<T>::new()));
73	}
74
75	/// Internal helper method to get or create a client of any type.
76	///
77	/// Uses a double-checked locking pattern:
78	/// 1. Fast path with read lock to check for existing client
79	/// 2. Slow path with write lock to create new client if needed
80	///
81	/// This ensures thread-safety while maintaining good performance
82	/// for the common case of accessing existing clients.
83	async fn get_or_create_client<T: BlockChainClient + 'static>(
84		&self,
85		client_type: BlockChainType,
86		network: &Network,
87		create_fn: impl Fn(&Network) -> BoxFuture<'static, Result<T, anyhow::Error>>,
88	) -> Result<Arc<T>, anyhow::Error> {
89		let storage = self
90			.storages
91			.get(&client_type)
92			.and_then(|s| s.downcast_ref::<ClientStorage<T>>())
93			.with_context(|| "Invalid client type")?;
94
95		// Fast path: check if client exists
96		if let Some(client) = storage.clients.read().await.get(&network.slug) {
97			return Ok(client.clone());
98		}
99
100		// Slow path: create new client
101		let mut clients = storage.clients.write().await;
102		let client = Arc::new(create_fn(network).await?);
103		clients.insert(network.slug.clone(), client.clone());
104		Ok(client)
105	}
106
107	/// Get the number of clients for a given client type.
108	pub async fn get_client_count<T: 'static>(&self, client_type: BlockChainType) -> usize {
109		match self
110			.storages
111			.get(&client_type)
112			.and_then(|s| s.downcast_ref::<ClientStorage<T>>())
113		{
114			Some(storage) => storage.clients.read().await.len(),
115			None => 0,
116		}
117	}
118}
119
120#[async_trait]
121impl ClientPoolTrait for ClientPool {
122	type EvmClient = EvmClient<EVMTransportClient>;
123	type StellarClient = StellarClient<StellarTransportClient>;
124
125	/// Gets or creates an EVM client for the given network.
126	///
127	/// First checks the cache for an existing client. If none exists,
128	/// creates a new client under a write lock.
129	async fn get_evm_client(
130		&self,
131		network: &Network,
132	) -> Result<Arc<Self::EvmClient>, anyhow::Error> {
133		self.get_or_create_client(BlockChainType::EVM, network, |n| {
134			let network = n.clone();
135			Box::pin(async move { Self::EvmClient::new(&network).await })
136		})
137		.await
138		.with_context(|| "Failed to get or create EVM client")
139	}
140
141	/// Gets or creates a Stellar client for the given network.
142	///
143	/// First checks the cache for an existing client. If none exists,
144	/// creates a new client under a write lock.
145	async fn get_stellar_client(
146		&self,
147		network: &Network,
148	) -> Result<Arc<Self::StellarClient>, anyhow::Error> {
149		self.get_or_create_client(BlockChainType::Stellar, network, |n| {
150			let network = n.clone();
151			Box::pin(async move { Self::StellarClient::new(&network).await })
152		})
153		.await
154		.with_context(|| "Failed to get or create Stellar client")
155	}
156}
157
158impl Default for ClientPool {
159	fn default() -> Self {
160		Self::new()
161	}
162}