Skip to content

Commit 1738886

Browse files
committed
Added a unit test for the config filewatcher
1 parent 013943a commit 1738886

7 files changed

Lines changed: 195 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/pbs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async fn main() -> Result<()> {
1515

1616
let _args = cb_cli::PbsArgs::parse();
1717

18-
let (pbs_config, config_path) = load_pbs_config().await?;
18+
let (pbs_config, config_path) = load_pbs_config(None).await?;
1919

2020
PbsService::init_metrics(pbs_config.chain)?;
2121
let state = PbsState::new(pbs_config, config_path);

crates/common/src/config/pbs.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,11 @@ fn default_pbs() -> String {
243243
}
244244

245245
/// Loads the default pbs config, i.e. with no signer client or custom data
246-
pub async fn load_pbs_config() -> Result<(PbsModuleConfig, PathBuf)> {
247-
let (config, config_path) = CommitBoostConfig::from_env_path()?;
246+
pub async fn load_pbs_config(config_path: Option<PathBuf>) -> Result<(PbsModuleConfig, PathBuf)> {
247+
let (config, config_path) = match config_path {
248+
Some(path) => (CommitBoostConfig::from_file(&path)?, path),
249+
None => CommitBoostConfig::from_env_path()?,
250+
};
248251
config.validate().await?;
249252

250253
// Make sure relays isn't empty - since the config is still technically valid if

crates/pbs/src/mev_boost/reload.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{BuilderApiState, PbsState};
66
/// Reload the PBS state with the latest configuration in the config file
77
/// Returns 200 if successful or 500 if failed
88
pub async fn reload<S: BuilderApiState>(state: PbsState<S>) -> eyre::Result<PbsState<S>> {
9-
let (pbs_config, config_path) = load_pbs_config().await?;
9+
let (pbs_config, config_path) = load_pbs_config(None).await?;
1010
let new_state = PbsState::new(pbs_config, config_path).with_data(state.data);
1111

1212
if state.config.pbs_config.host != new_state.config.pbs_config.host {

crates/pbs/src/service.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,36 @@ impl PbsService {
6262
}
6363

6464
// Set up the filesystem watcher for the config file
65-
let state_for_watcher = state.clone();
66-
let mut watcher = RecommendedWatcher::new(
67-
move |result: Result<Event, Error>| {
68-
let event = result.unwrap();
69-
if !event.kind.is_modify() {
70-
return;
71-
}
72-
73-
// Reload the configuration when the file is modified
74-
let result = futures::executor::block_on(load_pbs_config());
75-
match result {
76-
Ok((new_config, _)) => {
77-
let mut state = state_for_watcher.write();
78-
state.config = Arc::new(new_config);
79-
info!("configuration reloaded from file after update");
65+
let mut watcher: RecommendedWatcher;
66+
if config_path.to_str() != Some("") {
67+
let state_for_watcher = state.clone();
68+
let config_path_for_watcher = config_path.clone();
69+
watcher = RecommendedWatcher::new(
70+
move |result: Result<Event, Error>| {
71+
let event = result.unwrap();
72+
if !event.kind.is_modify() {
73+
return;
8074
}
81-
Err(err) => {
82-
warn!(%err, "failed to reload configuration from file after update");
75+
76+
// Reload the configuration when the file is modified
77+
let result = futures::executor::block_on(load_pbs_config(Some(
78+
config_path_for_watcher.to_path_buf(),
79+
)));
80+
match result {
81+
Ok((new_config, _)) => {
82+
let mut state = state_for_watcher.write();
83+
state.config = Arc::new(new_config);
84+
info!("configuration reloaded from file after update");
85+
}
86+
Err(err) => {
87+
warn!(%err, "failed to reload configuration from file after update");
88+
}
8389
}
84-
}
85-
},
86-
notify::Config::default(),
87-
)?;
88-
watcher.watch(config_path.as_path(), RecursiveMode::Recursive)?;
90+
},
91+
notify::Config::default(),
92+
)?;
93+
watcher.watch(config_path.as_path(), RecursiveMode::Recursive)?;
94+
}
8995

9096
// Run the registry refresher task
9197
if is_refreshing_required {

tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ reqwest.workspace = true
1616
serde_json.workspace = true
1717
tempfile.workspace = true
1818
tokio.workspace = true
19+
toml.workspace = true
1920
tracing.workspace = true
2021
tracing-subscriber.workspace = true
2122
tracing-test.workspace = true

tests/tests/pbs_cfg_file_update.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
2+
3+
use alloy::primitives::U256;
4+
use cb_common::{
5+
config::{CommitBoostConfig, LogsSettings, PbsConfig, RelayConfig, StaticPbsConfig},
6+
pbs::RelayEntry,
7+
signer::random_secret,
8+
types::Chain,
9+
};
10+
use cb_pbs::{DefaultBuilderApi, PbsService, PbsState};
11+
use cb_tests::{
12+
mock_relay::{MockRelayState, start_mock_relay_service},
13+
mock_validator::MockValidator,
14+
utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config},
15+
};
16+
use eyre::Result;
17+
use reqwest::StatusCode;
18+
use tracing::info;
19+
use url::Url;
20+
21+
/// Updates the config file that was used to load the PBS config, and ensures
22+
/// the filesystem watcher triggers a reload of the configuration.
23+
#[tokio::test]
24+
async fn test_cfg_file_update() -> Result<()> {
25+
// Random keys needed for the relays to start
26+
setup_test_env();
27+
let signer = random_secret();
28+
let pubkey = signer.public_key();
29+
30+
let chain = Chain::Hoodi;
31+
let pbs_port = 3720;
32+
33+
// Start relay 1
34+
let relay1_port = pbs_port + 1;
35+
let relay1 = generate_mock_relay(relay1_port, pubkey.clone())?;
36+
let relay1_state = Arc::new(MockRelayState::new(chain, signer.clone()));
37+
tokio::spawn(start_mock_relay_service(relay1_state.clone(), relay1_port));
38+
39+
// Start relay 2
40+
let relay2_port = relay1_port + 1;
41+
let relay2 = generate_mock_relay(relay2_port, pubkey.clone())?;
42+
let relay2_id = relay2.id.clone().to_string();
43+
let relay2_state = Arc::new(MockRelayState::new(chain, signer));
44+
tokio::spawn(start_mock_relay_service(relay2_state.clone(), relay2_port));
45+
46+
// Make a config with relay 1 only
47+
let pbs_config = PbsConfig {
48+
// get_pbs_static_config(pbs_port);
49+
host: Ipv4Addr::LOCALHOST,
50+
port: pbs_port,
51+
relay_check: false,
52+
wait_all_registrations: false,
53+
timeout_get_header_ms: 950,
54+
timeout_get_payload_ms: 4000,
55+
timeout_register_validator_ms: 3000,
56+
skip_sigverify: true,
57+
min_bid_wei: U256::ZERO,
58+
late_in_slot_time_ms: u64::MAX / 2, /* serde gets very upset about serializing u64::MAX
59+
* or anything close to it */
60+
extra_validation_enabled: false,
61+
rpc_url: None,
62+
ssv_api_url: Url::parse("http://example.com").unwrap(),
63+
http_timeout_seconds: 10,
64+
register_validator_retry_limit: 3,
65+
validator_registration_batch_size: None,
66+
mux_registry_refresh_interval_seconds: 384,
67+
};
68+
let cb_config = CommitBoostConfig {
69+
chain,
70+
pbs: StaticPbsConfig {
71+
docker_image: String::new(),
72+
pbs_config: pbs_config.clone(),
73+
with_signer: false,
74+
},
75+
muxes: None,
76+
modules: None,
77+
signer: None,
78+
logs: LogsSettings::default(),
79+
metrics: None,
80+
relays: vec![RelayConfig {
81+
id: Some(relay1.id.to_string()),
82+
enable_timing_games: false,
83+
frequency_get_header_ms: None,
84+
get_params: None,
85+
headers: None,
86+
target_first_request_ms: None,
87+
validator_registration_batch_size: None,
88+
entry: RelayEntry {
89+
id: relay1.id.to_string(),
90+
url: Url::parse(&format!("http://localhost:{relay1_port}"))?,
91+
pubkey: pubkey.clone(),
92+
},
93+
}],
94+
};
95+
96+
// Save to a file
97+
let temp_file = tempfile::NamedTempFile::new()?;
98+
let config_path = temp_file.path().to_path_buf();
99+
let config_toml = toml::to_string_pretty(&cb_config)?;
100+
info!("Writing initial config to {:?}", config_path);
101+
std::fs::write(config_path.clone(), config_toml.as_bytes())?;
102+
103+
// Run the PBS service
104+
let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![relay1.clone()]);
105+
let state = PbsState::new(config, config_path.clone());
106+
tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state));
107+
108+
// leave some time to start servers - extra time for the file watcher
109+
tokio::time::sleep(Duration::from_millis(1000)).await;
110+
111+
// Send a get header request - should go to relay 1 only
112+
let mock_validator = MockValidator::new(pbs_port)?;
113+
info!("Sending get header");
114+
let res = mock_validator.do_get_header(None).await?;
115+
assert_eq!(res.status(), StatusCode::OK);
116+
assert_eq!(relay1_state.received_get_header(), 1);
117+
assert_eq!(relay2_state.received_get_header(), 0);
118+
119+
// Update the config to only have relay 2
120+
let cb_config = CommitBoostConfig {
121+
chain,
122+
pbs: StaticPbsConfig { docker_image: String::new(), pbs_config, with_signer: false },
123+
muxes: None,
124+
modules: None,
125+
signer: None,
126+
logs: LogsSettings::default(),
127+
metrics: None,
128+
relays: vec![RelayConfig {
129+
id: Some(relay2_id.clone()),
130+
enable_timing_games: false,
131+
frequency_get_header_ms: None,
132+
get_params: None,
133+
headers: None,
134+
target_first_request_ms: None,
135+
validator_registration_batch_size: None,
136+
entry: RelayEntry {
137+
id: relay2_id,
138+
url: Url::parse(&format!("http://{pubkey}@localhost:{relay2_port}"))?,
139+
pubkey,
140+
},
141+
}],
142+
};
143+
let config_toml = toml::to_string_pretty(&cb_config)?;
144+
info!("Writing updated config to {:?}", config_path);
145+
std::fs::write(config_path, config_toml.as_bytes())?;
146+
147+
// leave some time for the watcher to pick up the change and reload
148+
tokio::time::sleep(Duration::from_millis(1000)).await;
149+
150+
// Send another get header request - should go to relay 2 only
151+
info!("Sending get header after config update");
152+
let res = mock_validator.do_get_header(None).await?;
153+
assert_eq!(res.status(), StatusCode::OK);
154+
assert_eq!(relay1_state.received_get_header(), 1); // no change
155+
assert_eq!(relay2_state.received_get_header(), 1); // incremented
156+
157+
Ok(())
158+
}

0 commit comments

Comments
 (0)