use std::io;
use parking_lot::Mutex;
pub trait SnapshotStore: Send + Sync + 'static {
fn save(&self, payload: &[u8]) -> io::Result<()>;
fn load(&self) -> io::Result<Option<Vec<u8>>>;
}
#[derive(Default)]
pub struct InMemorySnapshotStore {
bytes: Mutex<Option<Vec<u8>>>,
}
impl InMemorySnapshotStore {
pub fn new() -> Self {
Self::default()
}
}
impl SnapshotStore for InMemorySnapshotStore {
fn save(&self, payload: &[u8]) -> io::Result<()> {
*self.bytes.lock() = Some(payload.to_vec());
Ok(())
}
fn load(&self) -> io::Result<Option<Vec<u8>>> {
Ok(self.bytes.lock().clone())
}
}
#[cfg(feature = "rocksdb-snapshot-store")]
mod rocksdb_backed {
use std::io;
use std::sync::Arc;
use rocksdb::{DB, WriteOptions};
use super::SnapshotStore;
pub struct RocksdbSnapshotStore {
db: Arc<DB>,
cf: String,
key: Vec<u8>,
}
impl std::fmt::Debug for RocksdbSnapshotStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RocksdbSnapshotStore")
.field("cf", &self.cf)
.field("key_len", &self.key.len())
.finish()
}
}
impl RocksdbSnapshotStore {
pub fn open(db: Arc<DB>, cf: impl Into<String>) -> io::Result<Self> {
Self::open_with_key(db, cf, b"snapshot".to_vec())
}
pub fn open_with_key(db: Arc<DB>, cf: impl Into<String>, key: Vec<u8>) -> io::Result<Self> {
let cf = cf.into();
if db.cf_handle(&cf).is_none() {
return Err(io::Error::other(format!(
"snapshot column family `{cf}` not found on db"
)));
}
Ok(Self { db, cf, key })
}
fn cf_handle(&self) -> io::Result<Arc<rocksdb::BoundColumnFamily<'_>>> {
self.db.cf_handle(&self.cf).ok_or_else(|| {
io::Error::other(format!(
"snapshot column family `{}` disappeared from db",
self.cf
))
})
}
}
impl SnapshotStore for RocksdbSnapshotStore {
fn save(&self, payload: &[u8]) -> io::Result<()> {
let cf = self.cf_handle()?;
let mut wo = WriteOptions::default();
wo.set_sync(true);
self.db
.put_cf_opt(&cf, &self.key, payload, &wo)
.map_err(io::Error::other)
}
fn load(&self) -> io::Result<Option<Vec<u8>>> {
let cf = self.cf_handle()?;
self.db.get_cf(&cf, &self.key).map_err(io::Error::other)
}
}
}
#[cfg(feature = "rocksdb-snapshot-store")]
pub use rocksdb_backed::RocksdbSnapshotStore;
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn in_memory_load_is_none_before_save() {
let store = InMemorySnapshotStore::new();
assert!(store.load().unwrap().is_none());
}
#[test]
fn in_memory_save_then_load_round_trips() {
let store = InMemorySnapshotStore::new();
store.save(b"first").unwrap();
assert_eq!(store.load().unwrap().as_deref(), Some(&b"first"[..]));
}
#[test]
fn in_memory_save_overwrites_prior_value() {
let store = InMemorySnapshotStore::new();
store.save(b"first").unwrap();
store.save(b"second").unwrap();
assert_eq!(store.load().unwrap().as_deref(), Some(&b"second"[..]));
}
#[test]
fn in_memory_store_is_shareable_via_arc() {
let store: Arc<dyn SnapshotStore> = Arc::new(InMemorySnapshotStore::new());
let a = Arc::clone(&store);
let b = Arc::clone(&store);
a.save(b"shared").unwrap();
assert_eq!(b.load().unwrap().as_deref(), Some(&b"shared"[..]));
}
}
#[cfg(all(test, feature = "rocksdb-snapshot-store"))]
mod rocksdb_tests {
use super::*;
use std::sync::Arc;
use rocksdb::{ColumnFamilyDescriptor, DB, Options};
use tempfile::TempDir;
const SNAP_CF: &str = "raft_snapshot";
fn open_db(path: &std::path::Path) -> Arc<DB> {
let mut opts = Options::default();
opts.create_if_missing(true);
opts.create_missing_column_families(true);
let cfs = vec![ColumnFamilyDescriptor::new(SNAP_CF, Options::default())];
Arc::new(DB::open_cf_descriptors(&opts, path, cfs).unwrap())
}
#[test]
fn rocksdb_save_then_load_in_same_open() {
let dir = TempDir::new().unwrap();
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open(db, SNAP_CF).unwrap();
store.save(b"hello").unwrap();
assert_eq!(store.load().unwrap().as_deref(), Some(&b"hello"[..]));
}
#[test]
fn rocksdb_load_is_none_before_save() {
let dir = TempDir::new().unwrap();
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open(db, SNAP_CF).unwrap();
assert!(store.load().unwrap().is_none());
}
#[test]
fn rocksdb_save_overwrites_prior_value() {
let dir = TempDir::new().unwrap();
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open(db, SNAP_CF).unwrap();
store.save(b"v1").unwrap();
store.save(b"v2").unwrap();
assert_eq!(store.load().unwrap().as_deref(), Some(&b"v2"[..]));
}
#[test]
fn rocksdb_save_survives_reopen() {
let dir = TempDir::new().unwrap();
{
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open(db, SNAP_CF).unwrap();
store.save(b"durable").unwrap();
}
{
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open(db, SNAP_CF).unwrap();
assert_eq!(store.load().unwrap().as_deref(), Some(&b"durable"[..]));
}
}
#[test]
fn rocksdb_open_errors_when_cf_missing() {
let dir = TempDir::new().unwrap();
let mut opts = Options::default();
opts.create_if_missing(true);
let db = Arc::new(DB::open(&opts, dir.path()).unwrap());
let err = RocksdbSnapshotStore::open(db, "missing_cf").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("missing_cf"));
}
#[test]
fn rocksdb_load_errors_when_cf_dropped_after_open() {
let dir = TempDir::new().unwrap();
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open(db.clone(), SNAP_CF).unwrap();
store.save(b"prior").unwrap();
db.drop_cf(SNAP_CF).unwrap();
let err = store.load().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("disappeared"));
assert!(msg.contains(SNAP_CF));
}
#[test]
fn rocksdb_debug_includes_cf_and_key_len() {
let dir = TempDir::new().unwrap();
let db = open_db(dir.path());
let store = RocksdbSnapshotStore::open_with_key(db, SNAP_CF, b"some-key".to_vec()).unwrap();
let rendered = format!("{store:?}");
assert!(rendered.contains(SNAP_CF));
assert!(rendered.contains("key_len: 8"));
}
#[test]
fn rocksdb_distinct_keys_do_not_collide() {
let dir = TempDir::new().unwrap();
let db = open_db(dir.path());
let store_a =
RocksdbSnapshotStore::open_with_key(db.clone(), SNAP_CF, b"a".to_vec()).unwrap();
let store_b = RocksdbSnapshotStore::open_with_key(db, SNAP_CF, b"b".to_vec()).unwrap();
store_a.save(b"snap-a").unwrap();
store_b.save(b"snap-b").unwrap();
assert_eq!(store_a.load().unwrap().as_deref(), Some(&b"snap-a"[..]));
assert_eq!(store_b.load().unwrap().as_deref(), Some(&b"snap-b"[..]));
}
}