openzeppelin_relayer/utils/
rpc_selector.rsuse std::sync::{atomic::AtomicUsize, Arc};
use eyre::Result;
use rand::distr::weighted::WeightedIndex;
use rand::prelude::*;
use serde::Serialize;
use thiserror::Error;
use crate::models::RpcConfig;
#[derive(Error, Debug, Serialize)]
pub enum RpcSelectorError {
#[error("No providers available")]
NoProviders,
#[error("Client initialization failed: {0}")]
ClientInitializationError(String),
}
pub fn create_weights_distribution(configs: &[RpcConfig]) -> Option<Arc<WeightedIndex<u8>>> {
if configs.len() <= 1 {
return None;
}
let weights: Vec<u8> = configs.iter().map(|config| config.get_weight()).collect();
if weights.iter().all(|&w| w == weights[0]) {
None
} else {
match WeightedIndex::new(&weights) {
Ok(dist) => Some(Arc::new(dist)),
Err(_) => None,
}
}
}
#[derive(Debug)]
pub struct RpcSelector {
configs: Vec<RpcConfig>,
weights_dist: Option<Arc<WeightedIndex<u8>>>,
next_index: Arc<AtomicUsize>,
}
impl RpcSelector {
pub fn new(configs: Vec<RpcConfig>) -> Result<Self, RpcSelectorError> {
if configs.is_empty() {
return Err(RpcSelectorError::NoProviders);
}
let weights_dist = create_weights_distribution(&configs);
Ok(Self {
configs,
weights_dist,
next_index: Arc::new(AtomicUsize::new(0)),
})
}
fn select_url(&self) -> Result<&str, RpcSelectorError> {
if self.configs.is_empty() {
return Err(RpcSelectorError::NoProviders);
}
if self.configs.len() == 1 {
return Ok(&self.configs[0].url);
}
if let Some(dist) = &self.weights_dist {
let mut rng = rand::rng();
let index = dist.sample(&mut rng);
return Ok(&self.configs[index].url);
}
let len = self.configs.len();
let index = self
.next_index
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
% len;
Ok(&self.configs[index].url)
}
}
impl RpcSelector {
pub fn get_client<T>(
&self,
initializer: impl Fn(&str) -> Result<T>,
) -> Result<T, RpcSelectorError> {
let url = self.select_url()?;
initializer(url).map_err(|e| {
RpcSelectorError::ClientInitializationError(format!(
"Client initialization failed: {}",
e
))
})
}
}
impl Clone for RpcSelector {
fn clone(&self) -> Self {
Self {
configs: self.configs.clone(),
weights_dist: self.weights_dist.clone(),
next_index: self.next_index.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_weights_distribution_single_config() {
let configs = vec![RpcConfig {
url: "https://example.com/rpc".to_string(),
weight: 1,
}];
let result = create_weights_distribution(&configs);
assert!(result.is_none());
}
#[test]
fn test_create_weights_distribution_equal_weights() {
let configs = vec![
RpcConfig {
url: "https://example1.com/rpc".to_string(),
weight: 5,
},
RpcConfig {
url: "https://example2.com/rpc".to_string(),
weight: 5,
},
RpcConfig {
url: "https://example3.com/rpc".to_string(),
weight: 5,
},
];
let result = create_weights_distribution(&configs);
assert!(result.is_none());
}
#[test]
fn test_create_weights_distribution_different_weights() {
let configs = vec![
RpcConfig {
url: "https://example1.com/rpc".to_string(),
weight: 1,
},
RpcConfig {
url: "https://example2.com/rpc".to_string(),
weight: 2,
},
RpcConfig {
url: "https://example3.com/rpc".to_string(),
weight: 3,
},
];
let result = create_weights_distribution(&configs);
assert!(result.is_some());
}
#[test]
fn test_rpc_selector_new_empty_configs() {
let configs: Vec<RpcConfig> = vec![];
let result = RpcSelector::new(configs);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), RpcSelectorError::NoProviders));
}
#[test]
fn test_rpc_selector_new_single_config() {
let configs = vec![RpcConfig {
url: "https://example.com/rpc".to_string(),
weight: 1,
}];
let result = RpcSelector::new(configs);
assert!(result.is_ok());
let selector = result.unwrap();
assert!(selector.weights_dist.is_none());
}
#[test]
fn test_rpc_selector_new_multiple_equal_weights() {
let configs = vec![
RpcConfig {
url: "https://example1.com/rpc".to_string(),
weight: 5,
},
RpcConfig {
url: "https://example2.com/rpc".to_string(),
weight: 5,
},
];
let result = RpcSelector::new(configs);
assert!(result.is_ok());
let selector = result.unwrap();
assert!(selector.weights_dist.is_none());
}
#[test]
fn test_rpc_selector_new_multiple_different_weights() {
let configs = vec![
RpcConfig {
url: "https://example1.com/rpc".to_string(),
weight: 1,
},
RpcConfig {
url: "https://example2.com/rpc".to_string(),
weight: 3,
},
];
let result = RpcSelector::new(configs);
assert!(result.is_ok());
let selector = result.unwrap();
assert!(selector.weights_dist.is_some());
}
#[test]
fn test_rpc_selector_select_url_single_provider() {
let configs = vec![RpcConfig {
url: "https://example.com/rpc".to_string(),
weight: 1,
}];
let selector = RpcSelector::new(configs).unwrap();
let result = selector.select_url();
assert!(result.is_ok());
assert_eq!(result.unwrap(), "https://example.com/rpc");
}
#[test]
fn test_rpc_selector_select_url_round_robin() {
let configs = vec![
RpcConfig {
url: "https://example1.com/rpc".to_string(),
weight: 1,
},
RpcConfig {
url: "https://example2.com/rpc".to_string(),
weight: 1,
},
];
let selector = RpcSelector::new(configs).unwrap();
let first_url = selector.select_url().unwrap();
let second_url = selector.select_url().unwrap();
let third_url = selector.select_url().unwrap();
assert_ne!(first_url, second_url);
assert_eq!(first_url, third_url);
}
#[test]
fn test_rpc_selector_get_client_success() {
let configs = vec![RpcConfig {
url: "https://example.com/rpc".to_string(),
weight: 1,
}];
let selector = RpcSelector::new(configs).unwrap();
let initializer = |url: &str| -> Result<String> { Ok(url.to_string()) };
let result = selector.get_client(initializer);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "https://example.com/rpc");
}
#[test]
fn test_rpc_selector_get_client_failure() {
let configs = vec![RpcConfig {
url: "https://example.com/rpc".to_string(),
weight: 1,
}];
let selector = RpcSelector::new(configs).unwrap();
let initializer =
|_url: &str| -> Result<String> { Err(eyre::eyre!("Initialization error")) };
let result = selector.get_client(initializer);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
RpcSelectorError::ClientInitializationError(_)
));
}
#[test]
fn test_rpc_selector_clone() {
let configs = vec![
RpcConfig {
url: "https://example1.com/rpc".to_string(),
weight: 1,
},
RpcConfig {
url: "https://example2.com/rpc".to_string(),
weight: 3,
},
];
let selector = RpcSelector::new(configs).unwrap();
let cloned = selector.clone();
assert_eq!(selector.configs.len(), cloned.configs.len());
assert_eq!(selector.configs[0].url, cloned.configs[0].url);
assert_eq!(selector.configs[1].url, cloned.configs[1].url);
assert_eq!(
selector.weights_dist.is_some(),
cloned.weights_dist.is_some()
);
}
}