use anyhow::Result;
use parking_lot::{Mutex, MutexGuard};
use rocksdb::{
BlockBasedOptions, Cache, DB, DBCompressionType, DBIteratorWithThreadMode, Direction,
IteratorMode, Options, ReadOptions,
};
use std::path::Path;
use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
use super::RealRocksDBStorage;
const BLOCK_CACHE_BYTES: usize = 64 * 1024 * 1024;
const SESSION_LOCK_STRIPES: usize = 64;
pub(crate) struct SessionLocks {
stripes: Vec<Mutex<()>>,
}
impl SessionLocks {
fn new() -> Self {
Self {
stripes: (0..SESSION_LOCK_STRIPES).map(|_| Mutex::new(())).collect(),
}
}
pub(crate) fn lock(&self, session_id: Uuid) -> MutexGuard<'_, ()> {
let stripe = (session_id.as_u128() % SESSION_LOCK_STRIPES as u128) as usize;
self.stripes[stripe].lock()
}
}
pub(super) fn prefix_upper_bound(prefix: &[u8]) -> Option<Vec<u8>> {
let mut bound = prefix.to_vec();
while let Some(last) = bound.pop() {
if last < u8::MAX {
bound.push(last + 1);
return Some(bound);
}
}
None
}
pub(super) fn prefix_read_opts(prefix: &[u8]) -> ReadOptions {
let mut opts = ReadOptions::default();
opts.set_total_order_seek(true);
if let Some(upper) = prefix_upper_bound(prefix) {
opts.set_iterate_upper_bound(upper);
}
opts
}
pub(super) fn scan_prefix<'a>(db: &'a DB, prefix: &[u8]) -> DBIteratorWithThreadMode<'a, DB> {
db.iterator_opt(
IteratorMode::From(prefix, Direction::Forward),
prefix_read_opts(prefix),
)
}
pub(super) fn keys_with_prefix(db: &DB, prefix: &[u8]) -> Result<Vec<Box<[u8]>>> {
let mut keys = Vec::new();
for item in scan_prefix(db, prefix) {
let (key, _) = item?;
keys.push(key);
}
Ok(keys)
}
pub(super) async fn blocking<T, F>(f: F) -> Result<T>
where
F: FnOnce() -> Result<T> + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(f)
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
impl RealRocksDBStorage {
pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::open(path.as_ref(), None).await
}
pub async fn new_with_dimension<P: AsRef<Path>>(
path: P,
vector_dimension: usize,
) -> Result<Self> {
let dimension = (vector_dimension > 0).then_some(vector_dimension);
Self::open(path.as_ref(), dimension).await
}
async fn open(path: &Path, vector_dimension: Option<usize>) -> Result<Self> {
let data_dir = path.to_path_buf();
if !data_dir.exists() {
std::fs::create_dir_all(&data_dir)?;
}
let db_path = data_dir.join("rocksdb");
let mut opts = Options::default();
opts.create_if_missing(true);
opts.create_missing_column_families(true);
opts.set_max_open_files(1000); opts.set_write_buffer_size(64 * 1024 * 1024); opts.set_max_write_buffer_number(6); opts.set_min_write_buffer_number_to_merge(2); opts.set_target_file_size_base(64 * 1024 * 1024);
opts.set_level_zero_slowdown_writes_trigger(20); opts.set_level_zero_stop_writes_trigger(36);
opts.set_use_fsync(false); opts.set_bytes_per_sync(1024 * 1024);
opts.set_compaction_style(rocksdb::DBCompactionStyle::Universal);
opts.set_bottommost_compression_type(DBCompressionType::Zstd);
opts.set_prefix_extractor(rocksdb::SliceTransform::create_fixed_prefix(16));
let mut table_opts = BlockBasedOptions::default();
table_opts.set_bloom_filter(10.0, false);
table_opts.set_whole_key_filtering(true);
let cache = Cache::new_lru_cache(BLOCK_CACHE_BYTES);
table_opts.set_block_cache(&cache);
table_opts.set_cache_index_and_filter_blocks(true);
table_opts.set_pin_l0_filter_and_index_blocks_in_cache(true);
opts.set_block_based_table_factory(&table_opts);
let db = DB::open(&opts, &db_path)?;
let db = Arc::new(db);
info!(
"RealRocksDBStorage: Initialized RocksDB at {}",
db_path.display()
);
let storage = Self {
db,
data_dir,
vector_dimension,
session_locks: Arc::new(SessionLocks::new()),
};
storage.ensure_source_file_index().await?;
Ok(storage)
}
pub async fn get_stats(&self) -> Result<String> {
let db = self.db.clone();
tokio::task::spawn_blocking(move || -> Result<String> {
let stats = db
.property_value(rocksdb::properties::STATS)?
.unwrap_or_else(|| "No stats available".to_string());
Ok(stats)
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn compact(&self) -> Result<()> {
let db = self.db.clone();
tokio::task::spawn_blocking(move || -> Result<()> {
db.compact_range(None::<&[u8]>, None::<&[u8]>);
info!("RealRocksDBStorage: Database compacted");
Ok(())
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn get_key_count(&self) -> Result<usize> {
let db = self.db.clone();
tokio::task::spawn_blocking(move || -> Result<usize> {
if let Some(count_str) = db.property_value(rocksdb::properties::ESTIMATE_NUM_KEYS)?
&& let Ok(count) = count_str.parse::<usize>()
{
return Ok(count);
}
let mut count = 0;
let iter = db.iterator(rocksdb::IteratorMode::Start);
for item in iter {
let _ = item?;
count += 1;
}
Ok(count)
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
}
#[cfg(test)]
mod tests {
use super::prefix_upper_bound;
#[test]
fn upper_bound_increments_last_byte() {
assert_eq!(prefix_upper_bound(b"session:"), Some(b"session;".to_vec()));
assert_eq!(prefix_upper_bound(b"a\xff"), Some(b"b".to_vec()));
assert_eq!(prefix_upper_bound(b"\xff\xff"), None);
assert_eq!(prefix_upper_bound(b""), None);
}
}