Skip to main content

kvbm_config/
discovery.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Discovery configuration for Nova peer discovery.
5//!
6//! Supports three discovery backends:
7//! - **Etcd**: Centralized discovery using etcd key-value store
8//! - **P2P**: Decentralized discovery using libp2p DHT
9//! - **Filesystem**: File-based discovery for development/testing
10
11use std::path::PathBuf;
12
13use serde::{Deserialize, Serialize};
14use validator::Validate;
15
16/// Discovery configuration - only one type can be active at a time.
17///
18/// # JSON Configuration Examples
19///
20/// ## Etcd Discovery
21/// ```json
22/// {
23///   "type": "etcd",
24///   "cluster_id": "my-cluster",
25///   "endpoints": ["http://etcd1:2379", "http://etcd2:2379"],
26///   "ttl_secs": 60
27/// }
28/// ```
29///
30/// ## P2P Discovery
31/// ```json
32/// {
33///   "type": "p2p",
34///   "cluster_id": "my-cluster",
35///   "listen_port": 0,
36///   "bootstrap_peers": ["192.168.1.10:4001"],
37///   "enable_mdns": true
38/// }
39/// ```
40///
41/// ## Filesystem Discovery
42/// ```json
43/// {
44///   "type": "filesystem",
45///   "path": "/tmp/discovery.json"
46/// }
47/// ```
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(tag = "type", rename_all = "lowercase")]
50pub enum DiscoveryConfig {
51    /// Etcd-based discovery (centralized).
52    Etcd(EtcdDiscoveryConfig),
53    /// P2P discovery using libp2p DHT (decentralized).
54    P2p(P2pDiscoveryConfig),
55    /// Filesystem-based discovery (for dev/testing).
56    Filesystem(FilesystemDiscoveryConfig),
57}
58
59/// Etcd discovery configuration.
60#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
61pub struct EtcdDiscoveryConfig {
62    /// Cluster ID / key prefix for discovery (required).
63    pub cluster_id: String,
64
65    /// Etcd endpoints (default: ["http://localhost:2379"]).
66    #[serde(default = "default_etcd_endpoints")]
67    pub endpoints: Vec<String>,
68
69    /// Lease TTL in seconds (default: 60, range: 10-600).
70    #[serde(default = "default_etcd_ttl")]
71    #[validate(range(min = 10, max = 600))]
72    pub ttl_secs: u64,
73
74    /// Operation timeout in seconds (default: 30).
75    #[serde(default = "default_operation_timeout")]
76    pub operation_timeout_secs: u64,
77
78    /// Max retries for operations (default: 3).
79    #[serde(default = "default_max_retries")]
80    #[validate(range(min = 0, max = 10))]
81    pub max_retries: u32,
82}
83
84/// P2P discovery configuration.
85#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
86pub struct P2pDiscoveryConfig {
87    /// Cluster ID / swarm key (required).
88    pub cluster_id: String,
89
90    /// Listen port (default: 0 = OS-assigned).
91    #[serde(default)]
92    pub listen_port: u16,
93
94    /// Bootstrap peer addresses.
95    #[serde(default)]
96    pub bootstrap_peers: Vec<String>,
97
98    /// DHT replication factor (default: 3).
99    #[serde(default = "default_replication_factor")]
100    pub replication_factor: usize,
101
102    /// Enable mDNS for local network discovery (default: false).
103    #[serde(default)]
104    pub enable_mdns: bool,
105
106    /// Record TTL in seconds (default: 600).
107    #[serde(default = "default_record_ttl")]
108    pub record_ttl_secs: u64,
109}
110
111/// Filesystem discovery configuration.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct FilesystemDiscoveryConfig {
114    /// Path to the discovery JSON file.
115    pub path: PathBuf,
116}
117
118fn default_etcd_endpoints() -> Vec<String> {
119    vec!["http://localhost:2379".to_string()]
120}
121
122fn default_etcd_ttl() -> u64 {
123    60
124}
125
126fn default_operation_timeout() -> u64 {
127    30
128}
129
130fn default_max_retries() -> u32 {
131    3
132}
133
134fn default_replication_factor() -> usize {
135    3
136}
137
138fn default_record_ttl() -> u64 {
139    600
140}
141
142impl Default for EtcdDiscoveryConfig {
143    fn default() -> Self {
144        Self {
145            cluster_id: String::new(),
146            endpoints: default_etcd_endpoints(),
147            ttl_secs: default_etcd_ttl(),
148            operation_timeout_secs: default_operation_timeout(),
149            max_retries: default_max_retries(),
150        }
151    }
152}
153
154impl Default for P2pDiscoveryConfig {
155    fn default() -> Self {
156        Self {
157            cluster_id: String::new(),
158            listen_port: 0,
159            bootstrap_peers: Vec::new(),
160            replication_factor: default_replication_factor(),
161            enable_mdns: false,
162            record_ttl_secs: default_record_ttl(),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_deserialize_etcd_config() {
173        let json = r#"{
174            "type": "etcd",
175            "cluster_id": "test-cluster",
176            "endpoints": ["http://etcd1:2379"],
177            "ttl_secs": 120
178        }"#;
179
180        let config: DiscoveryConfig = serde_json::from_str(json).unwrap();
181        match config {
182            DiscoveryConfig::Etcd(etcd) => {
183                assert_eq!(etcd.cluster_id, "test-cluster");
184                assert_eq!(etcd.endpoints, vec!["http://etcd1:2379"]);
185                assert_eq!(etcd.ttl_secs, 120);
186                assert_eq!(etcd.operation_timeout_secs, 30); // default
187                assert_eq!(etcd.max_retries, 3); // default
188            }
189            _ => panic!("Expected Etcd config"),
190        }
191    }
192
193    #[test]
194    fn test_deserialize_p2p_config() {
195        let json = r#"{
196            "type": "p2p",
197            "cluster_id": "test-cluster",
198            "listen_port": 4001,
199            "bootstrap_peers": ["192.168.1.10:4001"],
200            "enable_mdns": true
201        }"#;
202
203        let config: DiscoveryConfig = serde_json::from_str(json).unwrap();
204        match config {
205            DiscoveryConfig::P2p(p2p) => {
206                assert_eq!(p2p.cluster_id, "test-cluster");
207                assert_eq!(p2p.listen_port, 4001);
208                assert_eq!(p2p.bootstrap_peers, vec!["192.168.1.10:4001"]);
209                assert!(p2p.enable_mdns);
210                assert_eq!(p2p.replication_factor, 3); // default
211                assert_eq!(p2p.record_ttl_secs, 600); // default
212            }
213            _ => panic!("Expected P2p config"),
214        }
215    }
216
217    #[test]
218    fn test_deserialize_filesystem_config() {
219        let json = r#"{
220            "type": "filesystem",
221            "path": "/tmp/discovery.json"
222        }"#;
223
224        let config: DiscoveryConfig = serde_json::from_str(json).unwrap();
225        match config {
226            DiscoveryConfig::Filesystem(fs) => {
227                assert_eq!(fs.path, PathBuf::from("/tmp/discovery.json"));
228            }
229            _ => panic!("Expected Filesystem config"),
230        }
231    }
232
233    #[test]
234    fn test_serialize_etcd_config() {
235        let config = DiscoveryConfig::Etcd(EtcdDiscoveryConfig {
236            cluster_id: "my-cluster".to_string(),
237            endpoints: vec!["http://localhost:2379".to_string()],
238            ttl_secs: 60,
239            operation_timeout_secs: 30,
240            max_retries: 3,
241        });
242
243        let json = serde_json::to_string(&config).unwrap();
244        assert!(json.contains(r#""type":"etcd""#));
245        assert!(json.contains(r#""cluster_id":"my-cluster""#));
246    }
247
248    #[test]
249    fn test_etcd_default() {
250        let config = EtcdDiscoveryConfig::default();
251        assert!(config.cluster_id.is_empty());
252        assert_eq!(config.endpoints, vec!["http://localhost:2379"]);
253        assert_eq!(config.ttl_secs, 60);
254        assert_eq!(config.operation_timeout_secs, 30);
255        assert_eq!(config.max_retries, 3);
256    }
257
258    #[test]
259    fn test_p2p_default() {
260        let config = P2pDiscoveryConfig::default();
261        assert!(config.cluster_id.is_empty());
262        assert_eq!(config.listen_port, 0);
263        assert!(config.bootstrap_peers.is_empty());
264        assert_eq!(config.replication_factor, 3);
265        assert!(!config.enable_mdns);
266        assert_eq!(config.record_ttl_secs, 600);
267    }
268}