Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions chain/ethereum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ tiny-keccak = "1.5.0"
hex = "0.4.3"
semver = "1.0.26"
thiserror = { workspace = true }
tokio = { version = "1", features = ["full"] }

itertools = "0.14.0"

Expand Down
71 changes: 71 additions & 0 deletions chain/ethereum/src/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use crate::adapter::EthereumAdapter as EthereumAdapterTrait;
use crate::EthereumAdapter;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use tokio::time::sleep;
#[derive(Debug)]
pub struct Health {
pub provider: Arc<EthereumAdapter>,
latency: Arc<RwLock<Duration>>,
error_rate: Arc<RwLock<f64>>,
consecutive_failures: Arc<RwLock<u32>>,
}

impl Health {
pub fn new(provider: Arc<EthereumAdapter>) -> Self {
Self {
provider,
latency: Arc::new(RwLock::new(Duration::from_secs(0))),
error_rate: Arc::new(RwLock::new(0.0)),
consecutive_failures: Arc::new(RwLock::new(0)),
}
}

pub fn provider(&self) -> &str {
self.provider.provider()
}

pub async fn check(&self) {
let start_time = Instant::now();
// For now, we'll just simulate a health check.
// In a real implementation, we would send a request to the provider.
let success = self.provider.provider().contains("rpc1"); // Simulate a failure for rpc2
let latency = start_time.elapsed();

self.update_metrics(success, latency);
}
Comment on lines +28 to +36
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health check implementation is using a simulated check that only checks if the provider name contains "rpc1". This is placeholder code that should not be in production. The health checking system should perform actual RPC calls (e.g., using latest_block_header or net_identifiers) to verify provider health instead of using a hardcoded simulation based on provider names.

Copilot uses AI. Check for mistakes.

fn update_metrics(&self, success: bool, latency: Duration) {
let mut latency_w = self.latency.write().unwrap();
*latency_w = latency;

let mut error_rate_w = self.error_rate.write().unwrap();
let mut consecutive_failures_w = self.consecutive_failures.write().unwrap();

if success {
*error_rate_w = *error_rate_w * 0.9; // Decay the error rate
*consecutive_failures_w = 0;
} else {
*error_rate_w = *error_rate_w * 0.9 + 0.1; // Increase the error rate
*consecutive_failures_w += 1;
}
}

pub fn score(&self) -> f64 {
let latency = *self.latency.read().unwrap();
let error_rate = *self.error_rate.read().unwrap();
let consecutive_failures = *self.consecutive_failures.read().unwrap();

// This is a simple scoring algorithm. A more sophisticated algorithm could be used here.
1.0 / (1.0 + latency.as_secs_f64() + error_rate + (consecutive_failures as f64))
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health score can theoretically be close to zero for providers with high latency, error rates, or consecutive failures. When multiplied with the weight in select_weighted_adapter (line 285), this could result in all weights being effectively zero, causing WeightedIndex::new to fail. While the code does fallback to random selection, a better approach would be to clamp the score to a minimum value (e.g., 0.01) to ensure there's always some probability of selection, or handle the all-zero case more explicitly.

Suggested change
1.0 / (1.0 + latency.as_secs_f64() + error_rate + (consecutive_failures as f64))
let raw_score = 1.0 / (1.0 + latency.as_secs_f64() + error_rate + (consecutive_failures as f64));
// Clamp to a small positive minimum to avoid effectively zero weights downstream.
raw_score.max(0.01)

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +60
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of unwrap() on RwLock read/write operations can cause panics if the lock is poisoned (e.g., if a thread panicked while holding the lock). In a production system, this could cause cascading failures. Consider using expect() with descriptive messages or properly handling the Result to provide better error recovery.

Copilot uses AI. Check for mistakes.
}
}

pub async fn health_check_task(health_checkers: Vec<Arc<Health>>) {
loop {
for health_checker in &health_checkers {
health_checker.check().await;
}
sleep(Duration::from_secs(10)).await;
}
}
1 change: 1 addition & 0 deletions chain/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod codec;
mod data_source;
mod env;
mod ethereum_adapter;
pub mod health;
mod ingestor;
mod polling_block_stream;
pub mod runtime;
Expand Down
Loading