Skip to main content

rpytest_daemon/
storage.rs

1//! Storage layer using sled for persistent data.
2
3use crate::error::{DaemonError, Result};
4use crate::models::{FixtureState, FlakinessRecord, NativeTestNode, ScheduledTest, TestNode};
5use rmp_serde::{Deserializer, Serializer};
6use serde::{Deserialize, Serialize};
7use sled::{Db, Tree};
8use std::path::PathBuf;
9use tracing::{debug, error};
10
11/// Storage tree names.
12const TREE_INVENTORY: &str = "inventory";
13const TREE_NATIVE_TESTS: &str = "native_tests";
14const TREE_FLAKINESS: &str = "flakiness";
15const TREE_FIXTURES: &str = "fixtures";
16const TREE_DURATION_HISTORY: &str = "duration_history";
17const TREE_CONTEXTS: &str = "contexts";
18const TREE_SCHEDULER: &str = "scheduler";
19const TREE_CONFIG: &str = "config";
20
21/// Schema version for storage format compatibility.
22const STORAGE_VERSION: u32 = 1;
23
24/// Main storage wrapper for the daemon.
25#[derive(Clone)]
26pub struct DaemonStorage {
27    db: Db,
28    inventory: Tree,
29    native_tests: Tree,
30    flakiness: Tree,
31    fixtures: Tree,
32    duration_history: Tree,
33    contexts: Tree,
34    scheduler: Tree,
35    config: Tree,
36}
37
38impl std::fmt::Debug for DaemonStorage {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("DaemonStorage").finish_non_exhaustive()
41    }
42}
43
44impl DaemonStorage {
45    /// Open or create the storage database.
46    pub fn open(storage_path: &PathBuf) -> Result<Self> {
47        // Configure sled for high performance:
48        // - cache_capacity: 256MB for hot data caching
49        // - flush_every_ms: batch flushes every 5s instead of immediate
50        // - mode: HighThroughput for write-heavy workloads
51        let config = sled::Config::default()
52            .path(storage_path)
53            .temporary(false)
54            .cache_capacity(256 * 1024 * 1024) // 256MB cache
55            .flush_every_ms(Some(5000)) // Batch flushes every 5s
56            .mode(sled::Mode::HighThroughput);
57
58        let db = config.open()?;
59
60        // Open or create trees
61        let inventory = db.open_tree(TREE_INVENTORY)?;
62        let native_tests = db.open_tree(TREE_NATIVE_TESTS)?;
63        let flakiness = db.open_tree(TREE_FLAKINESS)?;
64        let fixtures = db.open_tree(TREE_FIXTURES)?;
65        let duration_history = db.open_tree(TREE_DURATION_HISTORY)?;
66        let contexts = db.open_tree(TREE_CONTEXTS)?;
67        let scheduler = db.open_tree(TREE_SCHEDULER)?;
68        let config_tree = db.open_tree(TREE_CONFIG)?;
69
70        // Validate schema version
71        if let Some(version_bytes) = config_tree.get("version")? {
72            let version: u32 = rmp_serde::from_slice(&version_bytes)?;
73            if version != STORAGE_VERSION {
74                error!(
75                    "Storage schema version mismatch: expected {}, found {}",
76                    STORAGE_VERSION, version
77                );
78                return Err(DaemonError::Other(format!(
79                    "Storage schema version mismatch: expected {}, found {}",
80                    STORAGE_VERSION, version
81                )));
82            }
83        } else {
84            // First run - set version
85            let mut version_bytes = Vec::new();
86            STORAGE_VERSION.serialize(&mut Serializer::new(&mut version_bytes))?;
87            config_tree.insert("version", version_bytes)?;
88        }
89
90        Ok(Self {
91            db,
92            inventory,
93            native_tests,
94            flakiness,
95            fixtures,
96            duration_history,
97            contexts,
98            scheduler,
99            config: config_tree,
100        })
101    }
102
103    /// Clear all data (for testing or reset).
104    pub fn clear_all(&self) -> Result<()> {
105        self.inventory.clear()?;
106        self.native_tests.clear()?;
107        self.flakiness.clear()?;
108        self.fixtures.clear()?;
109        self.duration_history.clear()?;
110        self.contexts.clear()?;
111        self.scheduler.clear()?;
112        Ok(())
113    }
114
115    // ==================== Inventory ====================
116
117    /// Save a test node to inventory.
118    pub fn save_test_node(&self, node: &TestNode) -> Result<()> {
119        let mut buf = Vec::new();
120        node.serialize(&mut Serializer::new(&mut buf))?;
121        self.inventory.insert(&node.node_id, buf)?;
122        Ok(())
123    }
124
125    /// Save multiple test nodes to inventory in a batch (much faster than individual saves).
126    pub fn save_test_nodes_batch(&self, nodes: &[TestNode]) -> Result<()> {
127        if nodes.is_empty() {
128            return Ok(());
129        }
130
131        let mut batch = sled::Batch::default();
132        for node in nodes {
133            let mut buf = Vec::new();
134            node.serialize(&mut Serializer::new(&mut buf))?;
135            batch.insert(node.node_id.as_bytes(), buf);
136        }
137        self.inventory.apply_batch(batch)?;
138        Ok(())
139    }
140
141    /// Load a test node from inventory.
142    pub fn load_test_node(&self, node_id: &str) -> Result<Option<TestNode>> {
143        if let Some(bytes) = self.inventory.get(node_id)? {
144            let mut deserializer = Deserializer::new(&bytes[..]);
145            let node: TestNode = Deserialize::deserialize(&mut deserializer)?;
146            Ok(Some(node))
147        } else {
148            Ok(None)
149        }
150    }
151
152    /// Get all test nodes in inventory.
153    pub fn get_all_inventory(&self) -> Result<Vec<TestNode>> {
154        let mut nodes = Vec::new();
155        for item in self.inventory.iter() {
156            let (_, bytes) = item?;
157            let mut deserializer = Deserializer::new(&bytes[..]);
158            let node: TestNode = Deserialize::deserialize(&mut deserializer)?;
159            nodes.push(node);
160        }
161        Ok(nodes)
162    }
163
164    /// Get inventory count.
165    pub fn inventory_count(&self) -> usize {
166        self.inventory.len()
167    }
168
169    /// Clear inventory.
170    pub fn clear_inventory(&self) -> Result<()> {
171        self.inventory.clear()?;
172        Ok(())
173    }
174
175    // ==================== Native Tests ====================
176
177    /// Save a native test node.
178    pub fn save_native_test(&self, node: &NativeTestNode) -> Result<()> {
179        let mut buf = Vec::new();
180        node.serialize(&mut Serializer::new(&mut buf))?;
181        self.native_tests.insert(&node.node_id, buf)?;
182        Ok(())
183    }
184
185    /// Save multiple native test nodes in a batch (much faster than individual saves).
186    pub fn save_native_tests_batch(&self, nodes: &[NativeTestNode]) -> Result<()> {
187        if nodes.is_empty() {
188            return Ok(());
189        }
190
191        let mut batch = sled::Batch::default();
192        for node in nodes {
193            let mut buf = Vec::new();
194            node.serialize(&mut Serializer::new(&mut buf))?;
195            batch.insert(node.node_id.as_bytes(), buf);
196        }
197        self.native_tests.apply_batch(batch)?;
198        Ok(())
199    }
200
201    /// Load a native test node.
202    pub fn load_native_test(&self, node_id: &str) -> Result<Option<NativeTestNode>> {
203        if let Some(bytes) = self.native_tests.get(node_id)? {
204            let mut deserializer = Deserializer::new(&bytes[..]);
205            let node: NativeTestNode = Deserialize::deserialize(&mut deserializer)?;
206            Ok(Some(node))
207        } else {
208            Ok(None)
209        }
210    }
211
212    /// Get all native tests.
213    pub fn get_all_native_tests(&self) -> Result<Vec<NativeTestNode>> {
214        let mut nodes = Vec::new();
215        for item in self.native_tests.iter() {
216            let (_, bytes) = item?;
217            let mut deserializer = Deserializer::new(&bytes[..]);
218            let node: NativeTestNode = Deserialize::deserialize(&mut deserializer)?;
219            nodes.push(node);
220        }
221        Ok(nodes)
222    }
223
224    /// Clear native tests.
225    pub fn clear_native_tests(&self) -> Result<()> {
226        self.native_tests.clear()?;
227        Ok(())
228    }
229
230    // ==================== Flakiness ====================
231
232    /// Save flakiness record.
233    pub fn save_flakiness_record(&self, record: &FlakinessRecord) -> Result<()> {
234        let mut buf = Vec::new();
235        record.serialize(&mut Serializer::new(&mut buf))?;
236        self.flakiness.insert(&record.node_id, buf)?;
237        Ok(())
238    }
239
240    /// Load flakiness record.
241    pub fn load_flakiness_record(&self, node_id: &str) -> Result<Option<FlakinessRecord>> {
242        if let Some(bytes) = self.flakiness.get(node_id)? {
243            let mut deserializer = Deserializer::new(&bytes[..]);
244            let record: FlakinessRecord = Deserialize::deserialize(&mut deserializer)?;
245            Ok(Some(record))
246        } else {
247            Ok(None)
248        }
249    }
250
251    /// Get all flakiness records.
252    pub fn get_all_flakiness(&self) -> Result<Vec<FlakinessRecord>> {
253        let mut records = Vec::new();
254        for item in self.flakiness.iter() {
255            let (_, bytes) = item?;
256            let mut deserializer = Deserializer::new(&bytes[..]);
257            let record: FlakinessRecord = Deserialize::deserialize(&mut deserializer)?;
258            records.push(record);
259        }
260        Ok(records)
261    }
262
263    /// Delete flakiness record.
264    pub fn delete_flakiness_record(&self, node_id: &str) -> Result<()> {
265        self.flakiness.remove(node_id)?;
266        Ok(())
267    }
268
269    // ==================== Fixtures ====================
270
271    /// Save fixture state.
272    pub fn save_fixture(&self, context_id: &str, fixture: &FixtureState) -> Result<()> {
273        let key = format!("{}/{}", context_id, fixture.name);
274        let mut buf = Vec::new();
275        fixture.serialize(&mut Serializer::new(&mut buf))?;
276        self.fixtures.insert(key, buf)?;
277        Ok(())
278    }
279
280    /// Load fixture state.
281    pub fn load_fixture(&self, context_id: &str, name: &str) -> Result<Option<FixtureState>> {
282        let key = format!("{}/{}", context_id, name);
283        if let Some(bytes) = self.fixtures.get(key)? {
284            let mut deserializer = Deserializer::new(&bytes[..]);
285            let fixture: FixtureState = Deserialize::deserialize(&mut deserializer)?;
286            Ok(Some(fixture))
287        } else {
288            Ok(None)
289        }
290    }
291
292    /// Get all fixtures for a context.
293    pub fn get_context_fixtures(&self, context_id: &str) -> Result<Vec<FixtureState>> {
294        let prefix = format!("{}/", context_id);
295        let mut fixtures = Vec::new();
296        for item in self.fixtures.scan_prefix(prefix) {
297            let (_, bytes) = item?;
298            let mut deserializer = Deserializer::new(&bytes[..]);
299            let fixture: FixtureState = Deserialize::deserialize(&mut deserializer)?;
300            fixtures.push(fixture);
301        }
302        Ok(fixtures)
303    }
304
305    /// Delete fixture.
306    pub fn delete_fixture(&self, context_id: &str, name: &str) -> Result<()> {
307        let key = format!("{}/{}", context_id, name);
308        self.fixtures.remove(key)?;
309        Ok(())
310    }
311
312    /// Clear all fixtures for a context.
313    pub fn clear_context_fixtures(&self, context_id: &str) -> Result<()> {
314        let prefix = format!("{}/", context_id);
315        for item in self.fixtures.scan_prefix(prefix) {
316            let (key, _) = item?;
317            self.fixtures.remove(key)?;
318        }
319        Ok(())
320    }
321
322    // ==================== Duration History ====================
323
324    /// Save duration history for a test.
325    pub fn save_duration_history(&self, node_id: &str, durations: &[u64]) -> Result<()> {
326        let mut buf = Vec::new();
327        durations.serialize(&mut Serializer::new(&mut buf))?;
328        self.duration_history.insert(node_id, buf)?;
329        Ok(())
330    }
331
332    /// Load duration history for a test.
333    pub fn load_duration_history(&self, node_id: &str) -> Result<Option<Vec<u64>>> {
334        if let Some(bytes) = self.duration_history.get(node_id)? {
335            let mut deserializer = Deserializer::new(&bytes[..]);
336            let durations: Vec<u64> = Deserialize::deserialize(&mut deserializer)?;
337            Ok(Some(durations))
338        } else {
339            Ok(None)
340        }
341    }
342
343    /// Get average duration for a test.
344    pub fn get_average_duration(&self, node_id: &str) -> Result<Option<u64>> {
345        if let Some(durations) = self.load_duration_history(node_id)? {
346            if durations.is_empty() {
347                return Ok(None);
348            }
349            let sum: u64 = durations.iter().sum();
350            Ok(Some(sum / durations.len() as u64))
351        } else {
352            Ok(None)
353        }
354    }
355
356    /// Save duration history for multiple tests in a batch (much faster than individual saves).
357    pub fn save_duration_history_batch(&self, histories: &[(&str, &[u64])]) -> Result<()> {
358        if histories.is_empty() {
359            return Ok(());
360        }
361
362        let mut batch = sled::Batch::default();
363        for (node_id, durations) in histories {
364            let mut buf = Vec::new();
365            durations.serialize(&mut Serializer::new(&mut buf))?;
366            batch.insert(node_id.as_bytes(), buf);
367        }
368        self.duration_history.apply_batch(batch)?;
369        Ok(())
370    }
371
372    // ==================== Scheduler ====================
373
374    /// Save scheduled test.
375    pub fn save_scheduled_test(&self, test: &ScheduledTest) -> Result<()> {
376        let mut buf = Vec::new();
377        test.serialize(&mut Serializer::new(&mut buf))?;
378        self.scheduler.insert(&test.node_id, buf)?;
379        Ok(())
380    }
381
382    /// Load all scheduled tests.
383    pub fn get_all_scheduled_tests(&self) -> Result<Vec<ScheduledTest>> {
384        let mut tests = Vec::new();
385        for item in self.scheduler.iter() {
386            let (_, bytes) = item?;
387            let mut deserializer = Deserializer::new(&bytes[..]);
388            let test: ScheduledTest = Deserialize::deserialize(&mut deserializer)?;
389            tests.push(test);
390        }
391        Ok(tests)
392    }
393
394    /// Clear scheduler.
395    pub fn clear_scheduler(&self) -> Result<()> {
396        self.scheduler.clear()?;
397        Ok(())
398    }
399
400    // ==================== Contexts ====================
401
402    /// Save context metadata (simple string map).
403    pub fn save_context(&self, context_id: &str, data: &serde_json::Value) -> Result<()> {
404        let bytes = serde_json::to_vec(data)?;
405        self.contexts.insert(context_id, bytes)?;
406        Ok(())
407    }
408
409    /// Load context metadata.
410    pub fn load_context(&self, context_id: &str) -> Result<Option<serde_json::Value>> {
411        if let Some(bytes) = self.contexts.get(context_id)? {
412            let data: serde_json::Value = serde_json::from_slice(&bytes)?;
413            Ok(Some(data))
414        } else {
415            Ok(None)
416        }
417    }
418
419    /// Get all context IDs.
420    pub fn get_all_context_ids(&self) -> Vec<String> {
421        self.contexts
422            .iter()
423            .filter_map(|item| item.ok())
424            .map(|(key, _)| String::from_utf8_lossy(&key).to_string())
425            .collect()
426    }
427
428    /// Delete context.
429    pub fn delete_context(&self, context_id: &str) -> Result<()> {
430        self.contexts.remove(context_id)?;
431        Ok(())
432    }
433
434    // ==================== Config ====================
435
436    /// Save config value.
437    pub fn save_config(&self, key: &str, value: &serde_json::Value) -> Result<()> {
438        let bytes = serde_json::to_vec(value)?;
439        self.config.insert(key, bytes)?;
440        Ok(())
441    }
442
443    /// Load config value.
444    pub fn load_config(&self, key: &str) -> Result<Option<serde_json::Value>> {
445        if let Some(bytes) = self.config.get(key)? {
446            let value: serde_json::Value = serde_json::from_slice(&bytes)?;
447            Ok(Some(value))
448        } else {
449            Ok(None)
450        }
451    }
452
453    /// Flush to disk.
454    pub fn flush(&self) -> Result<()> {
455        self.db.flush()?;
456        Ok(())
457    }
458
459    // ==================== Maintenance ====================
460
461    /// Evict old data that hasn't been accessed in `max_age_days`.
462    /// Returns the number of records removed.
463    pub fn evict_old_data(&self, _max_age_days: u32) -> Result<usize> {
464        // Note: max_age_days reserved for future timestamp-based eviction
465        let mut removed = 0;
466
467        // Evict old flakiness records with few runs
468        let mut keys_to_remove = Vec::new();
469        for item in self.flakiness.iter() {
470            if let Ok((key, value)) = item {
471                let mut deserializer = Deserializer::new(&value[..]);
472                if let Ok(record) = FlakinessRecord::deserialize(&mut deserializer) {
473                    // Records with few runs are candidates for eviction
474                    if record.total_runs < 3 {
475                        keys_to_remove.push(key);
476                    }
477                }
478            }
479        }
480
481        for key in keys_to_remove {
482            self.flakiness.remove(key)?;
483            removed += 1;
484        }
485
486        // Trigger compaction after bulk delete
487        if removed > 100 {
488            debug!("Evicted {} records, triggering compaction", removed);
489            self.compact_async();
490        }
491
492        Ok(removed)
493    }
494
495    /// Trigger background compaction to reclaim disk space.
496    pub fn compact_async(&self) {
497        let db = self.db.clone();
498        std::thread::spawn(move || {
499            if let Err(e) = db.flush() {
500                error!("Background compaction failed: {}", e);
501            }
502        });
503    }
504
505    /// Get the internal database for advanced operations.
506    pub fn db(&self) -> &Db {
507        &self.db
508    }
509}