Skip to content

Commit 532efcb

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

File tree

4 files changed

+157
-4
lines changed

4 files changed

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

crates/apollo_l1_endpoint_monitor/src/monitor.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +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>;
813

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+
919
#[derive(Debug, Clone)]
1020
pub struct L1EndpointMonitor {
1121
pub current_l1_endpoint_index: usize,
@@ -47,13 +57,14 @@ impl L1EndpointMonitor {
4757
&self.config.ordered_l1_endpoint_urls[index]
4858
}
4959

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.
5063
async fn is_operational(&self, l1_endpoint_index: usize) -> bool {
5164
let l1_endpoint_url = self.get_node_url(l1_endpoint_index);
5265
let l1_client = ProviderBuilder::new().on_http(l1_endpoint_url.clone());
53-
// Is this fast enough? we can use something to just check connectivity, but a recent infura
54-
// bug failed on this API even though connectivity was fine. Besides, this API is called for
55-
// most of our operations anyway.
56-
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()
5768
}
5869
}
5970

0 commit comments

Comments
 (0)