Skip to content

Commit c04f4cb

Browse files
author
Gilad Chase
committed
chore(apollo_l1_endpoint_monitor): extract RPC call to const and add tests
1 parent f1a50b9 commit c04f4cb

File tree

4 files changed

+154
-4
lines changed

4 files changed

+154
-4
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/apollo_l1_endpoint_monitor/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ tracing.workspace = true
1313
url = { workspace = true, features = ["serde"] }
1414

1515
[dev-dependencies]
16+
mockito.workspace = true
17+
tokio.workspace = true
1618

1719
[lints]
1820
workspace = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use mockito::{Matcher, Server, ServerGuard};
2+
use url::Url;
3+
4+
use crate::monitor::{
5+
L1EndpointMonitor,
6+
L1EndpointMonitorConfig,
7+
L1EndpointMonitorError,
8+
HEALTH_CHECK_RPC_METHOD,
9+
};
10+
11+
// Unreachable localhost endpoints for simulating failures.
12+
// Using localhost to prevent IO (so don't switch to example.com in order to avoid port issues).
13+
// Using these ports since they are not well-used ports in unix and privileged (<1024),
14+
// so unless the user runs as root and binds them explicitly, they should be closed.
15+
const BAD_ENDPOINT_1: &str = "http://localhost:1";
16+
const BAD_ENDPOINT_2: &str = "http://localhost:2";
17+
18+
// Helper to assert the active URL and current index in one call.
19+
async fn check_get_active_l1_endpoint_success(
20+
monitor: &mut L1EndpointMonitor,
21+
expected_returned_url: &Url,
22+
expected_index_of_returned_url: usize,
23+
) {
24+
let active = monitor.get_active_l1_endpoint().await.unwrap();
25+
assert_eq!(&active, expected_returned_url);
26+
assert_eq!(monitor.current_l1_endpoint_index, expected_index_of_returned_url);
27+
}
28+
29+
fn url(url: &str) -> Url {
30+
Url::parse(url).unwrap()
31+
}
32+
33+
/// Used to mock an L1 endpoint, like infura.
34+
/// This can be replaced by Anvil, but for unit tests it isn't worth the large overhead Anvil
35+
/// entails, given that we only need a valid HTTP response from the given url to test the API.
36+
pub struct MockL1Endpoint {
37+
pub url: Url,
38+
pub endpoint: ServerGuard,
39+
}
40+
41+
async fn mock_working_l1_endpoint() -> MockL1Endpoint {
42+
// Very simple mock is all we need _for now_: create a thin http server that expect a single
43+
// call to the given API and return a valid response. Note that the validity of the response
44+
// is coupled with the RPC method used. Server is dropped when the guard drops.
45+
let mut server_guard = Server::new_async().await;
46+
server_guard
47+
.mock("POST", "/")
48+
// Catch this specific RPC method.
49+
.match_body(Matcher::PartialJsonString(format!(
50+
r#"{{ "method": "{}"}}"#,
51+
HEALTH_CHECK_RPC_METHOD
52+
)))
53+
.with_status(200)
54+
// Return 2_u64 as a valid response for the method.
55+
.with_body(r#"{"jsonrpc":"2.0","id":1,"result":"0x2"}"#)
56+
.create_async()
57+
.await;
58+
59+
let url = Url::parse(&server_guard.url()).unwrap();
60+
MockL1Endpoint { url, endpoint: server_guard }
61+
}
62+
63+
#[tokio::test]
64+
async fn non_responsive_skips_to_next() {
65+
// Setup.
66+
let endpoint = mock_working_l1_endpoint().await;
67+
let good_endpoint = endpoint.url.clone();
68+
69+
let mut monitor = L1EndpointMonitor {
70+
current_l1_endpoint_index: 0,
71+
config: L1EndpointMonitorConfig {
72+
ordered_l1_endpoint_urls: vec![url(BAD_ENDPOINT_1), good_endpoint.clone()],
73+
},
74+
};
75+
76+
// Test.
77+
check_get_active_l1_endpoint_success(&mut monitor, &good_endpoint, 1).await;
78+
}
79+
80+
#[tokio::test]
81+
async fn current_endpoint_still_works() {
82+
// Setup.
83+
let endpoint = mock_working_l1_endpoint().await;
84+
let good_endpoint = endpoint.url.clone();
85+
86+
let mut monitor = L1EndpointMonitor {
87+
current_l1_endpoint_index: 1,
88+
config: L1EndpointMonitorConfig {
89+
ordered_l1_endpoint_urls: vec![
90+
url(BAD_ENDPOINT_1),
91+
good_endpoint.clone(),
92+
url(BAD_ENDPOINT_2),
93+
],
94+
},
95+
};
96+
97+
// Test.
98+
check_get_active_l1_endpoint_success(&mut monitor, &good_endpoint, 1).await;
99+
}
100+
101+
#[tokio::test]
102+
async fn wrap_around_success() {
103+
// Setup.
104+
let endpoint = mock_working_l1_endpoint().await;
105+
let good_url = endpoint.url.clone();
106+
107+
let mut monitor = L1EndpointMonitor {
108+
current_l1_endpoint_index: 2,
109+
config: L1EndpointMonitorConfig {
110+
ordered_l1_endpoint_urls: vec![
111+
url(BAD_ENDPOINT_1),
112+
good_url.clone(),
113+
url(BAD_ENDPOINT_2),
114+
],
115+
},
116+
};
117+
118+
// Test.
119+
check_get_active_l1_endpoint_success(&mut monitor, &good_url, 1).await;
120+
}
121+
122+
#[tokio::test]
123+
async fn all_down_fails() {
124+
let mut monitor = L1EndpointMonitor {
125+
current_l1_endpoint_index: 0,
126+
config: L1EndpointMonitorConfig {
127+
ordered_l1_endpoint_urls: vec![url(BAD_ENDPOINT_1), url(BAD_ENDPOINT_2)],
128+
},
129+
};
130+
131+
let result = monitor.get_active_l1_endpoint().await;
132+
assert_eq!(result, Err(L1EndpointMonitorError::NoActiveL1Endpoint));
133+
assert_eq!(monitor.current_l1_endpoint_index, 0);
134+
}

crates/apollo_l1_endpoint_monitor/src/monitor.rs

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
use alloy::primitives::U64;
12
use alloy::providers::{Provider, ProviderBuilder};
23
use serde::{Deserialize, Serialize};
34
use thiserror::Error;
45
use tracing::{error, warn};
56
use url::Url;
67

8+
#[cfg(test)]
9+
#[path = "l1_endpoint_monitor_tests.rs"]
10+
pub mod l1_endpoint_monitor_tests;
11+
712
type L1EndpointMonitorResult<T> = Result<T, L1EndpointMonitorError>;
13+
14+
/// The JSON-RPC method used to check L1 endpoint health.
15+
// Note: is this fast enough? Alternatively, we can just check connectivity, but we already hit
16+
// a bug in infura where the connectivity was fine, but get_block_number() failed.
17+
pub const HEALTH_CHECK_RPC_METHOD: &str = "eth_blockNumber";
18+
819
#[derive(Debug, Clone)]
920
pub struct L1EndpointMonitor {
1021
pub current_l1_endpoint_index: usize,
@@ -46,13 +57,14 @@ impl L1EndpointMonitor {
4657
&self.config.ordered_l1_endpoint_urls[index]
4758
}
4859

60+
/// Check if the L1 endpoint is operational by sending a carefully-chosen request to it.
61+
// note: Using a raw request instead of just alloy API (like `get_block_number()`) to improve
62+
// high-level readability (through a dedicated const) and to improve testability.
4963
async fn is_operational(&self, l1_endpoint_index: usize) -> bool {
5064
let l1_endpoint_url = self.get_node_url(l1_endpoint_index);
5165
let l1_client = ProviderBuilder::new().on_http(l1_endpoint_url.clone());
52-
// Is this fast enough? we can use something to just check connectivity, but a recent infura
53-
// bug failed on this API even though connectivity was fine. Besides, this API is called for
54-
// most of our operations anyway.
55-
l1_client.get_block_number().await.is_ok()
66+
// Note: response type annotation is coupled with the rpc method used.
67+
l1_client.client().request_noparams::<U64>(HEALTH_CHECK_RPC_METHOD).await.is_ok()
5668
}
5769
}
5870

0 commit comments

Comments
 (0)