posemesh_compute_node/
config.rs

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