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