use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use dashmap::DashMap;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::entry::Entry;
use crate::error::{Error, Result};
use crate::tags::{Tag, collect_tags};
use crate::time::{Clock, Timestamp};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributedEntry<V> {
pub value: V,
pub created_ticks: i64,
pub logical_expiration_ticks: i64,
pub physical_expiration_ticks: i64,
pub is_from_fail_safe: bool,
pub etag: Option<String>,
pub last_modified_ticks: Option<i64>,
pub tags: Vec<String>,
}
impl<V: Clone> DistributedEntry<V> {
#[must_use]
pub fn from_entry(entry: &Entry<V>) -> Self {
let meta = entry.meta();
Self {
value: entry.value_cloned(),
created_ticks: meta.created().ticks(),
logical_expiration_ticks: meta.logical_expiration().ticks(),
physical_expiration_ticks: meta.physical_expiration().ticks(),
is_from_fail_safe: meta.is_from_fail_safe(),
etag: meta.etag().map(str::to_owned),
last_modified_ticks: meta.last_modified().map(Timestamp::ticks),
tags: meta.tags().iter().map(|t| t.as_str().to_owned()).collect(),
}
}
#[must_use]
pub fn into_entry(self, now: Timestamp) -> Entry<V> {
let tags: Box<[Tag]> = collect_tags(self.tags);
Entry::rehydrate(
self.value,
Timestamp::from_ticks(self.created_ticks),
Timestamp::from_ticks(self.logical_expiration_ticks),
Timestamp::from_ticks(self.physical_expiration_ticks),
self.is_from_fail_safe,
self.etag,
self.last_modified_ticks.map(Timestamp::from_ticks),
tags,
now,
)
}
}
pub trait DistributedSerializer<V>: Send + Sync {
fn serialize(&self, entry: &DistributedEntry<V>) -> Result<Vec<u8>>;
fn deserialize(&self, bytes: &[u8]) -> Result<DistributedEntry<V>>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct JsonSerializer;
impl<V> DistributedSerializer<V> for JsonSerializer
where
V: Serialize + DeserializeOwned,
{
fn serialize(&self, entry: &DistributedEntry<V>) -> Result<Vec<u8>> {
serde_json::to_vec(entry).map_err(|e| Error::Serialization(e.to_string()))
}
fn deserialize(&self, bytes: &[u8]) -> Result<DistributedEntry<V>> {
serde_json::from_slice(bytes).map_err(|e| Error::Deserialization(e.to_string()))
}
}
#[async_trait]
pub trait DistributedCache: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<()>;
async fn remove(&self, key: &str) -> Result<()>;
}
#[derive(Clone)]
pub struct InMemoryDistributedCache {
map: Arc<DashMap<String, StoredBytes>>,
clock: Arc<dyn Clock>,
}
#[derive(Clone)]
struct StoredBytes {
bytes: Vec<u8>,
expires_at: Option<Timestamp>,
}
impl InMemoryDistributedCache {
#[must_use]
pub fn new(clock: Arc<dyn Clock>) -> Self {
Self {
map: Arc::new(DashMap::new()),
clock,
}
}
}
#[async_trait]
impl DistributedCache for InMemoryDistributedCache {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let now = self.clock.now();
let hit = self.map.get(key).and_then(|stored| {
if stored.expires_at.is_none_or(|exp| now.is_before(exp)) {
Some(stored.bytes.clone())
} else {
None
}
});
if hit.is_none() {
self.map.remove(key);
}
Ok(hit)
}
async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<()> {
let expires_at = ttl.map(|d| self.clock.now().saturating_add(d));
self.map.insert(
key.to_owned(),
StoredBytes {
bytes: value,
expires_at,
},
);
Ok(())
}
async fn remove(&self, key: &str) -> Result<()> {
self.map.remove(key);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::ManualClock;
#[tokio::test]
async fn in_memory_l2_round_trips_and_expires() {
let clock = Arc::new(ManualClock::default());
let dyn_clock: Arc<dyn Clock> = clock.clone();
let l2 = InMemoryDistributedCache::new(dyn_clock);
l2.set("k", b"hello".to_vec(), Some(Duration::from_secs(10)))
.await
.unwrap();
assert_eq!(l2.get("k").await.unwrap(), Some(b"hello".to_vec()));
clock.advance(Duration::from_secs(11));
assert_eq!(l2.get("k").await.unwrap(), None);
}
#[test]
fn json_serializer_round_trips_envelope() {
let entry = DistributedEntry {
value: "v".to_owned(),
created_ticks: 1,
logical_expiration_ticks: 2,
physical_expiration_ticks: 3,
is_from_fail_safe: false,
etag: Some("e".to_owned()),
last_modified_ticks: None,
tags: vec!["t".to_owned()],
};
let ser = JsonSerializer;
let bytes = DistributedSerializer::<String>::serialize(&ser, &entry).unwrap();
let back = DistributedSerializer::<String>::deserialize(&ser, &bytes).unwrap();
assert_eq!(back.value, "v");
assert_eq!(back.tags, vec!["t".to_owned()]);
}
}