use crate::error::{DaemonError, Result};
use crate::models::{FixtureState, FlakinessRecord, NativeTestNode, ScheduledTest, TestNode};
use rmp_serde::{Deserializer, Serializer};
use serde::{Deserialize, Serialize};
use sled::{Db, Tree};
use std::path::PathBuf;
use tracing::{debug, error};
const TREE_INVENTORY: &str = "inventory";
const TREE_NATIVE_TESTS: &str = "native_tests";
const TREE_FLAKINESS: &str = "flakiness";
const TREE_FIXTURES: &str = "fixtures";
const TREE_DURATION_HISTORY: &str = "duration_history";
const TREE_CONTEXTS: &str = "contexts";
const TREE_SCHEDULER: &str = "scheduler";
const TREE_CONFIG: &str = "config";
const STORAGE_VERSION: u32 = 1;
#[derive(Clone)]
pub struct DaemonStorage {
db: Db,
inventory: Tree,
native_tests: Tree,
flakiness: Tree,
fixtures: Tree,
duration_history: Tree,
contexts: Tree,
scheduler: Tree,
config: Tree,
}
impl std::fmt::Debug for DaemonStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DaemonStorage").finish_non_exhaustive()
}
}
impl DaemonStorage {
pub fn open(storage_path: &PathBuf) -> Result<Self> {
let config = sled::Config::default()
.path(storage_path)
.temporary(false)
.cache_capacity(256 * 1024 * 1024) .flush_every_ms(Some(5000)) .mode(sled::Mode::HighThroughput);
let db = config.open()?;
let inventory = db.open_tree(TREE_INVENTORY)?;
let native_tests = db.open_tree(TREE_NATIVE_TESTS)?;
let flakiness = db.open_tree(TREE_FLAKINESS)?;
let fixtures = db.open_tree(TREE_FIXTURES)?;
let duration_history = db.open_tree(TREE_DURATION_HISTORY)?;
let contexts = db.open_tree(TREE_CONTEXTS)?;
let scheduler = db.open_tree(TREE_SCHEDULER)?;
let config_tree = db.open_tree(TREE_CONFIG)?;
if let Some(version_bytes) = config_tree.get("version")? {
let version: u32 = rmp_serde::from_slice(&version_bytes)?;
if version != STORAGE_VERSION {
error!(
"Storage schema version mismatch: expected {}, found {}",
STORAGE_VERSION, version
);
return Err(DaemonError::Other(format!(
"Storage schema version mismatch: expected {}, found {}",
STORAGE_VERSION, version
)));
}
} else {
let mut version_bytes = Vec::new();
STORAGE_VERSION.serialize(&mut Serializer::new(&mut version_bytes))?;
config_tree.insert("version", version_bytes)?;
}
Ok(Self {
db,
inventory,
native_tests,
flakiness,
fixtures,
duration_history,
contexts,
scheduler,
config: config_tree,
})
}
pub fn clear_all(&self) -> Result<()> {
self.inventory.clear()?;
self.native_tests.clear()?;
self.flakiness.clear()?;
self.fixtures.clear()?;
self.duration_history.clear()?;
self.contexts.clear()?;
self.scheduler.clear()?;
Ok(())
}
pub fn save_test_node(&self, node: &TestNode) -> Result<()> {
let mut buf = Vec::new();
node.serialize(&mut Serializer::new(&mut buf))?;
self.inventory.insert(&node.node_id, buf)?;
Ok(())
}
pub fn save_test_nodes_batch(&self, nodes: &[TestNode]) -> Result<()> {
if nodes.is_empty() {
return Ok(());
}
let mut batch = sled::Batch::default();
for node in nodes {
let mut buf = Vec::new();
node.serialize(&mut Serializer::new(&mut buf))?;
batch.insert(node.node_id.as_bytes(), buf);
}
self.inventory.apply_batch(batch)?;
Ok(())
}
pub fn load_test_node(&self, node_id: &str) -> Result<Option<TestNode>> {
if let Some(bytes) = self.inventory.get(node_id)? {
let mut deserializer = Deserializer::new(&bytes[..]);
let node: TestNode = Deserialize::deserialize(&mut deserializer)?;
Ok(Some(node))
} else {
Ok(None)
}
}
pub fn get_all_inventory(&self) -> Result<Vec<TestNode>> {
let mut nodes = Vec::new();
for item in self.inventory.iter() {
let (_, bytes) = item?;
let mut deserializer = Deserializer::new(&bytes[..]);
let node: TestNode = Deserialize::deserialize(&mut deserializer)?;
nodes.push(node);
}
Ok(nodes)
}
pub fn inventory_count(&self) -> usize {
self.inventory.len()
}
pub fn clear_inventory(&self) -> Result<()> {
self.inventory.clear()?;
Ok(())
}
pub fn save_native_test(&self, node: &NativeTestNode) -> Result<()> {
let mut buf = Vec::new();
node.serialize(&mut Serializer::new(&mut buf))?;
self.native_tests.insert(&node.node_id, buf)?;
Ok(())
}
pub fn save_native_tests_batch(&self, nodes: &[NativeTestNode]) -> Result<()> {
if nodes.is_empty() {
return Ok(());
}
let mut batch = sled::Batch::default();
for node in nodes {
let mut buf = Vec::new();
node.serialize(&mut Serializer::new(&mut buf))?;
batch.insert(node.node_id.as_bytes(), buf);
}
self.native_tests.apply_batch(batch)?;
Ok(())
}
pub fn load_native_test(&self, node_id: &str) -> Result<Option<NativeTestNode>> {
if let Some(bytes) = self.native_tests.get(node_id)? {
let mut deserializer = Deserializer::new(&bytes[..]);
let node: NativeTestNode = Deserialize::deserialize(&mut deserializer)?;
Ok(Some(node))
} else {
Ok(None)
}
}
pub fn get_all_native_tests(&self) -> Result<Vec<NativeTestNode>> {
let mut nodes = Vec::new();
for item in self.native_tests.iter() {
let (_, bytes) = item?;
let mut deserializer = Deserializer::new(&bytes[..]);
let node: NativeTestNode = Deserialize::deserialize(&mut deserializer)?;
nodes.push(node);
}
Ok(nodes)
}
pub fn clear_native_tests(&self) -> Result<()> {
self.native_tests.clear()?;
Ok(())
}
pub fn save_flakiness_record(&self, record: &FlakinessRecord) -> Result<()> {
let mut buf = Vec::new();
record.serialize(&mut Serializer::new(&mut buf))?;
self.flakiness.insert(&record.node_id, buf)?;
Ok(())
}
pub fn load_flakiness_record(&self, node_id: &str) -> Result<Option<FlakinessRecord>> {
if let Some(bytes) = self.flakiness.get(node_id)? {
let mut deserializer = Deserializer::new(&bytes[..]);
let record: FlakinessRecord = Deserialize::deserialize(&mut deserializer)?;
Ok(Some(record))
} else {
Ok(None)
}
}
pub fn get_all_flakiness(&self) -> Result<Vec<FlakinessRecord>> {
let mut records = Vec::new();
for item in self.flakiness.iter() {
let (_, bytes) = item?;
let mut deserializer = Deserializer::new(&bytes[..]);
let record: FlakinessRecord = Deserialize::deserialize(&mut deserializer)?;
records.push(record);
}
Ok(records)
}
pub fn delete_flakiness_record(&self, node_id: &str) -> Result<()> {
self.flakiness.remove(node_id)?;
Ok(())
}
pub fn save_fixture(&self, context_id: &str, fixture: &FixtureState) -> Result<()> {
let key = format!("{}/{}", context_id, fixture.name);
let mut buf = Vec::new();
fixture.serialize(&mut Serializer::new(&mut buf))?;
self.fixtures.insert(key, buf)?;
Ok(())
}
pub fn load_fixture(&self, context_id: &str, name: &str) -> Result<Option<FixtureState>> {
let key = format!("{}/{}", context_id, name);
if let Some(bytes) = self.fixtures.get(key)? {
let mut deserializer = Deserializer::new(&bytes[..]);
let fixture: FixtureState = Deserialize::deserialize(&mut deserializer)?;
Ok(Some(fixture))
} else {
Ok(None)
}
}
pub fn get_context_fixtures(&self, context_id: &str) -> Result<Vec<FixtureState>> {
let prefix = format!("{}/", context_id);
let mut fixtures = Vec::new();
for item in self.fixtures.scan_prefix(prefix) {
let (_, bytes) = item?;
let mut deserializer = Deserializer::new(&bytes[..]);
let fixture: FixtureState = Deserialize::deserialize(&mut deserializer)?;
fixtures.push(fixture);
}
Ok(fixtures)
}
pub fn delete_fixture(&self, context_id: &str, name: &str) -> Result<()> {
let key = format!("{}/{}", context_id, name);
self.fixtures.remove(key)?;
Ok(())
}
pub fn clear_context_fixtures(&self, context_id: &str) -> Result<()> {
let prefix = format!("{}/", context_id);
for item in self.fixtures.scan_prefix(prefix) {
let (key, _) = item?;
self.fixtures.remove(key)?;
}
Ok(())
}
pub fn save_duration_history(&self, node_id: &str, durations: &[u64]) -> Result<()> {
let mut buf = Vec::new();
durations.serialize(&mut Serializer::new(&mut buf))?;
self.duration_history.insert(node_id, buf)?;
Ok(())
}
pub fn load_duration_history(&self, node_id: &str) -> Result<Option<Vec<u64>>> {
if let Some(bytes) = self.duration_history.get(node_id)? {
let mut deserializer = Deserializer::new(&bytes[..]);
let durations: Vec<u64> = Deserialize::deserialize(&mut deserializer)?;
Ok(Some(durations))
} else {
Ok(None)
}
}
pub fn get_average_duration(&self, node_id: &str) -> Result<Option<u64>> {
if let Some(durations) = self.load_duration_history(node_id)? {
if durations.is_empty() {
return Ok(None);
}
let sum: u64 = durations.iter().sum();
Ok(Some(sum / durations.len() as u64))
} else {
Ok(None)
}
}
pub fn save_duration_history_batch(&self, histories: &[(&str, &[u64])]) -> Result<()> {
if histories.is_empty() {
return Ok(());
}
let mut batch = sled::Batch::default();
for (node_id, durations) in histories {
let mut buf = Vec::new();
durations.serialize(&mut Serializer::new(&mut buf))?;
batch.insert(node_id.as_bytes(), buf);
}
self.duration_history.apply_batch(batch)?;
Ok(())
}
pub fn save_scheduled_test(&self, test: &ScheduledTest) -> Result<()> {
let mut buf = Vec::new();
test.serialize(&mut Serializer::new(&mut buf))?;
self.scheduler.insert(&test.node_id, buf)?;
Ok(())
}
pub fn get_all_scheduled_tests(&self) -> Result<Vec<ScheduledTest>> {
let mut tests = Vec::new();
for item in self.scheduler.iter() {
let (_, bytes) = item?;
let mut deserializer = Deserializer::new(&bytes[..]);
let test: ScheduledTest = Deserialize::deserialize(&mut deserializer)?;
tests.push(test);
}
Ok(tests)
}
pub fn clear_scheduler(&self) -> Result<()> {
self.scheduler.clear()?;
Ok(())
}
pub fn save_context(&self, context_id: &str, data: &serde_json::Value) -> Result<()> {
let bytes = serde_json::to_vec(data)?;
self.contexts.insert(context_id, bytes)?;
Ok(())
}
pub fn load_context(&self, context_id: &str) -> Result<Option<serde_json::Value>> {
if let Some(bytes) = self.contexts.get(context_id)? {
let data: serde_json::Value = serde_json::from_slice(&bytes)?;
Ok(Some(data))
} else {
Ok(None)
}
}
pub fn get_all_context_ids(&self) -> Vec<String> {
self.contexts
.iter()
.filter_map(|item| item.ok())
.map(|(key, _)| String::from_utf8_lossy(&key).to_string())
.collect()
}
pub fn delete_context(&self, context_id: &str) -> Result<()> {
self.contexts.remove(context_id)?;
Ok(())
}
pub fn save_config(&self, key: &str, value: &serde_json::Value) -> Result<()> {
let bytes = serde_json::to_vec(value)?;
self.config.insert(key, bytes)?;
Ok(())
}
pub fn load_config(&self, key: &str) -> Result<Option<serde_json::Value>> {
if let Some(bytes) = self.config.get(key)? {
let value: serde_json::Value = serde_json::from_slice(&bytes)?;
Ok(Some(value))
} else {
Ok(None)
}
}
pub fn flush(&self) -> Result<()> {
self.db.flush()?;
Ok(())
}
pub fn evict_old_data(&self, _max_age_days: u32) -> Result<usize> {
let mut removed = 0;
let mut keys_to_remove = Vec::new();
for item in self.flakiness.iter() {
if let Ok((key, value)) = item {
let mut deserializer = Deserializer::new(&value[..]);
if let Ok(record) = FlakinessRecord::deserialize(&mut deserializer) {
if record.total_runs < 3 {
keys_to_remove.push(key);
}
}
}
}
for key in keys_to_remove {
self.flakiness.remove(key)?;
removed += 1;
}
if removed > 100 {
debug!("Evicted {} records, triggering compaction", removed);
self.compact_async();
}
Ok(removed)
}
pub fn compact_async(&self) {
let db = self.db.clone();
std::thread::spawn(move || {
if let Err(e) = db.flush() {
error!("Background compaction failed: {}", e);
}
});
}
pub fn db(&self) -> &Db {
&self.db
}
}