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    /// Persist the cache to disk (default: true).
48    ///
49    /// When `false` the cache is purely in-memory: nothing is loaded on
50    /// open, nothing is written on save, and no cache directory or lock
51    /// file is created. Runtime behaviour (quality scoring, peer
52    /// selection, maintenance) is unchanged.
53    pub persist: bool,
54
55    /// Quality score weights
56    pub weights: QualityWeights,
57}
58
59/// Weights for quality score calculation
60#[derive(Debug, Clone)]
61pub struct QualityWeights {
62    /// Weight for success rate component (default: 0.4)
63    pub success_rate: f64,
64    /// Weight for RTT component (default: 0.25)
65    pub rtt: f64,
66    /// Weight for age/freshness component (default: 0.15)
67    pub freshness: f64,
68    /// Weight for capability bonuses (default: 0.2)
69    pub capabilities: f64,
70}
71
72impl Default for BootstrapCacheConfig {
73    fn default() -> Self {
74        Self {
75            cache_dir: default_cache_dir(),
76            max_peers: 30_000,
77            epsilon: 0.1,
78            stale_threshold: Duration::from_secs(7 * 24 * 3600), // 7 days
79            reachability_ttl: crate::reachability::DIRECT_REACHABILITY_TTL,
80            save_interval: Duration::from_secs(5 * 60), // 5 minutes
81            quality_update_interval: Duration::from_secs(3600), // 1 hour
82            cleanup_interval: Duration::from_secs(6 * 3600), // 6 hours
83            min_peers_to_save: 10,
84            enable_file_locking: true,
85            persist: true,
86            weights: QualityWeights::default(),
87        }
88    }
89}
90
91impl Default for QualityWeights {
92    fn default() -> Self {
93        Self {
94            success_rate: 0.4,
95            rtt: 0.25,
96            freshness: 0.15,
97            capabilities: 0.2,
98        }
99    }
100}
101
102impl BootstrapCacheConfig {
103    /// Create a new configuration builder
104    pub fn builder() -> BootstrapCacheConfigBuilder {
105        BootstrapCacheConfigBuilder::default()
106    }
107}
108
109/// Builder for BootstrapCacheConfig
110#[derive(Default)]
111pub struct BootstrapCacheConfigBuilder {
112    config: BootstrapCacheConfig,
113}
114
115impl BootstrapCacheConfigBuilder {
116    /// Set the cache directory
117    pub fn cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
118        self.config.cache_dir = dir.into();
119        self
120    }
121
122    /// Set maximum number of peers
123    pub fn max_peers(mut self, max: usize) -> Self {
124        self.config.max_peers = max;
125        self
126    }
127
128    /// Set epsilon for exploration rate (clamped to 0.0-1.0)
129    pub fn epsilon(mut self, epsilon: f64) -> Self {
130        self.config.epsilon = epsilon.clamp(0.0, 1.0);
131        self
132    }
133
134    /// Set freshness window for peer-verified direct reachability evidence.
135    pub fn reachability_ttl(mut self, ttl: Duration) -> Self {
136        self.config.reachability_ttl = ttl;
137        self
138    }
139
140    /// Set stale threshold duration
141    pub fn stale_threshold(mut self, duration: Duration) -> Self {
142        self.config.stale_threshold = duration;
143        self
144    }
145
146    /// Set save interval
147    pub fn save_interval(mut self, duration: Duration) -> Self {
148        self.config.save_interval = duration;
149        self
150    }
151
152    /// Set quality update interval
153    pub fn quality_update_interval(mut self, duration: Duration) -> Self {
154        self.config.quality_update_interval = duration;
155        self
156    }
157
158    /// Set cleanup interval
159    pub fn cleanup_interval(mut self, duration: Duration) -> Self {
160        self.config.cleanup_interval = duration;
161        self
162    }
163
164    /// Set minimum peers required to save
165    pub fn min_peers_to_save(mut self, min: usize) -> Self {
166        self.config.min_peers_to_save = min;
167        self
168    }
169
170    /// Enable or disable file locking
171    pub fn enable_file_locking(mut self, enable: bool) -> Self {
172        self.config.enable_file_locking = enable;
173        self
174    }
175
176    /// Enable or disable disk persistence (see [`BootstrapCacheConfig::persist`])
177    pub fn persist(mut self, persist: bool) -> Self {
178        self.config.persist = persist;
179        self
180    }
181
182    /// Set quality weights
183    pub fn weights(mut self, weights: QualityWeights) -> Self {
184        self.config.weights = weights;
185        self
186    }
187
188    /// Build the configuration
189    pub fn build(self) -> BootstrapCacheConfig {
190        self.config
191    }
192}
193
194fn default_cache_dir() -> PathBuf {
195    // Prefer TMPDIR for sandbox compatibility (Claude Code sets this to /tmp/claude)
196    if let Ok(tmpdir) = std::env::var("TMPDIR") {
197        return PathBuf::from(tmpdir).join("ant-quic-cache");
198    }
199
200    // Try platform-specific cache directory, fallback to current directory
201    if let Some(cache_dir) = dirs::cache_dir() {
202        cache_dir.join("ant-quic")
203    } else if let Some(home) = dirs::home_dir() {
204        home.join(".cache").join("ant-quic")
205    } else {
206        PathBuf::from(".ant-quic-cache")
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_default_config() {
216        let config = BootstrapCacheConfig::default();
217        assert_eq!(config.max_peers, 30_000);
218        assert!((config.epsilon - 0.1).abs() < f64::EPSILON);
219        assert_eq!(config.stale_threshold, Duration::from_secs(7 * 24 * 3600));
220    }
221
222    #[test]
223    fn test_builder() {
224        let config = BootstrapCacheConfig::builder()
225            .max_peers(10_000)
226            .epsilon(0.2)
227            .cache_dir("/tmp/test")
228            .build();
229
230        assert_eq!(config.max_peers, 10_000);
231        assert!((config.epsilon - 0.2).abs() < f64::EPSILON);
232        assert_eq!(config.cache_dir, PathBuf::from("/tmp/test"));
233    }
234
235    #[test]
236    fn test_epsilon_clamping() {
237        let config = BootstrapCacheConfig::builder().epsilon(1.5).build();
238        assert!((config.epsilon - 1.0).abs() < f64::EPSILON);
239
240        let config = BootstrapCacheConfig::builder().epsilon(-0.5).build();
241        assert!(config.epsilon.abs() < f64::EPSILON);
242    }
243
244    #[test]
245    fn default_config_sets_all_intervals_and_flags() {
246        let config = BootstrapCacheConfig::default();
247        assert_eq!(
248            config.reachability_ttl,
249            crate::reachability::DIRECT_REACHABILITY_TTL
250        );
251        assert_eq!(config.save_interval, Duration::from_secs(5 * 60));
252        assert_eq!(config.quality_update_interval, Duration::from_secs(3600));
253        assert_eq!(config.cleanup_interval, Duration::from_secs(6 * 3600));
254        assert_eq!(config.min_peers_to_save, 10);
255        assert!(config.enable_file_locking);
256        assert!(config.persist);
257        assert!(
258            config.cache_dir.ends_with("ant-quic-cache") || config.cache_dir.ends_with("ant-quic")
259        );
260    }
261
262    #[test]
263    fn quality_weights_default_sum_to_one() {
264        let weights = QualityWeights::default();
265        assert!((weights.success_rate - 0.4).abs() < f64::EPSILON);
266        assert!((weights.rtt - 0.25).abs() < f64::EPSILON);
267        assert!((weights.freshness - 0.15).abs() < f64::EPSILON);
268        assert!((weights.capabilities - 0.2).abs() < f64::EPSILON);
269        let total = weights.success_rate + weights.rtt + weights.freshness + weights.capabilities;
270        assert!((total - 1.0).abs() < f64::EPSILON);
271    }
272
273    #[test]
274    fn builder_sets_all_duration_fields() {
275        let config = BootstrapCacheConfig::builder()
276            .stale_threshold(Duration::from_secs(1))
277            .reachability_ttl(Duration::from_secs(2))
278            .save_interval(Duration::from_secs(3))
279            .quality_update_interval(Duration::from_secs(4))
280            .cleanup_interval(Duration::from_secs(5))
281            .build();
282
283        assert_eq!(config.stale_threshold, Duration::from_secs(1));
284        assert_eq!(config.reachability_ttl, Duration::from_secs(2));
285        assert_eq!(config.save_interval, Duration::from_secs(3));
286        assert_eq!(config.quality_update_interval, Duration::from_secs(4));
287        assert_eq!(config.cleanup_interval, Duration::from_secs(5));
288    }
289
290    #[test]
291    fn builder_sets_save_threshold_and_locking() {
292        let config = BootstrapCacheConfig::builder()
293            .min_peers_to_save(0)
294            .enable_file_locking(false)
295            .build();
296
297        assert_eq!(config.min_peers_to_save, 0);
298        assert!(!config.enable_file_locking);
299    }
300
301    #[test]
302    fn builder_replaces_quality_weights() {
303        let weights = QualityWeights {
304            success_rate: 1.0,
305            rtt: 2.0,
306            freshness: 3.0,
307            capabilities: 4.0,
308        };
309        let config = BootstrapCacheConfig::builder()
310            .weights(weights.clone())
311            .build();
312
313        assert!((config.weights.success_rate - weights.success_rate).abs() < f64::EPSILON);
314        assert!((config.weights.rtt - weights.rtt).abs() < f64::EPSILON);
315        assert!((config.weights.freshness - weights.freshness).abs() < f64::EPSILON);
316        assert!((config.weights.capabilities - weights.capabilities).abs() < f64::EPSILON);
317    }
318
319    #[test]
320    fn config_clone_preserves_custom_values() {
321        let config = BootstrapCacheConfig::builder()
322            .cache_dir("relative/cache")
323            .max_peers(42)
324            .epsilon(0.75)
325            .min_peers_to_save(3)
326            .enable_file_locking(false)
327            .build();
328        let cloned = config.clone();
329
330        assert_eq!(cloned.cache_dir, PathBuf::from("relative/cache"));
331        assert_eq!(cloned.max_peers, 42);
332        assert!((cloned.epsilon - 0.75).abs() < f64::EPSILON);
333        assert_eq!(cloned.min_peers_to_save, 3);
334        assert!(!cloned.enable_file_locking);
335    }
336}