Skip to main content

ant_node/payment/
metrics.rs

1//! Quoting metrics tracking for ant-node.
2//!
3//! Tracks metrics used for quote generation, including:
4//! - `received_payment_count` - number of payments received
5//! - Storage capacity and usage
6//! - Network liveness information
7
8use crate::logging::{debug, info, warn};
9use evmlib::quoting_metrics::QuotingMetrics;
10use parking_lot::RwLock;
11use std::path::PathBuf;
12use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
13use std::time::Instant;
14
15/// Number of operations between disk persists (debounce).
16const PERSIST_INTERVAL: usize = 10;
17
18/// Tracker for quoting metrics.
19///
20/// Maintains state that influences quote pricing, including payment history,
21/// storage usage, and network information.
22#[derive(Debug)]
23pub struct QuotingMetricsTracker {
24    /// Number of payments received by this node.
25    received_payment_count: AtomicUsize,
26    /// Number of records currently stored.
27    close_records_stored: AtomicUsize,
28    /// Records stored by type: `Vec<(data_type_index, count)>`.
29    records_per_type: RwLock<Vec<(u32, u32)>>,
30    /// Node start time for calculating `live_time`.
31    start_time: Instant,
32    /// Path for persisting metrics (optional).
33    persist_path: Option<PathBuf>,
34    /// Estimated network size.
35    network_size: AtomicU64,
36    /// Operations since last persist (for debouncing disk I/O).
37    ops_since_persist: AtomicUsize,
38}
39
40impl QuotingMetricsTracker {
41    /// Create a new metrics tracker.
42    ///
43    /// # Arguments
44    ///
45    /// * `initial_records` - Initial number of records stored
46    #[must_use]
47    pub fn new(initial_records: usize) -> Self {
48        Self {
49            received_payment_count: AtomicUsize::new(0),
50            close_records_stored: AtomicUsize::new(initial_records),
51            records_per_type: RwLock::new(Vec::new()),
52            start_time: Instant::now(),
53            persist_path: None,
54            network_size: AtomicU64::new(500), // Conservative default
55            ops_since_persist: AtomicUsize::new(0),
56        }
57    }
58
59    /// Create a new metrics tracker with persistence.
60    ///
61    /// # Arguments
62    ///
63    /// * `persist_path` - Path to persist metrics to disk
64    #[must_use]
65    pub fn with_persistence(persist_path: &std::path::Path) -> Self {
66        let mut tracker = Self::new(0);
67        tracker.persist_path = Some(persist_path.to_path_buf());
68
69        // Try to load existing metrics
70        if let Some(loaded) = Self::load_from_disk(persist_path) {
71            tracker
72                .received_payment_count
73                .store(loaded.received_payment_count, Ordering::SeqCst);
74            tracker
75                .close_records_stored
76                .store(loaded.close_records_stored, Ordering::SeqCst);
77            *tracker.records_per_type.write() = loaded.records_per_type;
78            info!(
79                "Loaded persisted metrics: {} payments received",
80                loaded.received_payment_count
81            );
82        }
83
84        tracker
85    }
86
87    /// Record a payment received.
88    pub fn record_payment(&self) {
89        let count = self.received_payment_count.fetch_add(1, Ordering::SeqCst) + 1;
90        debug!("Payment received, total count: {count}");
91        self.maybe_persist();
92    }
93
94    /// Record data stored.
95    ///
96    /// # Arguments
97    ///
98    /// * `data_type` - Type index of the data
99    pub fn record_store(&self, data_type: u32) {
100        self.close_records_stored.fetch_add(1, Ordering::SeqCst);
101
102        // Update per-type counts (scope the write lock)
103        {
104            let mut records = self.records_per_type.write();
105            if let Some(entry) = records.iter_mut().find(|(t, _)| *t == data_type) {
106                entry.1 = entry.1.saturating_add(1);
107            } else {
108                records.push((data_type, 1));
109            }
110        }
111
112        self.maybe_persist();
113    }
114
115    /// Get the number of payments received.
116    #[must_use]
117    pub fn payment_count(&self) -> usize {
118        self.received_payment_count.load(Ordering::SeqCst)
119    }
120
121    /// Get the number of records stored.
122    #[must_use]
123    pub fn records_stored(&self) -> usize {
124        self.close_records_stored.load(Ordering::SeqCst)
125    }
126
127    /// Get the node's live time in hours.
128    #[must_use]
129    pub fn live_time_hours(&self) -> u64 {
130        self.start_time.elapsed().as_secs() / 3600
131    }
132
133    /// Update the estimated network size.
134    pub fn set_network_size(&self, size: u64) {
135        self.network_size.store(size, Ordering::SeqCst);
136    }
137
138    /// Get quoting metrics for quote generation.
139    ///
140    /// # Arguments
141    ///
142    /// * `data_size` - Size of the data being quoted
143    /// * `data_type` - Type index of the data
144    #[must_use]
145    pub fn get_metrics(&self, data_size: usize, data_type: u32) -> QuotingMetrics {
146        QuotingMetrics {
147            data_type,
148            data_size,
149            close_records_stored: self.close_records_stored.load(Ordering::SeqCst),
150            records_per_type: self.records_per_type.read().clone(),
151            received_payment_count: self.received_payment_count.load(Ordering::SeqCst),
152            live_time: self.live_time_hours(),
153            network_density: None, // Not used in pricing; reserved for future DHT range filtering
154            network_size: Some(self.network_size.load(Ordering::SeqCst)),
155        }
156    }
157
158    /// Debounced persist: only writes to disk every `PERSIST_INTERVAL` operations.
159    fn maybe_persist(&self) {
160        let ops = self.ops_since_persist.fetch_add(1, Ordering::Relaxed);
161        if ops % PERSIST_INTERVAL == 0 {
162            self.persist();
163        }
164    }
165
166    /// Persist metrics to disk.
167    fn persist(&self) {
168        if let Some(ref path) = self.persist_path {
169            let data = PersistedMetrics {
170                received_payment_count: self.received_payment_count.load(Ordering::SeqCst),
171                close_records_stored: self.close_records_stored.load(Ordering::SeqCst),
172                records_per_type: self.records_per_type.read().clone(),
173            };
174
175            if let Ok(bytes) = rmp_serde::to_vec(&data) {
176                if let Err(e) = std::fs::write(path, bytes) {
177                    warn!("Failed to persist metrics: {e}");
178                }
179            }
180        }
181    }
182
183    /// Load metrics from disk.
184    fn load_from_disk(path: &std::path::Path) -> Option<PersistedMetrics> {
185        let bytes = std::fs::read(path).ok()?;
186        rmp_serde::from_slice(&bytes).ok()
187    }
188}
189
190impl Drop for QuotingMetricsTracker {
191    fn drop(&mut self) {
192        self.persist();
193    }
194}
195
196/// Metrics persisted to disk.
197#[derive(Debug, serde::Serialize, serde::Deserialize)]
198struct PersistedMetrics {
199    received_payment_count: usize,
200    close_records_stored: usize,
201    records_per_type: Vec<(u32, u32)>,
202}
203
204#[cfg(test)]
205#[allow(clippy::expect_used)]
206mod tests {
207    use super::*;
208    use tempfile::tempdir;
209
210    #[test]
211    fn test_new_tracker() {
212        let tracker = QuotingMetricsTracker::new(50);
213        assert_eq!(tracker.payment_count(), 0);
214        assert_eq!(tracker.records_stored(), 50);
215    }
216
217    #[test]
218    fn test_record_payment() {
219        let tracker = QuotingMetricsTracker::new(0);
220        assert_eq!(tracker.payment_count(), 0);
221
222        tracker.record_payment();
223        assert_eq!(tracker.payment_count(), 1);
224
225        tracker.record_payment();
226        assert_eq!(tracker.payment_count(), 2);
227    }
228
229    #[test]
230    fn test_record_store() {
231        let tracker = QuotingMetricsTracker::new(0);
232        assert_eq!(tracker.records_stored(), 0);
233
234        tracker.record_store(0); // Chunk type
235        assert_eq!(tracker.records_stored(), 1);
236
237        tracker.record_store(0);
238        tracker.record_store(1); // Different type
239        assert_eq!(tracker.records_stored(), 3);
240
241        let metrics = tracker.get_metrics(1024, 0);
242        assert_eq!(metrics.records_per_type.len(), 2);
243    }
244
245    #[test]
246    fn test_get_metrics() {
247        let tracker = QuotingMetricsTracker::new(100);
248        tracker.record_payment();
249        tracker.record_payment();
250
251        let metrics = tracker.get_metrics(2048, 0);
252        assert_eq!(metrics.data_size, 2048);
253        assert_eq!(metrics.data_type, 0);
254        assert_eq!(metrics.close_records_stored, 100);
255        assert_eq!(metrics.received_payment_count, 2);
256    }
257
258    #[test]
259    fn test_persistence() {
260        let dir = tempdir().expect("tempdir");
261        let path = dir.path().join("metrics.bin");
262
263        // Create and populate tracker
264        {
265            let tracker = QuotingMetricsTracker::with_persistence(&path);
266            tracker.record_payment();
267            tracker.record_payment();
268            tracker.record_store(0);
269        }
270
271        // Load from disk
272        let tracker = QuotingMetricsTracker::with_persistence(&path);
273        assert_eq!(tracker.payment_count(), 2);
274        assert_eq!(tracker.records_stored(), 1);
275    }
276
277    #[test]
278    fn test_live_time_hours() {
279        let tracker = QuotingMetricsTracker::new(0);
280        // Just started, so live_time should be 0 hours
281        assert_eq!(tracker.live_time_hours(), 0);
282    }
283
284    #[test]
285    fn test_set_network_size() {
286        let tracker = QuotingMetricsTracker::new(0);
287        tracker.set_network_size(1000);
288
289        let metrics = tracker.get_metrics(0, 0);
290        assert_eq!(metrics.network_size, Some(1000));
291    }
292
293    #[test]
294    fn test_records_per_type_multiple_types() {
295        let tracker = QuotingMetricsTracker::new(0);
296
297        tracker.record_store(0);
298        tracker.record_store(0);
299        tracker.record_store(1);
300        tracker.record_store(2);
301        tracker.record_store(1);
302
303        let metrics = tracker.get_metrics(0, 0);
304        assert_eq!(metrics.records_per_type.len(), 3);
305
306        // Verify per-type counts
307        let type_0 = metrics.records_per_type.iter().find(|(t, _)| *t == 0);
308        let type_1 = metrics.records_per_type.iter().find(|(t, _)| *t == 1);
309        let type_2 = metrics.records_per_type.iter().find(|(t, _)| *t == 2);
310
311        assert_eq!(type_0.expect("type 0 exists").1, 2);
312        assert_eq!(type_1.expect("type 1 exists").1, 2);
313        assert_eq!(type_2.expect("type 2 exists").1, 1);
314    }
315
316    #[test]
317    fn test_persistence_round_trip_with_types() {
318        let dir = tempdir().expect("tempdir");
319        let path = dir.path().join("metrics_types.bin");
320
321        {
322            let tracker = QuotingMetricsTracker::with_persistence(&path);
323            tracker.record_store(0);
324            tracker.record_store(0);
325            tracker.record_store(1);
326            tracker.record_payment();
327        }
328
329        let tracker = QuotingMetricsTracker::with_persistence(&path);
330        assert_eq!(tracker.payment_count(), 1);
331        assert_eq!(tracker.records_stored(), 3); // 2 type-0 + 1 type-1
332
333        let metrics = tracker.get_metrics(0, 0);
334        assert_eq!(metrics.records_per_type.len(), 2);
335    }
336
337    #[test]
338    fn test_with_persistence_nonexistent_path() {
339        let dir = tempdir().expect("tempdir");
340        let path = dir.path().join("nonexistent_subdir").join("metrics.bin");
341
342        // Should not panic — just starts with defaults
343        let tracker = QuotingMetricsTracker::with_persistence(&path);
344        assert_eq!(tracker.payment_count(), 0);
345        assert_eq!(tracker.records_stored(), 0);
346    }
347
348    #[test]
349    fn test_get_metrics_passes_data_params() {
350        let tracker = QuotingMetricsTracker::new(0);
351        let metrics = tracker.get_metrics(4096, 3);
352        assert_eq!(metrics.data_size, 4096);
353        assert_eq!(metrics.data_type, 3);
354    }
355
356    #[test]
357    fn test_default_network_size() {
358        let tracker = QuotingMetricsTracker::new(0);
359        let metrics = tracker.get_metrics(0, 0);
360        assert_eq!(metrics.network_size, Some(500));
361    }
362}