use crate::config::StorageConfig;
use crate::error::{Result, StorageError};
use parking_lot::RwLock;
use rocksdb::{ColumnFamily, ColumnFamilyDescriptor, Options, WriteBatch, WriteOptions, DB};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
pub const CF_BLOCKS: &str = "blocks";
pub const CF_STATE: &str = "state";
pub const CF_ACCOUNTS: &str = "accounts";
pub const CF_TRANSACTIONS: &str = "transactions";
pub const CF_METADATA: &str = "metadata";
pub const CF_SNAPSHOTS: &str = "snapshots";
pub const CF_IDENTITIES: &str = "identities";
pub const CF_DELEGATIONS: &str = "delegations";
pub const CF_CREDENTIALS: &str = "credentials";
pub const CF_CHANNELS: &str = "channels";
pub const CF_AGENTS: &str = "agents";
pub const CF_MODELS: &str = "models";
pub const CF_PROVIDERS: &str = "providers";
pub const CF_TASKS: &str = "tasks";
pub const CF_AGENT_TEMPLATES: &str = "agent_templates";
pub const CF_SKILLS: &str = "skills";
pub const CF_TOOLS: &str = "tools";
pub const CF_KNOWLEDGE: &str = "knowledge";
pub const CF_WORKFLOW_TEMPLATES: &str = "workflow_templates";
pub const CF_TOKENS: &str = "tokens";
pub const CF_SETTLEMENTS: &str = "settlements";
pub const CF_MODEL_SERVICES: &str = "model_services";
pub const CF_NFTS: &str = "nfts";
pub const CF_EVENTS: &str = "events";
pub const CF_WEBHOOKS: &str = "webhooks";
pub const CF_COMPLIANCE: &str = "compliance";
pub const CF_TRAINING_RUNS: &str = "training_runs";
pub const CF_TRAINING_RECEIPTS: &str = "training_receipts";
pub const CF_AUDIT: &str = "audit";
pub const CF_APPROVALS: &str = "approvals";
pub const CF_API_KEYS: &str = "api_keys";
pub const CF_MPC_KEYSHARES: &str = "mpc_keyshares";
pub const CF_CANTON_ANALYTICS: &str = "canton_analytics";
pub const CF_BRIDGE_ANALYTICS: &str = "bridge_analytics";
pub const CF_VALIDATOR_MODULES: &str = "validator_modules";
pub trait KvStore: Send + Sync {
fn get(&self, cf: &str, key: &[u8]) -> Result<Option<Vec<u8>>>;
fn put(&self, cf: &str, key: &[u8], value: &[u8]) -> Result<()>;
fn delete(&self, cf: &str, key: &[u8]) -> Result<()>;
fn contains(&self, cf: &str, key: &[u8]) -> Result<bool> {
Ok(self.get(cf, key)?.is_some())
}
fn write_batch(&self, operations: Vec<WriteOp>) -> Result<()>;
fn write_batch_sync(&self, operations: Vec<WriteOp>) -> Result<()>;
fn get_keys_with_prefix(&self, cf: &str, prefix: &[u8]) -> Result<Vec<Vec<u8>>>;
fn scan_prefix(&self, cf: &str, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
let keys = self.get_keys_with_prefix(cf, prefix)?;
let mut results = Vec::with_capacity(keys.len());
for key in keys {
if let Some(value) = self.get(cf, &key)? {
results.push((key, value));
}
}
Ok(results)
}
fn scan_prefix_for_each(
&self,
cf: &str,
prefix: &[u8],
f: &mut dyn FnMut(&[u8], &[u8]) -> Result<()>,
) -> Result<()> {
for (key, value) in self.scan_prefix(cf, prefix)? {
f(&key, &value)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum WriteOp {
Put {
cf: String,
key: Vec<u8>,
value: Vec<u8>,
},
Delete { cf: String, key: Vec<u8> },
}
pub struct RocksDbStore {
db: Arc<DB>,
}
impl RocksDbStore {
fn column_family_descriptors() -> Vec<ColumnFamilyDescriptor> {
vec![
ColumnFamilyDescriptor::new(CF_BLOCKS, Options::default()),
ColumnFamilyDescriptor::new(CF_STATE, Options::default()),
ColumnFamilyDescriptor::new(CF_ACCOUNTS, Options::default()),
ColumnFamilyDescriptor::new(CF_TRANSACTIONS, Options::default()),
ColumnFamilyDescriptor::new(CF_METADATA, Options::default()),
ColumnFamilyDescriptor::new(CF_SNAPSHOTS, Options::default()),
ColumnFamilyDescriptor::new(CF_IDENTITIES, Options::default()),
ColumnFamilyDescriptor::new(CF_DELEGATIONS, Options::default()),
ColumnFamilyDescriptor::new(CF_CREDENTIALS, Options::default()),
ColumnFamilyDescriptor::new(CF_CHANNELS, Options::default()),
ColumnFamilyDescriptor::new(CF_AGENTS, Options::default()),
ColumnFamilyDescriptor::new(CF_MODELS, Options::default()),
ColumnFamilyDescriptor::new(CF_PROVIDERS, Options::default()),
ColumnFamilyDescriptor::new(CF_TASKS, Options::default()),
ColumnFamilyDescriptor::new(CF_AGENT_TEMPLATES, Options::default()),
ColumnFamilyDescriptor::new(CF_SKILLS, Options::default()),
ColumnFamilyDescriptor::new(CF_TOOLS, Options::default()),
ColumnFamilyDescriptor::new(CF_KNOWLEDGE, Options::default()),
ColumnFamilyDescriptor::new(CF_WORKFLOW_TEMPLATES, Options::default()),
ColumnFamilyDescriptor::new(CF_TOKENS, Options::default()),
ColumnFamilyDescriptor::new(CF_SETTLEMENTS, Options::default()),
ColumnFamilyDescriptor::new(CF_MODEL_SERVICES, Options::default()),
ColumnFamilyDescriptor::new(CF_NFTS, Options::default()),
ColumnFamilyDescriptor::new(CF_EVENTS, Options::default()),
ColumnFamilyDescriptor::new(CF_WEBHOOKS, Options::default()),
ColumnFamilyDescriptor::new(CF_COMPLIANCE, Options::default()),
ColumnFamilyDescriptor::new(CF_TRAINING_RUNS, Options::default()),
ColumnFamilyDescriptor::new(CF_TRAINING_RECEIPTS, Options::default()),
ColumnFamilyDescriptor::new(CF_AUDIT, Options::default()),
ColumnFamilyDescriptor::new(CF_APPROVALS, Options::default()),
ColumnFamilyDescriptor::new(CF_API_KEYS, Options::default()),
ColumnFamilyDescriptor::new(CF_MPC_KEYSHARES, Options::default()),
ColumnFamilyDescriptor::new(CF_CANTON_ANALYTICS, Options::default()),
ColumnFamilyDescriptor::new(CF_BRIDGE_ANALYTICS, Options::default()),
ColumnFamilyDescriptor::new(CF_VALIDATOR_MODULES, Options::default()),
]
}
pub fn open(config: &StorageConfig) -> Result<Self> {
let mut opts = Options::default();
opts.create_if_missing(true);
opts.create_missing_column_families(true);
opts.set_max_open_files(config.max_open_files);
opts.set_write_buffer_size(config.write_buffer_size);
opts.set_max_write_buffer_number(config.max_write_buffer_number);
opts.set_target_file_size_base(config.target_file_size_base);
if config.compression {
opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
}
if config.enable_statistics {
opts.enable_statistics();
}
let db = match DB::open_cf_descriptors(&opts, &config.db_path, Self::column_family_descriptors()) {
Ok(db) => db,
Err(e) => {
let error_str = e.to_string();
if error_str.contains("Corruption") || error_str.contains("corruption") ||
error_str.contains("log") || error_str.contains("WAL") {
tracing::warn!(
"Database corruption detected at {:?}, attempting repair: {}",
config.db_path,
error_str
);
if let Err(repair_err) = Self::repair_database(&config.db_path) {
tracing::error!("Database repair failed: {}", repair_err);
return Err(StorageError::DatabaseError(format!(
"Failed to open database and repair failed: {} (repair error: {})",
error_str,
repair_err
)));
}
tracing::info!("Database repair completed, reopening...");
DB::open_cf_descriptors(&opts, &config.db_path, Self::column_family_descriptors())
.map_err(|e| StorageError::DatabaseError(format!(
"Failed to open database after repair: {}",
e
)))?
} else {
return Err(e.into());
}
}
};
Ok(Self { db: Arc::new(db) })
}
pub fn repair_database<P: AsRef<Path>>(path: P) -> Result<()> {
tracing::warn!(
"Repairing database at {:?}. This may result in loss of recent uncommitted data.",
path.as_ref()
);
DB::repair(&Options::default(), path.as_ref())
.map_err(|e| StorageError::DatabaseError(format!("Repair failed: {}", e)))?;
tracing::info!("Database repair completed successfully");
Ok(())
}
pub fn open_default<P: AsRef<Path>>(path: P) -> Result<Self> {
let config = StorageConfig::new(path.as_ref().to_path_buf());
Self::open(&config)
}
fn cf_handle(&self, name: &str) -> Result<&ColumnFamily> {
self.db
.cf_handle(name)
.ok_or_else(|| StorageError::ColumnFamilyNotFound(name.to_string()))
}
pub fn db(&self) -> &DB {
&self.db
}
}
impl KvStore for RocksDbStore {
fn get(&self, cf: &str, key: &[u8]) -> Result<Option<Vec<u8>>> {
let cf_handle = self.cf_handle(cf)?;
Ok(self.db.get_cf(cf_handle, key)?)
}
fn put(&self, cf: &str, key: &[u8], value: &[u8]) -> Result<()> {
let cf_handle = self.cf_handle(cf)?;
Ok(self.db.put_cf(cf_handle, key, value)?)
}
fn delete(&self, cf: &str, key: &[u8]) -> Result<()> {
let cf_handle = self.cf_handle(cf)?;
Ok(self.db.delete_cf(cf_handle, key)?)
}
fn write_batch(&self, operations: Vec<WriteOp>) -> Result<()> {
let mut batch = WriteBatch::default();
for op in operations {
match op {
WriteOp::Put { cf, key, value } => {
let cf_handle = self.cf_handle(&cf)?;
batch.put_cf(cf_handle, key, value);
}
WriteOp::Delete { cf, key } => {
let cf_handle = self.cf_handle(&cf)?;
batch.delete_cf(cf_handle, key);
}
}
}
Ok(self.db.write(batch)?)
}
fn write_batch_sync(&self, operations: Vec<WriteOp>) -> Result<()> {
let mut batch = WriteBatch::default();
for op in operations {
match op {
WriteOp::Put { cf, key, value } => {
let cf_handle = self.cf_handle(&cf)?;
batch.put_cf(cf_handle, key, value);
}
WriteOp::Delete { cf, key } => {
let cf_handle = self.cf_handle(&cf)?;
batch.delete_cf(cf_handle, key);
}
}
}
let mut write_opts = WriteOptions::default();
write_opts.set_sync(true);
Ok(self.db.write_opt(batch, &write_opts)?)
}
fn get_keys_with_prefix(&self, cf: &str, prefix: &[u8]) -> Result<Vec<Vec<u8>>> {
let cf_handle = self.cf_handle(cf)?;
let mut keys = Vec::new();
let iter = self.db.prefix_iterator_cf(cf_handle, prefix);
for item in iter {
let (key, _) = item?;
if key.starts_with(prefix) {
keys.push(key.to_vec());
} else {
break;
}
}
Ok(keys)
}
fn scan_prefix(&self, cf: &str, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
let cf_handle = self.cf_handle(cf)?;
let mut results = Vec::new();
let iter = self.db.prefix_iterator_cf(cf_handle, prefix);
for item in iter {
let (key, value) = item?;
if key.starts_with(prefix) {
results.push((key.to_vec(), value.to_vec()));
} else {
break;
}
}
Ok(results)
}
fn scan_prefix_for_each(
&self,
cf: &str,
prefix: &[u8],
f: &mut dyn FnMut(&[u8], &[u8]) -> Result<()>,
) -> Result<()> {
let cf_handle = self.cf_handle(cf)?;
let iter = self.db.prefix_iterator_cf(cf_handle, prefix);
for item in iter {
let (key, value) = item?;
if !key.starts_with(prefix) {
break;
}
f(&key, &value)?;
}
Ok(())
}
}
pub struct MemoryStore {
data: Arc<RwLock<HashMap<String, HashMap<Vec<u8>, Vec<u8>>>>>,
}
impl MemoryStore {
pub fn new() -> Self {
let mut data = HashMap::new();
data.insert(CF_BLOCKS.to_string(), HashMap::new());
data.insert(CF_STATE.to_string(), HashMap::new());
data.insert(CF_ACCOUNTS.to_string(), HashMap::new());
data.insert(CF_TRANSACTIONS.to_string(), HashMap::new());
data.insert(CF_METADATA.to_string(), HashMap::new());
data.insert(CF_SNAPSHOTS.to_string(), HashMap::new());
data.insert(CF_IDENTITIES.to_string(), HashMap::new());
data.insert(CF_DELEGATIONS.to_string(), HashMap::new());
data.insert(CF_CREDENTIALS.to_string(), HashMap::new());
data.insert(CF_CHANNELS.to_string(), HashMap::new());
data.insert(CF_AGENTS.to_string(), HashMap::new());
data.insert(CF_MODELS.to_string(), HashMap::new());
data.insert(CF_PROVIDERS.to_string(), HashMap::new());
data.insert(CF_TASKS.to_string(), HashMap::new());
data.insert(CF_AGENT_TEMPLATES.to_string(), HashMap::new());
data.insert(CF_SKILLS.to_string(), HashMap::new());
data.insert(CF_TOOLS.to_string(), HashMap::new());
data.insert(CF_TOKENS.to_string(), HashMap::new());
data.insert(CF_SETTLEMENTS.to_string(), HashMap::new());
data.insert(CF_MODEL_SERVICES.to_string(), HashMap::new());
data.insert(CF_NFTS.to_string(), HashMap::new());
data.insert(CF_EVENTS.to_string(), HashMap::new());
data.insert(CF_WEBHOOKS.to_string(), HashMap::new());
data.insert(CF_COMPLIANCE.to_string(), HashMap::new());
data.insert(CF_TRAINING_RUNS.to_string(), HashMap::new());
data.insert(CF_TRAINING_RECEIPTS.to_string(), HashMap::new());
data.insert(CF_AUDIT.to_string(), HashMap::new());
data.insert(CF_APPROVALS.to_string(), HashMap::new());
Self {
data: Arc::new(RwLock::new(data)),
}
}
}
impl Default for MemoryStore {
fn default() -> Self {
Self::new()
}
}
impl KvStore for MemoryStore {
fn get(&self, cf: &str, key: &[u8]) -> Result<Option<Vec<u8>>> {
let data = self.data.read();
Ok(data
.get(cf)
.and_then(|cf_data| cf_data.get(key))
.cloned())
}
fn put(&self, cf: &str, key: &[u8], value: &[u8]) -> Result<()> {
let mut data = self.data.write();
data.entry(cf.to_string())
.or_default()
.insert(key.to_vec(), value.to_vec());
Ok(())
}
fn delete(&self, cf: &str, key: &[u8]) -> Result<()> {
let mut data = self.data.write();
if let Some(cf_data) = data.get_mut(cf) {
cf_data.remove(key);
}
Ok(())
}
fn write_batch(&self, operations: Vec<WriteOp>) -> Result<()> {
let mut data = self.data.write();
for op in operations {
match op {
WriteOp::Put { cf, key, value } => {
data.entry(cf)
.or_default()
.insert(key, value);
}
WriteOp::Delete { cf, key } => {
if let Some(cf_data) = data.get_mut(&cf) {
cf_data.remove(&key);
}
}
}
}
Ok(())
}
fn write_batch_sync(&self, operations: Vec<WriteOp>) -> Result<()> {
self.write_batch(operations)
}
fn get_keys_with_prefix(&self, cf: &str, prefix: &[u8]) -> Result<Vec<Vec<u8>>> {
let data = self.data.read();
let keys = data
.get(cf)
.map(|cf_data| {
cf_data
.keys()
.filter(|k| k.starts_with(prefix))
.cloned()
.collect()
})
.unwrap_or_default();
Ok(keys)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_store() {
let store = MemoryStore::new();
store.put(CF_STATE, b"key1", b"value1").unwrap();
let value = store.get(CF_STATE, b"key1").unwrap();
assert_eq!(value, Some(b"value1".to_vec()));
store.delete(CF_STATE, b"key1").unwrap();
let value = store.get(CF_STATE, b"key1").unwrap();
assert_eq!(value, None);
let ops = vec![
WriteOp::Put {
cf: CF_STATE.to_string(),
key: b"key2".to_vec(),
value: b"value2".to_vec(),
},
WriteOp::Put {
cf: CF_STATE.to_string(),
key: b"key3".to_vec(),
value: b"value3".to_vec(),
},
];
store.write_batch(ops).unwrap();
let value2 = store.get(CF_STATE, b"key2").unwrap();
let value3 = store.get(CF_STATE, b"key3").unwrap();
assert_eq!(value2, Some(b"value2".to_vec()));
assert_eq!(value3, Some(b"value3".to_vec()));
}
#[test]
fn test_prefix_search() {
let store = MemoryStore::new();
store.put(CF_STATE, b"prefix_key1", b"value1").unwrap();
store.put(CF_STATE, b"prefix_key2", b"value2").unwrap();
store.put(CF_STATE, b"other_key", b"value3").unwrap();
let keys = store.get_keys_with_prefix(CF_STATE, b"prefix_").unwrap();
assert_eq!(keys.len(), 2);
}
fn unique_temp_db_path(label: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("tenzro-storage-test-{}-{}-{}-{}", label, pid, id, nanos))
}
#[test]
fn test_rocksdb_write_batch_sync_durability() {
let path = unique_temp_db_path("sync");
{
let store = RocksDbStore::open_default(&path).expect("open db");
store
.write_batch_sync(vec![WriteOp::Put {
cf: CF_STATE.to_string(),
key: b"durable-key".to_vec(),
value: b"durable-value".to_vec(),
}])
.expect("sync write");
}
{
let store = RocksDbStore::open_default(&path).expect("reopen db");
let value = store.get(CF_STATE, b"durable-key").expect("read");
assert_eq!(value, Some(b"durable-value".to_vec()));
}
let _ = std::fs::remove_dir_all(&path);
}
#[test]
fn test_rocksdb_repair_recovers_from_truncated_wal() {
let path = unique_temp_db_path("repair");
{
let store = RocksDbStore::open_default(&path).expect("open db");
for i in 0..32u64 {
store
.write_batch_sync(vec![WriteOp::Put {
cf: CF_STATE.to_string(),
key: format!("key-{}", i).into_bytes(),
value: format!("value-{}", i).into_bytes(),
}])
.expect("sync write");
}
}
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) == Some("log")
&& let Ok(meta) = std::fs::metadata(&p)
{
let new_len = meta.len() / 2;
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&p) {
let _ = file.set_len(new_len);
}
}
}
}
let repair_result = RocksDbStore::repair_database(&path);
assert!(
repair_result.is_ok(),
"repair_database should succeed on truncated WAL: {:?}",
repair_result
);
let store = RocksDbStore::open_default(&path).expect("reopen after repair");
let _ = store.get(CF_STATE, b"key-0");
let _ = std::fs::remove_dir_all(&path);
}
#[test]
fn test_rocksdb_open_recovers_from_wal_corruption() {
let path = unique_temp_db_path("auto-repair");
{
let store = RocksDbStore::open_default(&path).expect("open db");
for i in 0..16u64 {
store
.write_batch_sync(vec![WriteOp::Put {
cf: CF_BLOCKS.to_string(),
key: format!("block-{}", i).into_bytes(),
value: format!("payload-{}", i).into_bytes(),
}])
.expect("sync write");
}
}
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) == Some("log")
&& let Ok(meta) = std::fs::metadata(&p)
{
let new_len = meta.len() / 3;
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&p) {
let _ = file.set_len(new_len);
}
}
}
}
let _ = RocksDbStore::open_default(&path);
let _ = std::fs::remove_dir_all(&path);
}
}