Skip to main content

posemesh_compute_node/
config.rs

1use anyhow::{bail, Context, Result};
2use serde::{Deserialize, Serialize};
3use std::env;
4use url::Url;
5
6const DEFAULT_DMS_BASE_URL: &str = "https://dms.auki.network/v1";
7const DEFAULT_DDS_BASE_URL: &str = "https://dds.auki.network";
8const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 60;
9const DEFAULT_REGISTER_INTERVAL_SECS: u64 = 120;
10const DEFAULT_REGISTER_MAX_RETRY: i32 = -1;
11const DEFAULT_HEARTBEAT_MIN_RATIO: f64 = 0.25;
12const DEFAULT_HEARTBEAT_MAX_RATIO: f64 = 0.35;
13
14/// Log output format.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
16#[serde(rename_all = "lowercase")]
17pub enum LogFormat {
18    #[default]
19    Json,
20    Text,
21}
22
23/// Node configuration loaded from environment (SPECS ยง8 Configuration).
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct NodeConfig {
26    // Core settings (defaults available).
27    pub dms_base_url: Url,
28    pub node_version: String,
29    pub request_timeout_secs: u64,
30
31    // Auth: either static node identity or SIWE via DDS
32    pub dds_base_url: Option<Url>,
33    pub node_url: Option<Url>,
34    pub reg_secret: Option<String>,
35    pub secp256k1_privhex: Option<String>,
36
37    // Optional
38    pub heartbeat_jitter_ms: u64,
39    pub heartbeat_min_ratio: f64,
40    pub heartbeat_max_ratio: f64,
41    pub poll_backoff_ms_min: u64,
42    pub poll_backoff_ms_max: u64,
43    pub token_safety_ratio: f32,
44    pub token_reauth_max_retries: u32,
45    pub token_reauth_jitter_ms: u64,
46    pub register_interval_secs: Option<u64>,
47    pub register_max_retry: Option<i32>,
48    pub max_concurrency: u32,
49    pub log_format: LogFormat,
50    pub enable_noop: bool,
51    pub noop_sleep_secs: u64,
52}
53
54impl NodeConfig {
55    /// Load configuration from environment variables.
56    pub fn from_env() -> Result<Self> {
57        // Core settings (defaults when unset).
58        let dms_base_url = parse_url_default("DMS_BASE_URL", DEFAULT_DMS_BASE_URL)?;
59        let request_timeout_secs =
60            parse_u64_default("REQUEST_TIMEOUT_SECS", DEFAULT_REQUEST_TIMEOUT_SECS)?;
61        let node_version = env::var("NODE_VERSION")
62            .ok()
63            .filter(|s| !s.trim().is_empty())
64            .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
65
66        // Auth options
67        let dds_base_url = parse_url_default("DDS_BASE_URL", DEFAULT_DDS_BASE_URL)?;
68        let node_url = Url::parse(
69            &env::var("NODE_URL")
70                .with_context(|| "NODE_URL required for DDS SIWE authentication")?,
71        )
72        .with_context(|| "invalid URL in NODE_URL")?;
73        let reg_secret = env::var("REG_SECRET")
74            .with_context(|| "REG_SECRET required for DDS SIWE authentication")?
75            .trim()
76            .to_string();
77        if reg_secret.is_empty() {
78            bail!("REG_SECRET required for DDS SIWE authentication");
79        }
80        let secp256k1_privhex = env::var("SECP256K1_PRIVHEX")
81            .with_context(|| "SECP256K1_PRIVHEX required for DDS SIWE authentication")?
82            .trim()
83            .to_string();
84        if secp256k1_privhex.is_empty() {
85            bail!("SECP256K1_PRIVHEX required for DDS SIWE authentication");
86        }
87
88        // Optional
89        let heartbeat_jitter_ms = parse_u64_opt("HEARTBEAT_JITTER_MS", 250)?;
90        let heartbeat_min_ratio =
91            parse_f64_opt("HEARTBEAT_MIN_RATIO", DEFAULT_HEARTBEAT_MIN_RATIO)?;
92        let heartbeat_max_ratio =
93            parse_f64_opt("HEARTBEAT_MAX_RATIO", DEFAULT_HEARTBEAT_MAX_RATIO)?;
94        let poll_backoff_ms_min = parse_u64_opt("POLL_BACKOFF_MS_MIN", 1000)?;
95        let poll_backoff_ms_max = parse_u64_opt("POLL_BACKOFF_MS_MAX", 30000)?;
96        let token_safety_ratio = parse_f32_opt("TOKEN_SAFETY_RATIO", 0.75)?;
97        let token_reauth_max_retries = parse_u32_opt("TOKEN_REAUTH_MAX_RETRIES", 3)?;
98        let token_reauth_jitter_ms = parse_u64_opt("TOKEN_REAUTH_JITTER_MS", 500)?;
99        let register_interval_secs = Some(parse_u64_default(
100            "REGISTER_INTERVAL_SECS",
101            DEFAULT_REGISTER_INTERVAL_SECS,
102        )?);
103        let register_max_retry = Some(parse_i32_default(
104            "REGISTER_MAX_RETRY",
105            DEFAULT_REGISTER_MAX_RETRY,
106        )?);
107        let max_concurrency = parse_u32_opt("MAX_CONCURRENCY", 1)?;
108        let log_format = parse_log_format("LOG_FORMAT").unwrap_or_default();
109        let enable_noop = parse_bool_opt("ENABLE_NOOP", false)?;
110        let noop_sleep_secs = parse_u64_opt("NOOP_SLEEP_SECS", 5)?;
111
112        Ok(Self {
113            dms_base_url,
114            node_version,
115            request_timeout_secs,
116            dds_base_url: Some(dds_base_url),
117            node_url: Some(node_url),
118            reg_secret: Some(reg_secret),
119            secp256k1_privhex: Some(secp256k1_privhex),
120            heartbeat_jitter_ms,
121            heartbeat_min_ratio,
122            heartbeat_max_ratio,
123            poll_backoff_ms_min,
124            poll_backoff_ms_max,
125            token_safety_ratio,
126            token_reauth_max_retries,
127            token_reauth_jitter_ms,
128            register_interval_secs,
129            register_max_retry,
130            max_concurrency,
131            log_format,
132            enable_noop,
133            noop_sleep_secs,
134        })
135    }
136}
137
138fn env_var_trimmed(key: &str) -> Option<String> {
139    env::var(key).ok().and_then(|value| {
140        let trimmed = value.trim();
141        if trimmed.is_empty() {
142            None
143        } else {
144            Some(trimmed.to_string())
145        }
146    })
147}
148
149fn parse_url_default(key: &str, default: &str) -> Result<Url> {
150    let raw = env_var_trimmed(key).unwrap_or_else(|| default.to_string());
151    Url::parse(&raw).with_context(|| format!("invalid URL in {key}"))
152}
153
154fn parse_u64_default(key: &str, default: u64) -> Result<u64> {
155    match env_var_trimmed(key) {
156        Some(value) => value
157            .parse()
158            .with_context(|| format!("invalid integer in {key}")),
159        None => Ok(default),
160    }
161}
162
163fn parse_u64_opt(key: &str, default: u64) -> Result<u64> {
164    match env::var(key) {
165        Ok(v) => v
166            .parse()
167            .with_context(|| format!("invalid integer in {key}")),
168        Err(_) => Ok(default),
169    }
170}
171
172fn parse_u32_opt(key: &str, default: u32) -> Result<u32> {
173    match env::var(key) {
174        Ok(v) => v
175            .parse()
176            .with_context(|| format!("invalid integer in {key}")),
177        Err(_) => Ok(default),
178    }
179}
180
181fn parse_i32_default(key: &str, default: i32) -> Result<i32> {
182    match env_var_trimmed(key) {
183        Some(value) => {
184            let parsed: i32 = value
185                .parse()
186                .with_context(|| format!("invalid integer in {key}"))?;
187            if parsed < -1 {
188                bail!("{key} must be -1 or a non-negative integer, got {parsed}");
189            }
190            Ok(parsed)
191        }
192        None => Ok(default),
193    }
194}
195
196fn parse_f32_opt(key: &str, default: f32) -> Result<f32> {
197    match env::var(key) {
198        Ok(v) => v.parse().with_context(|| format!("invalid float in {key}")),
199        Err(_) => Ok(default),
200    }
201}
202
203fn parse_f64_opt(key: &str, default: f64) -> Result<f64> {
204    match env::var(key) {
205        Ok(v) => v.parse().with_context(|| format!("invalid float in {key}")),
206        Err(_) => Ok(default),
207    }
208}
209
210fn parse_bool_opt(key: &str, default: bool) -> Result<bool> {
211    match env::var(key) {
212        Ok(v) => v
213            .parse::<bool>()
214            .with_context(|| format!("invalid bool in {key}; expected true/false")),
215        Err(_) => Ok(default),
216    }
217}
218
219fn parse_log_format(key: &str) -> Option<LogFormat> {
220    match env::var(key).ok()?.to_lowercase().as_str() {
221        "json" => Some(LogFormat::Json),
222        "text" => Some(LogFormat::Text),
223        _ => None,
224    }
225}