Skip to content
Merged
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: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vixy"
version = "0.1.3"
version = "0.1.4"
edition = "2024"
description = "A Rust proxy that monitors Ethereum EL and CL nodes, tracks their health, and routes requests to healthy nodes"
license = "MIT"
Expand Down
20 changes: 20 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ pub struct Global {
pub health_check_max_failures: u32,
/// Maximum request body size in bytes (default: unlimited)
pub max_body_size: usize,
/// Whether to mark an EL node unhealthy when its `newHeads` subscription stalls
/// (stops advancing) even though HTTP polling still succeeds. Detects upstreams
/// that serve a stale head over the subscription while otherwise looking healthy.
pub subscription_health_enabled: bool,
/// Time in milliseconds without a new head on vixy's own `newHeads` probe
/// subscription before the probe drops the connection and reconnects to
/// re-establish a fresh subscription.
pub subscription_stall_timeout_ms: u64,
}

/// Metrics configuration settings
Expand Down Expand Up @@ -69,6 +77,8 @@ impl Default for Global {
max_retries: 2,
health_check_max_failures: 3,
max_body_size: usize::MAX,
subscription_health_enabled: true,
subscription_stall_timeout_ms: 30000,
}
}
}
Expand Down Expand Up @@ -178,6 +188,16 @@ impl Config {
fn validate(&self) -> Result<()> {
self.el.validate().wrap_err("invalid EL configuration")?;

if self.global.subscription_health_enabled && self.global.subscription_stall_timeout_ms == 0
{
return Err(ConfigError::ValidationFailed(
"subscription_stall_timeout_ms must be greater than 0 when \
subscription_health_enabled is true"
.to_string(),
)
.into());
}

if self.cl.is_empty() {
return Err(ConfigError::ValidationFailed(
"at least one CL node is required".to_string(),
Expand Down
25 changes: 24 additions & 1 deletion src/health/el.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ pub fn calculate_el_health(
// Calculate lag (how far behind the node is from chain head)
node.lag = chain_head.saturating_sub(node.block_number);

// Determine if this check passed (check succeeded AND lag is within threshold)
// Determine if this check passed (check succeeded AND lag is within threshold).
// NOTE: newHeads-subscription freshness is deliberately NOT part of is_healthy —
// it is a separate, WebSocket-only signal (see state::ElNodeState::subscription_healthy
// and proxy::selection::select_el_ws_node) so a stalled subscription never diverts
// HTTP traffic or flips the failover flag.
let check_passed = node.check_ok && node.lag <= max_lag;

if check_passed {
Expand Down Expand Up @@ -236,6 +240,9 @@ mod tests {
is_healthy: false,
lag: 0,
consecutive_failures: 0,
sub_block_number: block_number,
sub_last_head_at: None,
subscription_healthy: true,
}
}

Expand Down Expand Up @@ -378,6 +385,22 @@ mod tests {
assert!(node.is_healthy, "Node should be healthy after recovery");
}

#[test]
fn test_subscription_staleness_does_not_affect_is_healthy() {
// A stalled newHeads subscription must NOT make is_healthy false: subscription
// freshness is a separate, WS-only signal. HTTP health depends only on the poll.
let mut node = make_el_node("test", 1000, true);
node.subscription_healthy = false; // subscription stale...
let chain_head = 1000;

calculate_el_health(&mut node, chain_head, 5, 3);

assert!(
node.is_healthy,
"is_healthy must reflect HTTP poll only, independent of subscription freshness"
);
}

// =========================================================================
// update_el_chain_head tests
// =========================================================================
Expand Down
1 change: 1 addition & 0 deletions src/health/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

pub mod cl;
pub mod el;
pub mod subscription;
Loading
Loading