solana_validator_optimizer/
preflight.rs1use crate::{config::AppConfig, rpc_health, system_health};
2use anyhow::Result;
3use serde::Serialize;
4
5#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum CheckStatus {
8 Pass,
9 Warn,
10 Fail,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct PreflightCheck {
15 pub name: String,
16 pub status: CheckStatus,
17 pub message: String,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct PreflightReport {
22 pub endpoint: String,
23 pub ready: bool,
24 pub checks: Vec<PreflightCheck>,
25 pub warnings: Vec<String>,
26}
27
28pub async fn run(config: &AppConfig) -> Result<PreflightReport> {
29 run_with_expected_genesis(config, None).await
30}
31
32pub async fn run_with_expected_genesis(
33 config: &AppConfig,
34 expected_genesis_hash: Option<&str>,
35) -> Result<PreflightReport> {
36 let rpc = rpc_health::check_rpc_health(config).await?;
37 let system = system_health::check_system_health();
38
39 Ok(evaluate_reports(rpc, system, expected_genesis_hash))
40}
41
42fn evaluate_reports(
43 rpc: rpc_health::RpcHealthReport,
44 system: system_health::SystemHealthReport,
45 expected_genesis_hash: Option<&str>,
46) -> PreflightReport {
47 let mut checks = Vec::new();
48
49 checks.push(PreflightCheck {
50 name: "rpc_health".to_string(),
51 status: if rpc.healthy {
52 CheckStatus::Pass
53 } else {
54 CheckStatus::Fail
55 },
56 message: format!("RPC health status: {}", rpc.health_status),
57 });
58
59 checks.push(match &rpc.solana_version {
60 Some(version) => PreflightCheck {
61 name: "solana_version".to_string(),
62 status: CheckStatus::Pass,
63 message: format!("Detected Solana/Agave version: {version}"),
64 },
65 None => PreflightCheck {
66 name: "solana_version".to_string(),
67 status: CheckStatus::Fail,
68 message: "Unable to determine Solana/Agave version".to_string(),
69 },
70 });
71
72 checks.push(match (&rpc.genesis_hash, expected_genesis_hash) {
73 (Some(observed), Some(expected)) if observed == expected => PreflightCheck {
74 name: "genesis_hash".to_string(),
75 status: CheckStatus::Pass,
76 message: format!("Genesis hash matches expected value: {observed}"),
77 },
78 (Some(observed), Some(expected)) => PreflightCheck {
79 name: "genesis_hash".to_string(),
80 status: CheckStatus::Fail,
81 message: format!("Genesis hash mismatch: expected {expected}, observed {observed}"),
82 },
83 (Some(observed), None) => PreflightCheck {
84 name: "genesis_hash".to_string(),
85 status: CheckStatus::Pass,
86 message: format!("Genesis hash: {observed}"),
87 },
88 (None, _) => PreflightCheck {
89 name: "genesis_hash".to_string(),
90 status: CheckStatus::Fail,
91 message: "Unable to retrieve genesis hash".to_string(),
92 },
93 });
94
95 checks.push(match rpc.slot {
96 Some(slot) => PreflightCheck {
97 name: "current_slot".to_string(),
98 status: CheckStatus::Pass,
99 message: format!("Current slot: {slot}"),
100 },
101 None => PreflightCheck {
102 name: "current_slot".to_string(),
103 status: CheckStatus::Fail,
104 message: "Unable to retrieve current slot".to_string(),
105 },
106 });
107
108 checks.push(match &rpc.latest_blockhash {
109 Some(blockhash) => PreflightCheck {
110 name: "latest_blockhash".to_string(),
111 status: CheckStatus::Pass,
112 message: format!("Latest blockhash available: {blockhash}"),
113 },
114 None => PreflightCheck {
115 name: "latest_blockhash".to_string(),
116 status: CheckStatus::Fail,
117 message: "Unable to retrieve latest blockhash".to_string(),
118 },
119 });
120
121 let latency_values = [
122 ("getHealth", Some(rpc.get_health_latency_ms)),
123 ("getVersion", rpc.get_version_latency_ms),
124 ("getSlot", rpc.get_slot_latency_ms),
125 ("getLatestBlockhash", rpc.get_latest_blockhash_latency_ms),
126 ];
127
128 let slow_calls: Vec<String> = latency_values
129 .iter()
130 .filter_map(|(name, latency)| match latency {
131 Some(ms) if *ms > 1_000 => Some(format!("{name}: {ms} ms")),
132 _ => None,
133 })
134 .collect();
135
136 checks.push(if slow_calls.is_empty() {
137 PreflightCheck {
138 name: "rpc_latency".to_string(),
139 status: CheckStatus::Pass,
140 message: "RPC latency is within the 1000 ms threshold".to_string(),
141 }
142 } else {
143 PreflightCheck {
144 name: "rpc_latency".to_string(),
145 status: CheckStatus::Warn,
146 message: format!("High RPC latency detected: {}", slow_calls.join(", ")),
147 }
148 });
149
150 checks.push(PreflightCheck {
151 name: "cpu".to_string(),
152 status: if system.logical_cpu_count < 2 {
153 CheckStatus::Fail
154 } else if system.logical_cpu_count < 8 {
155 CheckStatus::Warn
156 } else {
157 CheckStatus::Pass
158 },
159 message: format!("Logical CPU count: {}", system.logical_cpu_count),
160 });
161
162 checks.push(PreflightCheck {
163 name: "memory_total".to_string(),
164 status: if system.total_memory_gib() < 4.0 {
165 CheckStatus::Fail
166 } else if system.total_memory_gib() < 16.0 {
167 CheckStatus::Warn
168 } else {
169 CheckStatus::Pass
170 },
171 message: format!("Total memory: {:.1} GiB", system.total_memory_gib()),
172 });
173
174 checks.push(PreflightCheck {
175 name: "memory_available".to_string(),
176 status: if system.available_memory_gib() < 4.0 {
177 CheckStatus::Warn
178 } else {
179 CheckStatus::Pass
180 },
181 message: format!("Available memory: {:.2} GiB", system.available_memory_gib()),
182 });
183
184 checks.push(match system.disk_available_gib() {
185 Some(available) => PreflightCheck {
186 name: "disk_available".to_string(),
187 status: if available < 10.0 {
188 CheckStatus::Fail
189 } else if available < 50.0 {
190 CheckStatus::Warn
191 } else {
192 CheckStatus::Pass
193 },
194 message: match system.disk_total_gib() {
195 Some(total) => {
196 format!("Disk space: {available:.1} GiB available / {total:.1} GiB total")
197 }
198 None => format!("Disk space available: {available:.1} GiB"),
199 },
200 },
201 None => PreflightCheck {
202 name: "disk_available".to_string(),
203 status: CheckStatus::Warn,
204 message: "Unable to determine disk capacity".to_string(),
205 },
206 });
207
208 let ready = !checks.iter().any(|check| check.status == CheckStatus::Fail);
209
210 PreflightReport {
211 endpoint: rpc.endpoint,
212 ready,
213 checks,
214 warnings: rpc
215 .warnings
216 .into_iter()
217 .filter(|warning| !warning.contains("latency is high"))
218 .collect(),
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn healthy_rpc_report() -> rpc_health::RpcHealthReport {
227 rpc_health::RpcHealthReport {
228 endpoint: "http://localhost:8899".to_string(),
229 healthy: true,
230 health_status: "ok".to_string(),
231 solana_version: Some("4.2.0-rc.1".to_string()),
232 genesis_hash: Some("test-genesis-hash".to_string()),
233 slot: Some(438_200_491),
234 latest_blockhash: Some("test-blockhash".to_string()),
235 get_health_latency_ms: 100,
236 get_version_latency_ms: Some(100),
237 get_genesis_hash_latency_ms: Some(100),
238 get_slot_latency_ms: Some(100),
239 get_latest_blockhash_latency_ms: Some(100),
240 warnings: Vec::new(),
241 }
242 }
243
244 fn healthy_system_report() -> system_health::SystemHealthReport {
245 system_health::SystemHealthReport {
246 logical_cpu_count: 16,
247 total_memory_bytes: 64 * 1024 * 1024 * 1024,
248 available_memory_bytes: 32 * 1024 * 1024 * 1024,
249 disk_total_bytes: Some(2 * 1024 * 1024 * 1024 * 1024),
250 disk_available_bytes: Some(500 * 1024 * 1024 * 1024),
251 }
252 }
253
254 #[test]
255 fn healthy_rpc_is_ready() {
256 let report = evaluate_reports(healthy_rpc_report(), healthy_system_report(), None);
257
258 assert!(report.ready);
259 assert!(report
260 .checks
261 .iter()
262 .all(|check| check.status != CheckStatus::Fail));
263 }
264
265 #[test]
266 fn high_latency_warns_but_remains_ready() {
267 let mut rpc = healthy_rpc_report();
268 rpc.get_health_latency_ms = 1_500;
269 rpc.warnings
270 .push("getHealth latency is high: 1500 ms".to_string());
271
272 let report = evaluate_reports(rpc, healthy_system_report(), None);
273
274 assert!(report.ready);
275 assert!(report
276 .checks
277 .iter()
278 .any(|check| { check.name == "rpc_latency" && check.status == CheckStatus::Warn }));
279 }
280
281 #[test]
282 fn unhealthy_rpc_is_not_ready() {
283 let mut rpc = healthy_rpc_report();
284 rpc.healthy = false;
285 rpc.health_status = "unhealthy".to_string();
286
287 let report = evaluate_reports(rpc, healthy_system_report(), None);
288
289 assert!(!report.ready);
290 assert!(report
291 .checks
292 .iter()
293 .any(|check| { check.name == "rpc_health" && check.status == CheckStatus::Fail }));
294 }
295
296 #[test]
297 fn matching_expected_genesis_is_ready() {
298 let rpc = healthy_rpc_report();
299
300 let report = evaluate_reports(rpc, healthy_system_report(), Some("test-genesis-hash"));
301
302 assert!(report.ready);
303 assert!(report
304 .checks
305 .iter()
306 .any(|check| { check.name == "genesis_hash" && check.status == CheckStatus::Pass }));
307 }
308
309 #[test]
310 fn mismatched_expected_genesis_is_not_ready() {
311 let rpc = healthy_rpc_report();
312
313 let report = evaluate_reports(rpc, healthy_system_report(), Some("wrong-genesis-hash"));
314
315 assert!(!report.ready);
316 assert!(report
317 .checks
318 .iter()
319 .any(|check| { check.name == "genesis_hash" && check.status == CheckStatus::Fail }));
320 }
321
322 #[test]
323 fn missing_genesis_hash_is_not_ready() {
324 let mut rpc = healthy_rpc_report();
325 rpc.genesis_hash = None;
326
327 let report = evaluate_reports(rpc, healthy_system_report(), None);
328
329 assert!(!report.ready);
330 assert!(report
331 .checks
332 .iter()
333 .any(|check| { check.name == "genesis_hash" && check.status == CheckStatus::Fail }));
334 }
335
336 #[test]
337 fn missing_slot_is_not_ready() {
338 let mut rpc = healthy_rpc_report();
339 rpc.slot = None;
340
341 let report = evaluate_reports(rpc, healthy_system_report(), None);
342
343 assert!(!report.ready);
344 assert!(report
345 .checks
346 .iter()
347 .any(|check| { check.name == "current_slot" && check.status == CheckStatus::Fail }));
348 }
349 #[test]
350 fn low_memory_is_not_ready() {
351 let mut system = healthy_system_report();
352 system.total_memory_bytes = 2 * 1024 * 1024 * 1024;
353
354 let report = evaluate_reports(healthy_rpc_report(), system, None);
355
356 assert!(!report.ready);
357 assert!(report
358 .checks
359 .iter()
360 .any(|check| { check.name == "memory_total" && check.status == CheckStatus::Fail }));
361 }
362
363 #[test]
364 fn low_available_memory_warns_but_remains_ready() {
365 let mut system = healthy_system_report();
366 system.available_memory_bytes = 512 * 1024 * 1024;
367
368 let report = evaluate_reports(healthy_rpc_report(), system, None);
369
370 assert!(report.ready);
371 assert!(report.checks.iter().any(|check| {
372 check.name == "memory_available" && check.status == CheckStatus::Warn
373 }));
374 }
375
376 #[test]
377 fn low_disk_warns_but_remains_ready() {
378 let mut system = healthy_system_report();
379 system.disk_available_bytes = Some(25 * 1024 * 1024 * 1024);
380
381 let report = evaluate_reports(healthy_rpc_report(), system, None);
382
383 assert!(report.ready);
384 assert!(report
385 .checks
386 .iter()
387 .any(|check| { check.name == "disk_available" && check.status == CheckStatus::Warn }));
388 }
389}