1use anyhow::{Context, Result};
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use serde_with::DurationSeconds;
8use serde_with::serde_as;
9use std::{net::SocketAddr, time::Duration};
10use tracing::error;
11use url::Url;
12
13#[serde_as]
14#[derive(Clone, Debug, Deserialize, Serialize)]
15#[serde(rename_all = "kebab-case")]
16pub struct ProxyConfig {
17 pub listen_address: SocketAddr,
18 pub metrics_address: SocketAddr,
19 pub execution_peer: PeerConfig,
20 pub read_peer: PeerConfig,
21 #[serde(default = "default_max_idle_connections")]
25 pub max_idle_connections: usize,
26 #[serde_as(as = "DurationSeconds")]
30 #[serde(default = "default_idle_timeout")]
31 pub idle_timeout_seconds: Duration,
32 #[serde(default)]
34 pub logging: LoggingConfig,
35}
36
37fn default_max_idle_connections() -> usize {
38 100
39}
40
41fn default_idle_timeout() -> Duration {
42 Duration::from_secs(60)
43}
44
45#[derive(Clone, Debug, Deserialize, Serialize)]
46#[serde(rename_all = "kebab-case")]
47pub struct PeerConfig {
48 pub address: Url,
49}
50
51#[derive(Clone, Debug, Default, Deserialize, Serialize)]
52#[serde(rename_all = "kebab-case")]
53pub struct LoggingConfig {
54 #[serde(default = "default_sample_rate")]
57 pub read_request_sample_rate: f64,
58}
59
60fn default_sample_rate() -> f64 {
61 0.0
62}
63
64pub async fn load<P: AsRef<std::path::Path>>(path: P) -> Result<(ProxyConfig, Client)> {
66 let path = path.as_ref();
67 let config: ProxyConfig = serde_yaml::from_reader(
68 std::fs::File::open(path).context(format!("cannot open {:?}", path))?,
69 )?;
70
71 let client = reqwest::ClientBuilder::new()
73 .http2_prior_knowledge()
74 .http2_keep_alive_while_idle(true)
75 .pool_idle_timeout(config.idle_timeout_seconds)
76 .pool_max_idle_per_host(config.max_idle_connections)
77 .build()
78 .expect("Failed to build HTTP/2 client");
79
80 validate_peer_url(&client, &config.read_peer).await?;
81 validate_peer_url(&client, &config.execution_peer).await?;
82
83 Ok((config, client))
84}
85
86async fn validate_peer_url(client: &Client, peer: &PeerConfig) -> Result<()> {
88 let health_url = peer
89 .address
90 .join("/health")
91 .context("Failed to construct health check URL")?;
92
93 const RETRY_DELAY: Duration = Duration::from_secs(1);
94 const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
95
96 let mut attempt = 1;
97 loop {
98 match client
99 .get(health_url.clone())
100 .timeout(REQUEST_TIMEOUT)
101 .send()
102 .await
103 {
104 Ok(response) => {
105 if response.version() != reqwest::Version::HTTP_2 {
106 tracing::warn!(
107 "Peer {} does not support HTTP/2 (using {:?})",
108 peer.address,
109 response.version()
110 );
111 }
112
113 if !response.status().is_success() {
114 tracing::warn!(
115 "Health check failed for peer {} with status {}",
116 peer.address,
117 response.status()
118 );
119 }
120 return Ok(());
121 }
122 Err(e) => {
123 error!(
124 "Failed to connect to peer {} (attempt {}): {}",
125 peer.address, attempt, e
126 );
127 tokio::time::sleep(RETRY_DELAY).await;
128 attempt += 1;
129 continue;
130 }
131 }
132 }
133}