use std::{path::Path, sync::Arc};
use rocksdb::{Cache, ColumnFamilyDescriptor, OptimisticTransactionDB, Options};
use crate::{
store::{
rocks::{
cf_options, snapshot::Snapshot, transaction::Transaction, CF_EDGES_IN, CF_EDGES_OUT, CF_SCHEMA,
CF_VERTEX_DEGREE, CF_VERTICES,
},
traits::GraphStore,
},
types::StoreError,
};
#[derive(Debug, Clone)]
pub struct RocksOptions {
pub block_cache_size: usize,
pub write_buffer_size: usize,
pub max_write_buffer_number: i32,
pub max_background_jobs: i32,
pub vertex_block_size: usize,
pub edge_block_size: usize,
pub cache_index_and_filter_blocks: bool,
}
impl Default for RocksOptions {
fn default() -> Self {
Self {
block_cache_size: 1024 * 1024 * 1024,
write_buffer_size: 128 * 1024 * 1024,
max_write_buffer_number: 3,
max_background_jobs: 4,
vertex_block_size: 4 * 1024,
edge_block_size: 16 * 1024,
cache_index_and_filter_blocks: true,
}
}
}
pub struct RocksStorage {
pub(crate) db: Arc<OptimisticTransactionDB>,
#[cfg(feature = "rocksdb-stats")]
opts: std::sync::Mutex<Options>,
}
impl RocksStorage {
pub fn open(path: impl AsRef<Path>, rocksdb_opts: &RocksOptions) -> Result<Self, StoreError> {
let mut opts = Options::default();
opts.create_if_missing(true);
opts.create_missing_column_families(true);
opts.set_max_background_jobs(rocksdb_opts.max_background_jobs);
#[cfg(feature = "rocksdb-stats")]
opts.enable_statistics();
let block_cache = Cache::new_lru_cache(rocksdb_opts.block_cache_size);
let mut edge_block_opts = cf_options::edge_block_opts(rocksdb_opts);
edge_block_opts.set_block_cache(&block_cache);
let edge_cf_opts = cf_options::edge_cf_opts(rocksdb_opts, &edge_block_opts);
let mut vertex_block_opts = cf_options::vertex_block_opts(rocksdb_opts);
vertex_block_opts.set_block_cache(&block_cache);
let vertex_cf_opts = cf_options::vertex_cf_opts(rocksdb_opts, &vertex_block_opts);
let cfs = vec![
ColumnFamilyDescriptor::new(CF_VERTICES, vertex_cf_opts.clone()),
ColumnFamilyDescriptor::new(CF_VERTEX_DEGREE, vertex_cf_opts),
ColumnFamilyDescriptor::new(CF_EDGES_OUT, edge_cf_opts.clone()),
ColumnFamilyDescriptor::new(CF_EDGES_IN, edge_cf_opts),
ColumnFamilyDescriptor::new(CF_SCHEMA, Options::default()),
];
let db = OptimisticTransactionDB::open_cf_descriptors(&opts, path, cfs).map_err(StoreError::RocksDb)?;
Ok(Self {
db: Arc::new(db),
#[cfg(feature = "rocksdb-stats")]
opts: std::sync::Mutex::new(opts),
})
}
pub(crate) fn recover_bulk_load_crash(&self) -> Result<(), StoreError> {
let cf_s = self.db.cf_handle(CF_SCHEMA);
if cf_s.is_none() {
return Ok(());
}
let cf_s = cf_s.unwrap();
if self.db.get_cf(&cf_s, super::bulk_loader::BULK_LOAD_IN_PROGRESS_KEY).map_err(StoreError::RocksDb)?.is_none()
{
return Ok(());
}
let cf_v = self.db.cf_handle(CF_VERTICES);
if let Some(ref cf_vtx) = cf_v {
if self.db.iterator_cf(cf_vtx, rocksdb::IteratorMode::Start).next().is_some() {
let mut cleanup = rocksdb::WriteBatchWithTransaction::<true>::default();
cleanup.delete_cf(&cf_s, super::bulk_loader::BULK_LOAD_IN_PROGRESS_KEY);
self.db.write(cleanup).map_err(StoreError::RocksDb)?;
return Ok(());
}
}
Err(StoreError::IncompleteLoad { msg: "bulk load interrupted before ingest — retry load_initial".into() })
}
pub fn load_schema(
&self,
defaults: crate::schema::definition::GraphOptions,
) -> Result<crate::schema::Schema, StoreError> {
use super::CF_SCHEMA;
use crate::{
schema::definition::{DataType, EdgeMode, PropKeyConfig, Schema, SchemaMode},
types::kv_codec::{
decode_schema_label_value, decode_schema_meta, decode_schema_prop_value, encode_schema_meta,
SCHEMA_KIND_EDGE_LABEL, SCHEMA_KIND_META, SCHEMA_KIND_PROP_KEY, SCHEMA_KIND_VERTEX_LABEL,
SCHEMA_META_KEY,
},
};
use rocksdb::IteratorMode;
let cf = self.db.cf_handle(CF_SCHEMA).ok_or(StoreError::MissingColumnFamily(CF_SCHEMA))?;
let mut schema = Schema::new();
if let Some(meta_bytes) = self.db.get_cf(&cf, SCHEMA_META_KEY).map_err(StoreError::RocksDb)? {
let (version, edge_mode_u8, schema_mode_u8) =
decode_schema_meta(&meta_bytes).ok_or(StoreError::CorruptData("invalid schema metadata"))?;
schema.version = version;
schema.edge_mode = EdgeMode::from_u8(edge_mode_u8).ok_or(StoreError::CorruptData("invalid edge mode"))?;
schema.mode = SchemaMode::from_u8(schema_mode_u8).ok_or(StoreError::CorruptData("invalid schema mode"))?;
} else {
schema.version = 0;
schema.edge_mode = defaults.edge_mode;
schema.mode = defaults.mode;
let meta_bytes = encode_schema_meta(schema.version, schema.edge_mode.to_u8(), schema.mode.to_u8());
self.db.put_cf(&cf, SCHEMA_META_KEY, meta_bytes).map_err(StoreError::RocksDb)?;
}
let iter = self.db.iterator_cf(&cf, IteratorMode::Start);
for item in iter {
let (k, v) = item.map_err(StoreError::RocksDb)?;
if k.is_empty() {
continue;
}
let kind = k[0];
if kind == SCHEMA_KIND_META {
continue;
}
let name_bytes = &k[1..];
let name_str =
std::str::from_utf8(name_bytes).map_err(|_| StoreError::CorruptData("invalid schema name encoding"))?;
match kind {
SCHEMA_KIND_VERTEX_LABEL => {
let id =
decode_schema_label_value(&v).ok_or(StoreError::CorruptData("invalid vertex label value"))?;
schema.vertex_labels.insert(id, smol_str::SmolStr::new(name_str));
schema.persisted_vertex_labels.insert(id);
}
SCHEMA_KIND_EDGE_LABEL => {
let id =
decode_schema_label_value(&v).ok_or(StoreError::CorruptData("invalid edge label value"))?;
schema.edge_labels.insert(id, smol_str::SmolStr::new(name_str));
schema.persisted_edge_labels.insert(id);
}
SCHEMA_KIND_PROP_KEY => {
let (id, data_type_u8) =
decode_schema_prop_value(&v).ok_or(StoreError::CorruptData("invalid prop key value"))?;
let data_type = DataType::from_u8(data_type_u8)
.ok_or(StoreError::CorruptData("invalid data type discriminant"))?;
schema.prop_keys.insert(id, smol_str::SmolStr::new(name_str));
schema.prop_key_types.insert(id, PropKeyConfig { data_type });
schema.persisted_prop_keys.insert(id);
}
_ => {}
}
}
Ok(schema)
}
}
#[cfg(feature = "rocksdb-stats")]
impl RocksStorage {
pub fn statistics(&self) -> Option<String> {
use rocksdb::statistics::Ticker;
let opts = self.opts.lock().unwrap();
let hit_b = opts.get_ticker_count(Ticker::BlockCacheDataHit);
let miss_b = opts.get_ticker_count(Ticker::BlockCacheDataMiss);
let hit_i = opts.get_ticker_count(Ticker::BlockCacheIndexHit);
let miss_i = opts.get_ticker_count(Ticker::BlockCacheIndexMiss);
let hit_f = opts.get_ticker_count(Ticker::BlockCacheFilterHit);
let miss_f = opts.get_ticker_count(Ticker::BlockCacheFilterMiss);
let cache_bytes_read = opts.get_ticker_count(Ticker::BlockCacheBytesRead);
let pct = |hit: u64, miss: u64| -> String {
let total = hit + miss;
if total == 0 {
"n/a".into()
} else {
format!("{:.1}%", 100.0 * hit as f64 / total as f64)
}
};
let tickers = format!(
"--- Bloom Filter (SST file, aggregated across all CFs) ---\n\
bloom.filter.useful : {}\n\
bloom.filter.full.positive : {}\n\
bloom.filter.full.true.positive : {}\n\
bloom.filter.prefix.checked : {} (memtable only; 0 when data is in SSTs)\n\
bloom.filter.prefix.useful : {}\n\
bloom.filter.prefix.true.positive : {}\n\
\n\
--- Block Cache Hit Rates (aggregated across all CFs) ---\n\
data blocks: hit={hit_b:>10} miss={miss_b:>10} hit_rate={}\n\
index blocks: hit={hit_i:>10} miss={miss_i:>10} hit_rate={}\n\
filter blocks:hit={hit_f:>10} miss={miss_f:>10} hit_rate={}\n\
cache_bytes_read: {} MB",
opts.get_ticker_count(Ticker::BloomFilterUseful),
opts.get_ticker_count(Ticker::BloomFilterFullPositive),
opts.get_ticker_count(Ticker::BloomFilterFullTruePositive),
opts.get_ticker_count(Ticker::BloomFilterPrefixChecked),
opts.get_ticker_count(Ticker::BloomFilterPrefixUseful),
opts.get_ticker_count(Ticker::BloomFilterPrefixTruePositive),
pct(hit_b, miss_b),
pct(hit_i, miss_i),
pct(hit_f, miss_f),
cache_bytes_read / (1024 * 1024),
);
drop(opts);
let cf_stats: String = [CF_VERTICES, CF_VERTEX_DEGREE, CF_EDGES_OUT, CF_EDGES_IN]
.iter()
.filter_map(|cf_name| {
let cf = self.db.cf_handle(cf_name)?;
let stats = self.db.property_value_cf(&cf, "rocksdb.cfstats").ok().flatten()?;
Some(format!("\n=== CF: {cf_name} ===\n{stats}"))
})
.collect();
Some(format!("{tickers}\n{cf_stats}"))
}
}
impl GraphStore for RocksStorage {
type Snapshot = Snapshot;
type Txn = Transaction;
fn snapshot(&self) -> Snapshot {
Snapshot::new(Arc::clone(&self.db))
}
fn begin(&self) -> Transaction {
Transaction::new(Arc::clone(&self.db))
}
}