sui_edge_proxy/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    /// Maximum number of idle connections to keep in the connection pool.
22    /// When set, this limits the number of connections that remain open but unused,
23    /// helping to conserve system resources.
24    #[serde(default = "default_max_idle_connections")]
25    pub max_idle_connections: usize,
26    /// Idle timeout for connections in the connection pool.
27    /// This should be set to a value less than the keep-alive timeout of the server to avoid sending requests to a closed connection.
28    /// if your you expect sui-edge-proxy to recieve a small number of requests per second, you should set this to a higher value.
29    #[serde_as(as = "DurationSeconds")]
30    #[serde(default = "default_idle_timeout")]
31    pub idle_timeout_seconds: Duration,
32    /// Logging configuration for read requests including sample rate and log file path.
33    #[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    /// The sample rate for read-request logging. 0.0 = no logs;
55    /// 1.0 = log all read requests.
56    #[serde(default = "default_sample_rate")]
57    pub read_request_sample_rate: f64,
58}
59
60fn default_sample_rate() -> f64 {
61    0.0
62}
63
64/// Load and validate configuration
65pub 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    // Build a reqwest client that supports HTTP/2
72    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
86/// Validate that the given PeerConfig URL has a valid host
87async 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}