use std::sync::atomic::{AtomicU64, Ordering};
use crate::{Error, ExternalBufferSerde};
use super::ExternalBuffer;
pub struct ExternalBufferSled {
db: sled::Db,
head_counter: AtomicU64,
tail_counter: AtomicU64,
}
impl ExternalBufferSled {
pub fn new<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
let db = sled::open(path)?;
let (head, tail) = Self::initialize_counters(&db)?;
Ok(Self {
db,
head_counter: AtomicU64::new(head),
tail_counter: AtomicU64::new(tail),
})
}
fn initialize_counters(db: &sled::Db) -> Result<(u64, u64), Error> {
let mut min_key = u64::MAX;
let mut max_key = 0u64;
let mut has_keys = false;
for result in db.iter() {
let (key, _) = result?;
if key.len() == 8 {
let key_u64 = u64::from_be_bytes(
key.as_ref()
.try_into()
.map_err(|_| Error::InvalidSledKeyFormat)?,
);
min_key = min_key.min(key_u64);
max_key = max_key.max(key_u64);
has_keys = true;
}
}
if has_keys {
Ok((min_key, max_key + 1))
} else {
Ok((0, 0))
}
}
fn key_from_u64(value: u64) -> [u8; 8] {
value.to_be_bytes()
}
}
#[async_trait::async_trait]
impl<T: ExternalBufferSerde + Send + 'static> ExternalBuffer<T> for ExternalBufferSled {
async fn push(&self, item: T) -> Result<(), Error> {
let serialized = item.into_external_buffer()?;
let key = self.tail_counter.fetch_add(1, Ordering::SeqCst);
let key_bytes = Self::key_from_u64(key);
self.db.insert(&key_bytes, serialized)?;
Ok(())
}
async fn shift(&self) -> Result<Option<T>, Error> {
loop {
let current_head = self.head_counter.load(Ordering::SeqCst);
let current_tail = self.tail_counter.load(Ordering::SeqCst);
if current_head >= current_tail {
return Ok(None);
}
let key_bytes = Self::key_from_u64(current_head);
match self.db.remove(&key_bytes)? {
Some(data) => {
self.head_counter.fetch_add(1, Ordering::SeqCst);
let item = T::from_external_buffer(&data)?;
return Ok(Some(item));
}
None => {
self.head_counter.fetch_add(1, Ordering::SeqCst);
continue;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bincode::{Decode, Encode};
use tempfile::TempDir;
#[derive(Debug, Clone, PartialEq, Encode, Decode)]
struct TestItem {
id: u32,
name: String,
}
#[tokio::test]
async fn test_push_and_shift() {
let temp_dir = TempDir::new().unwrap();
let buffer = ExternalBufferSled::new(temp_dir.path().join("test_db")).unwrap();
let item1 = TestItem {
id: 1,
name: "first".to_string(),
};
let item2 = TestItem {
id: 2,
name: "second".to_string(),
};
buffer.push(item1.clone()).await.unwrap();
buffer.push(item2.clone()).await.unwrap();
let shifted1 = buffer.shift().await.unwrap();
assert_eq!(shifted1, Some(item1));
let shifted2 = buffer.shift().await.unwrap();
assert_eq!(shifted2, Some(item2));
let shifted3: Option<TestItem> = buffer.shift().await.unwrap();
assert_eq!(shifted3, None);
}
#[tokio::test]
async fn test_empty_buffer() {
let temp_dir = TempDir::new().unwrap();
let buffer = ExternalBufferSled::new(temp_dir.path().join("empty_db")).unwrap();
let result: Option<TestItem> = buffer.shift().await.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_persistence() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("persistent_db");
let item = TestItem {
id: 42,
name: "persistent".to_string(),
};
{
let buffer = ExternalBufferSled::new(&db_path).unwrap();
buffer.push(item.clone()).await.unwrap();
}
{
let buffer = ExternalBufferSled::new(&db_path).unwrap();
let retrieved = buffer.shift().await.unwrap();
assert_eq!(retrieved, Some(item));
}
}
#[tokio::test]
async fn test_multiple_pushes_and_shifts() {
let temp_dir = TempDir::new().unwrap();
let buffer = ExternalBufferSled::new(temp_dir.path().join("multi_db")).unwrap();
let items: Vec<TestItem> = (0..10)
.map(|i| TestItem {
id: i,
name: format!("item_{}", i),
})
.collect();
for item in &items {
buffer.push(item.clone()).await.unwrap();
}
for expected_item in &items {
let shifted = buffer.shift().await.unwrap();
assert_eq!(shifted, Some(expected_item.clone()));
}
let result: Option<TestItem> = buffer.shift().await.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_interleaved_push_and_shift() {
let temp_dir = TempDir::new().unwrap();
let buffer = ExternalBufferSled::new(temp_dir.path().join("interleaved_db")).unwrap();
let item1 = TestItem {
id: 1,
name: "first".to_string(),
};
let item2 = TestItem {
id: 2,
name: "second".to_string(),
};
let item3 = TestItem {
id: 3,
name: "third".to_string(),
};
buffer.push(item1.clone()).await.unwrap();
let shifted1 = buffer.shift().await.unwrap();
assert_eq!(shifted1, Some(item1));
buffer.push(item2.clone()).await.unwrap();
buffer.push(item3.clone()).await.unwrap();
let shifted2 = buffer.shift().await.unwrap();
assert_eq!(shifted2, Some(item2));
let shifted3 = buffer.shift().await.unwrap();
assert_eq!(shifted3, Some(item3));
let result: Option<TestItem> = buffer.shift().await.unwrap();
assert_eq!(result, None);
}
}