use std::{
collections::HashMap,
sync::Mutex,
time::{Duration, Instant},
};
use lexe_byte_array::ByteArray;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PutError {
Occupied,
TooLarge,
Full,
}
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
pub struct Address([u8; 32]);
lexe_byte_array::impl_byte_array!(Address, 32);
lexe_byte_array::impl_fromstr_fromhex!(Address, 32);
lexe_byte_array::impl_debug_display_as_hex!(Address);
lexe_serde::impl_serde_hexstr_or_bytes!(Address);
pub struct MailboxStore {
blobs: Mutex<HashMap<Address, StoredBlob>>,
max_blob_len: usize,
max_blobs: usize,
ttl: Duration,
}
struct StoredBlob {
blob: Vec<u8>,
expires_at: Instant,
}
impl MailboxStore {
pub const DEFAULT_TTL: Duration = Duration::from_secs(5 * 60);
pub const DEFAULT_MAX_BLOB_LEN: usize = 8192;
pub const DEFAULT_MAX_BLOBS: usize = 512;
pub fn new() -> Self {
Self {
blobs: Mutex::new(HashMap::new()),
max_blob_len: Self::DEFAULT_MAX_BLOB_LEN,
max_blobs: Self::DEFAULT_MAX_BLOBS,
ttl: Self::DEFAULT_TTL,
}
}
pub fn with_max_blob_len(mut self, max_blob_len: usize) -> Self {
self.max_blob_len = max_blob_len;
self
}
pub fn with_max_blobs(mut self, max_blobs: usize) -> Self {
self.max_blobs = max_blobs;
self
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
pub fn get(&self, address: &Address, now: Instant) -> Option<Vec<u8>> {
let mut locked_blobs = self.blobs.lock().unwrap();
let stored = locked_blobs.get(address)?;
if stored.expires_at <= now {
locked_blobs.remove(address);
return None;
}
Some(stored.blob.clone())
}
pub fn put(
&self,
address: Address,
blob: Vec<u8>,
now: Instant,
) -> Result<(), PutError> {
if blob.len() > self.max_blob_len {
return Err(PutError::TooLarge);
}
let mut locked_blobs = self.blobs.lock().unwrap();
if locked_blobs.len() >= self.max_blobs {
locked_blobs.retain(|_, stored| stored.expires_at > now);
if locked_blobs.len() >= self.max_blobs {
return Err(PutError::Full);
}
}
if let Some(stored) = locked_blobs.get(&address)
&& stored.expires_at > now
{
return if stored.blob == blob {
Ok(())
} else {
Err(PutError::Occupied)
};
}
locked_blobs.insert(
address,
StoredBlob {
blob,
expires_at: now + self.ttl,
},
);
Ok(())
}
}
#[cfg(test)]
mod test {
use lexe_byte_array::ByteArray;
use super::*;
#[test]
fn first_write_wins_retries_then_expires() {
let store = MailboxStore::new().with_ttl(Duration::from_secs(10));
let a = Address::from_array([1; 32]);
let t0 = Instant::now();
assert_eq!(store.get(&a, t0), None);
assert_eq!(store.put(a, vec![1], t0), Ok(()));
assert_eq!(store.put(a, vec![2], t0), Err(PutError::Occupied));
assert_eq!(store.get(&a, t0), Some(vec![1]));
let t_retry = t0 + Duration::from_secs(9);
assert_eq!(store.put(a, vec![1], t_retry), Ok(()));
assert_eq!(store.get(&a, t_retry), Some(vec![1]));
let t1 = t0 + Duration::from_secs(10);
assert_eq!(store.get(&a, t1), None);
assert_eq!(store.put(a, vec![2], t1), Ok(()));
assert_eq!(store.get(&a, t1), Some(vec![2]));
let too_large = vec![0; MailboxStore::DEFAULT_MAX_BLOB_LEN + 1];
let b = Address::from_array([2; 32]);
assert_eq!(store.put(b, too_large, t1), Err(PutError::TooLarge));
}
#[test]
fn full_store_sweeps_expired() {
const MAX_BLOBS: usize = 4;
let store = MailboxStore::new()
.with_ttl(Duration::from_secs(1))
.with_max_blobs(MAX_BLOBS);
let t0 = Instant::now();
for i in 0..MAX_BLOBS {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&(i as u64).to_le_bytes());
store.put(Address::from_array(bytes), vec![], t0).unwrap();
}
let a = Address::from_array([0xff; 32]);
assert_eq!(store.put(a, vec![], t0), Err(PutError::Full));
assert_eq!(store.put(a, vec![], t0 + Duration::from_secs(1)), Ok(()));
}
}