Skip to main content

ant_quic/bootstrap_cache/
config.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Bootstrap cache configuration.
9
10use std::path::PathBuf;
11use std::time::Duration;
12
13/// Configuration for the bootstrap cache
14#[derive(Debug, Clone)]
15pub struct BootstrapCacheConfig {
16    /// Directory for cache files
17    pub cache_dir: PathBuf,
18
19    /// Maximum number of peers to cache (default: 30,000 per ADR-007)
20    pub max_peers: usize,
21
22    /// Epsilon for exploration rate (default: 0.1 = 10%)
23    /// Higher values = more exploration of unknown peers
24    pub epsilon: f64,
25
26    /// Time after which peers are considered stale (default: 7 days)
27    pub stale_threshold: Duration,
28
29    /// Freshness window for peer-verified direct reachability evidence.
30    pub reachability_ttl: Duration,
31
32    /// Interval between background save operations (default: 5 minutes)
33    pub save_interval: Duration,
34
35    /// Interval between quality score recalculations (default: 1 hour)
36    pub quality_update_interval: Duration,
37
38    /// Interval between stale peer cleanup (default: 6 hours)
39    pub cleanup_interval: Duration,
40
41    /// Minimum peers required before saving (prevents empty cache overwrite)
42    pub min_peers_to_save: usize,
43
44    /// Enable file locking for multi-process safety
45    pub enable_file_locking: bool,
46
47    /// Quality score weights
48    pub weights: QualityWeights,
49}
50
51/// Weights for quality score calculation
52#[derive(Debug, Clone)]
53pub struct QualityWeights {
54    /// Weight for success rate component (default: 0.4)
55    pub success_rate: f64,
56    /// Weight for RTT component (default: 0.25)
57    pub rtt: f64,
58    /// Weight for age/freshness component (default: 0.15)
59    pub freshness: f64,
60    /// Weight for capability bonuses (default: 0.2)
61    pub capabilities: f64,
62}
63
64impl Default for BootstrapCacheConfig {
65    fn default() -> Self {
66        Self {
67            cache_dir: default_cache_dir(),
68            max_peers: 30_000,
69            epsilon: 0.1,
70            stale_threshold: Duration::from_secs(7 * 24 * 3600), // 7 days
71            reachability_ttl: crate::reachability::DIRECT_REACHABILITY_TTL,
72            save_interval: Duration::from_secs(5 * 60), // 5 minutes
73            quality_update_interval: Duration::from_secs(3600), // 1 hour
74            cleanup_interval: Duration::from_secs(6 * 3600), // 6 hours
75            min_peers_to_save: 10,
76            enable_file_locking: true,
77            weights: QualityWeights::default(),
78        }
79    }
80}
81
82impl Default for QualityWeights {
83    fn default() -> Self {
84        Self {
85            success_rate: 0.4,
86            rtt: 0.25,
87            freshness: 0.15,
88            capabilities: 0.2,
89        }
90    }
91}
92
93impl BootstrapCacheConfig {
94    /// Create a new configuration builder
95    pub fn builder() -> BootstrapCacheConfigBuilder {
96        BootstrapCacheConfigBuilder::default()
97    }
98}
99
100/// Builder for BootstrapCacheConfig
101#[derive(Default)]
102pub struct BootstrapCacheConfigBuilder {
103    config: BootstrapCacheConfig,
104}
105
106impl BootstrapCacheConfigBuilder {
107    /// Set the cache directory
108    pub fn cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
109        self.config.cache_dir = dir.into();
110        self
111    }
112
113    /// Set maximum number of peers
114    pub fn max_peers(mut self, max: usize) -> Self {
115        self.config.max_peers = max;
116        self
117    }
118
119    /// Set epsilon for exploration rate (clamped to 0.0-1.0)
120    pub fn epsilon(mut self, epsilon: f64) -> Self {
121        self.config.epsilon = epsilon.clamp(0.0, 1.0);
122        self
123    }
124
125    /// Set freshness window for peer-verified direct reachability evidence.
126    pub fn reachability_ttl(mut self, ttl: Duration) -> Self {
127        self.config.reachability_ttl = ttl;
128        self
129    }
130
131    /// Set stale threshold duration
132    pub fn stale_threshold(mut self, duration: Duration) -> Self {
133        self.config.stale_threshold = duration;
134        self
135    }
136
137    /// Set save interval
138    pub fn save_interval(mut self, duration: Duration) -> Self {
139        self.config.save_interval = duration;
140        self
141    }
142
143    /// Set quality update interval
144    pub fn quality_update_interval(mut self, duration: Duration) -> Self {
145        self.config.quality_update_interval = duration;
146        self
147    }
148
149    /// Set cleanup interval
150    pub fn cleanup_interval(mut self, duration: Duration) -> Self {
151        self.config.cleanup_interval = duration;
152        self
153    }
154
155    /// Set minimum peers required to save
156    pub fn min_peers_to_save(mut self, min: usize) -> Self {
157        self.config.min_peers_to_save = min;
158        self
159    }
160
161    /// Enable or disable file locking
162    pub fn enable_file_locking(mut self, enable: bool) -> Self {
163        self.config.enable_file_locking = enable;
164        self
165    }
166
167    /// Set quality weights
168    pub fn weights(mut self, weights: QualityWeights) -> Self {
169        self.config.weights = weights;
170        self
171    }
172
173    /// Build the configuration
174    pub fn build(self) -> BootstrapCacheConfig {
175        self.config
176    }
177}
178
179fn default_cache_dir() -> PathBuf {
180    // Prefer TMPDIR for sandbox compatibility (Claude Code sets this to /tmp/claude)
181    if let Ok(tmpdir) = std::env::var("TMPDIR") {
182        return PathBuf::from(tmpdir).join("ant-quic-cache");
183    }
184
185    // Try platform-specific cache directory, fallback to current directory
186    if let Some(cache_dir) = dirs::cache_dir() {
187        cache_dir.join("ant-quic")
188    } else if let Some(home) = dirs::home_dir() {
189        home.join(".cache").join("ant-quic")
190    } else {
191        PathBuf::from(".ant-quic-cache")
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_default_config() {
201        let config = BootstrapCacheConfig::default();
202        assert_eq!(config.max_peers, 30_000);
203        assert!((config.epsilon - 0.1).abs() < f64::EPSILON);
204        assert_eq!(config.stale_threshold, Duration::from_secs(7 * 24 * 3600));
205    }
206
207    #[test]
208    fn test_builder() {
209        let config = BootstrapCacheConfig::builder()
210            .max_peers(10_000)
211            .epsilon(0.2)
212            .cache_dir("/tmp/test")
213            .build();
214
215        assert_eq!(config.max_peers, 10_000);
216        assert!((config.epsilon - 0.2).abs() < f64::EPSILON);
217        assert_eq!(config.cache_dir, PathBuf::from("/tmp/test"));
218    }
219
220    #[test]
221    fn test_epsilon_clamping() {
222        let config = BootstrapCacheConfig::builder().epsilon(1.5).build();
223        assert!((config.epsilon - 1.0).abs() < f64::EPSILON);
224
225        let config = BootstrapCacheConfig::builder().epsilon(-0.5).build();
226        assert!(config.epsilon.abs() < f64::EPSILON);
227    }
228
229    #[test]
230    fn default_config_sets_all_intervals_and_flags() {
231        let config = BootstrapCacheConfig::default();
232        assert_eq!(
233            config.reachability_ttl,
234            crate::reachability::DIRECT_REACHABILITY_TTL
235        );
236        assert_eq!(config.save_interval, Duration::from_secs(5 * 60));
237        assert_eq!(config.quality_update_interval, Duration::from_secs(3600));
238        assert_eq!(config.cleanup_interval, Duration::from_secs(6 * 3600));
239        assert_eq!(config.min_peers_to_save, 10);
240        assert!(config.enable_file_locking);
241        assert!(
242            config.cache_dir.ends_with("ant-quic-cache") || config.cache_dir.ends_with("ant-quic")
243        );
244    }
245
246    #[test]
247    fn quality_weights_default_sum_to_one() {
248        let weights = QualityWeights::default();
249        assert!((weights.success_rate - 0.4).abs() < f64::EPSILON);
250        assert!((weights.rtt - 0.25).abs() < f64::EPSILON);
251        assert!((weights.freshness - 0.15).abs() < f64::EPSILON);
252        assert!((weights.capabilities - 0.2).abs() < f64::EPSILON);
253        let total = weights.success_rate + weights.rtt + weights.freshness + weights.capabilities;
254        assert!((total - 1.0).abs() < f64::EPSILON);
255    }
256
257    #[test]
258    fn builder_sets_all_duration_fields() {
259        let config = BootstrapCacheConfig::builder()
260            .stale_threshold(Duration::from_secs(1))
261            .reachability_ttl(Duration::from_secs(2))
262            .save_interval(Duration::from_secs(3))
263            .quality_update_interval(Duration::from_secs(4))
264            .cleanup_interval(Duration::from_secs(5))
265            .build();
266
267        assert_eq!(config.stale_threshold, Duration::from_secs(1));
268        assert_eq!(config.reachability_ttl, Duration::from_secs(2));
269        assert_eq!(config.save_interval, Duration::from_secs(3));
270        assert_eq!(config.quality_update_interval, Duration::from_secs(4));
271        assert_eq!(config.cleanup_interval, Duration::from_secs(5));
272    }
273
274    #[test]
275    fn builder_sets_save_threshold_and_locking() {
276        let config = BootstrapCacheConfig::builder()
277            .min_peers_to_save(0)
278            .enable_file_locking(false)
279            .build();
280
281        assert_eq!(config.min_peers_to_save, 0);
282        assert!(!config.enable_file_locking);
283    }
284
285    #[test]
286    fn builder_replaces_quality_weights() {
287        let weights = QualityWeights {
288            success_rate: 1.0,
289            rtt: 2.0,
290            freshness: 3.0,
291            capabilities: 4.0,
292        };
293        let config = BootstrapCacheConfig::builder()
294            .weights(weights.clone())
295            .build();
296
297        assert!((config.weights.success_rate - weights.success_rate).abs() < f64::EPSILON);
298        assert!((config.weights.rtt - weights.rtt).abs() < f64::EPSILON);
299        assert!((config.weights.freshness - weights.freshness).abs() < f64::EPSILON);
300        assert!((config.weights.capabilities - weights.capabilities).abs() < f64::EPSILON);
301    }
302
303    #[test]
304    fn config_clone_preserves_custom_values() {
305        let config = BootstrapCacheConfig::builder()
306            .cache_dir("relative/cache")
307            .max_peers(42)
308            .epsilon(0.75)
309            .min_peers_to_save(3)
310            .enable_file_locking(false)
311            .build();
312        let cloned = config.clone();
313
314        assert_eq!(cloned.cache_dir, PathBuf::from("relative/cache"));
315        assert_eq!(cloned.max_peers, 42);
316        assert!((cloned.epsilon - 0.75).abs() < f64::EPSILON);
317        assert_eq!(cloned.min_peers_to_save, 3);
318        assert!(!cloned.enable_file_locking);
319    }
320}