use crate::RecordId;
use crate::error::Result;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub trait BlobStore {
fn get(&self, id: RecordId) -> Result<Vec<u8>>;
fn put(&mut self, data: &[u8]) -> Result<RecordId>;
fn remove(&mut self, id: RecordId) -> Result<()>;
fn contains(&self, id: RecordId) -> bool;
fn size(&self, id: RecordId) -> Result<Option<usize>>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn stats(&self) -> BlobStoreStats {
BlobStoreStats::default()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BlobStoreStats {
pub blob_count: usize,
pub total_size: usize,
pub average_size: f64,
pub get_count: u64,
pub put_count: u64,
pub remove_count: u64,
pub cache_hit_ratio: f64,
}
impl BlobStoreStats {
pub fn new() -> Self {
Self::default()
}
pub fn record_get(&mut self, hit: bool) {
self.get_count += 1;
if hit {
self.cache_hit_ratio =
(self.cache_hit_ratio * (self.get_count - 1) as f64 + 1.0) / self.get_count as f64;
} else {
self.cache_hit_ratio =
(self.cache_hit_ratio * (self.get_count - 1) as f64) / self.get_count as f64;
}
}
pub fn record_put(&mut self, size: usize) {
self.put_count += 1;
self.blob_count += 1;
self.total_size += size;
self.average_size = self.total_size as f64 / self.blob_count as f64;
}
pub fn record_remove(&mut self, size: usize) {
self.remove_count += 1;
if self.blob_count > 0 {
self.blob_count -= 1;
self.total_size = self.total_size.saturating_sub(size);
self.average_size = if self.blob_count > 0 {
self.total_size as f64 / self.blob_count as f64
} else {
0.0
};
}
}
}
pub trait IterableBlobStore: BlobStore {
type IdIter: Iterator<Item = RecordId>;
fn iter_ids(&self) -> Self::IdIter;
fn iter_blobs(&self) -> BlobIterator<'_, Self> {
BlobIterator::new(self)
}
}
pub struct BlobIterator<'a, S: BlobStore + ?Sized> {
store: &'a S,
ids: Box<dyn Iterator<Item = RecordId> + 'a>,
}
impl<'a, S: IterableBlobStore + ?Sized> BlobIterator<'a, S> {
fn new(store: &'a S) -> Self {
Self {
store,
ids: Box::new(store.iter_ids()),
}
}
}
impl<'a, S: BlobStore + ?Sized> Iterator for BlobIterator<'a, S> {
type Item = Result<(RecordId, Vec<u8>)>;
fn next(&mut self) -> Option<Self::Item> {
self.ids
.next()
.map(|id| self.store.get(id).map(|data| (id, data)))
}
}
pub trait BatchBlobStore: BlobStore {
fn put_batch<I>(&mut self, blobs: I) -> Result<Vec<RecordId>>
where
I: IntoIterator<Item = Vec<u8>>;
fn get_batch<I>(&self, ids: I) -> Result<Vec<Option<Vec<u8>>>>
where
I: IntoIterator<Item = RecordId>;
fn remove_batch<I>(&mut self, ids: I) -> Result<usize>
where
I: IntoIterator<Item = RecordId>;
}
pub trait CompressedBlobStore: BlobStore {
fn compression_ratio(&self, id: RecordId) -> Result<Option<f32>>;
fn compressed_size(&self, id: RecordId) -> Result<Option<usize>>;
fn compression_stats(&self) -> CompressionStats;
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CompressionStats {
pub uncompressed_size: usize,
pub compressed_size: usize,
pub compression_ratio: f32,
pub compressed_count: usize,
}
impl CompressionStats {
pub fn ratio(&self) -> f32 {
if self.uncompressed_size > 0 {
self.compressed_size as f32 / self.uncompressed_size as f32
} else {
1.0
}
}
pub fn space_saved_percent(&self) -> f32 {
(1.0 - self.ratio()) * 100.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blob_store_stats() {
let mut stats = BlobStoreStats::new();
stats.record_put(100);
assert_eq!(stats.blob_count, 1);
assert_eq!(stats.total_size, 100);
assert_eq!(stats.average_size, 100.0);
assert_eq!(stats.put_count, 1);
stats.record_put(200);
assert_eq!(stats.blob_count, 2);
assert_eq!(stats.total_size, 300);
assert_eq!(stats.average_size, 150.0);
stats.record_remove(100);
assert_eq!(stats.blob_count, 1);
assert_eq!(stats.total_size, 200);
assert_eq!(stats.average_size, 200.0);
assert_eq!(stats.remove_count, 1);
stats.record_get(true); stats.record_get(false); stats.record_get(true); assert_eq!(stats.get_count, 3);
assert!((stats.cache_hit_ratio - 2.0 / 3.0).abs() < 0.001);
}
#[test]
fn test_compression_stats() {
let stats = CompressionStats {
uncompressed_size: 1000,
compressed_size: 300,
compression_ratio: 0.3,
compressed_count: 10,
};
assert_eq!(stats.ratio(), 0.3);
assert_eq!(stats.space_saved_percent(), 70.0);
let empty_stats = CompressionStats::default();
assert_eq!(empty_stats.ratio(), 1.0);
assert_eq!(empty_stats.space_saved_percent(), 0.0);
}
}