Skip to main content

sassi/backend/
mod.rs

1//! Pluggable L2 cache backend interfaces and built-in implementations.
2//!
3//! A backend is scoped by [`BackendKeyspace`], which Sassi constructs
4//! from [`crate::punnu::PunnuConfig::namespace`] and
5//! [`crate::Cacheable::cache_type_name`]. Backend implementations must treat
6//! that keyspace as the only namespace/type source of truth.
7
8mod file;
9mod memory;
10
11use crate::Cacheable;
12use crate::error::BackendError;
13use async_trait::async_trait;
14use futures::future::BoxFuture;
15use futures::stream::BoxStream;
16use serde::{Serialize, de::DeserializeOwned};
17use std::sync::Arc;
18use std::time::Duration;
19
20pub use file::FileBackend;
21pub use memory::MemoryBackend;
22
23/// Stream type used by [`CacheBackend::invalidation_stream`].
24pub type BackendInvalidationStream<Id> =
25    BoxStream<'static, Result<BackendInvalidation<Id>, BackendError>>;
26
27/// Namespace/type scope for backend storage and invalidation channels.
28///
29/// `namespace` comes from [`crate::punnu::PunnuConfig::namespace`].
30/// `type_name` is [`crate::Cacheable::cache_type_name`] for the cached type.
31/// Backends should encode both components before putting them in
32/// filesystem paths, Redis keys, channels, or other backend-native
33/// identifiers.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct BackendKeyspace {
36    /// Optional deployment/application namespace.
37    pub namespace: Option<Arc<str>>,
38    /// Cached type label from [`crate::Cacheable::cache_type_name`].
39    pub type_name: &'static str,
40}
41
42impl BackendKeyspace {
43    /// Build the canonical keyspace for `T`.
44    pub(crate) fn for_type<T: Cacheable>(namespace: Option<&str>) -> Self {
45        Self {
46            namespace: namespace.map(Arc::from),
47            type_name: T::cache_type_name(),
48        }
49    }
50}
51
52/// Backend-driven invalidation message.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
54pub enum BackendInvalidation<Id> {
55    /// Invalidate one id in the scoped type keyspace.
56    Id(Id),
57    /// Invalidate every resident L1 entry for the scoped type keyspace.
58    All,
59}
60
61/// L2 cache backend for a single [`Cacheable`] payload type.
62///
63/// Backends receive a [`BackendKeyspace`] on every operation. They
64/// should not carry an independent namespace because it could diverge
65/// from the owning [`crate::punnu::Punnu`].
66#[async_trait]
67pub trait CacheBackend<T>: Send + Sync
68where
69    T: Cacheable + Serialize + DeserializeOwned,
70    T::Id: Serialize + DeserializeOwned,
71{
72    /// Read an entry from the backend.
73    async fn get(&self, keyspace: &BackendKeyspace, id: &T::Id) -> Result<Option<T>, BackendError>;
74
75    /// Store an entry in the backend.
76    async fn put(
77        &self,
78        keyspace: &BackendKeyspace,
79        id: &T::Id,
80        value: &T,
81        ttl: Option<Duration>,
82    ) -> Result<(), BackendError>;
83
84    /// Invalidate one backend entry and publish an id-scoped invalidation if supported.
85    async fn invalidate(&self, keyspace: &BackendKeyspace, id: &T::Id) -> Result<(), BackendError>;
86
87    /// Invalidate backend entries in this keyspace and publish an all-scoped
88    /// invalidation if supported.
89    ///
90    /// Backend implementations may need to scan or batch-delete native storage.
91    /// Unless an implementation documents stronger guarantees, this is not a
92    /// quiescence barrier against concurrent writers in the same keyspace.
93    async fn invalidate_all(&self, keyspace: &BackendKeyspace) -> Result<(), BackendError>;
94
95    /// Subscribe to backend invalidations for one keyspace.
96    fn invalidation_stream(&self, _keyspace: BackendKeyspace) -> BackendInvalidationStream<T::Id> {
97        Box::pin(futures::stream::empty())
98    }
99}
100
101pub(crate) trait BackendRuntime<T: Cacheable>: Send + Sync {
102    fn get<'a>(
103        &'a self,
104        keyspace: &'a BackendKeyspace,
105        id: &'a T::Id,
106    ) -> BoxFuture<'a, Result<Option<T>, BackendError>>;
107
108    fn put<'a>(
109        &'a self,
110        keyspace: &'a BackendKeyspace,
111        id: &'a T::Id,
112        value: &'a T,
113        ttl: Option<Duration>,
114    ) -> BoxFuture<'a, Result<(), BackendError>>;
115
116    fn invalidate<'a>(
117        &'a self,
118        keyspace: &'a BackendKeyspace,
119        id: &'a T::Id,
120    ) -> BoxFuture<'a, Result<(), BackendError>>;
121
122    fn invalidation_stream(&self, keyspace: BackendKeyspace) -> BackendInvalidationStream<T::Id>;
123}
124
125struct BackendRuntimeAdapter<B> {
126    backend: B,
127}
128
129pub(crate) fn erase_backend<T, B>(backend: B) -> Arc<dyn BackendRuntime<T>>
130where
131    T: Cacheable + Serialize + DeserializeOwned,
132    T::Id: Serialize + DeserializeOwned,
133    B: CacheBackend<T> + 'static,
134{
135    Arc::new(BackendRuntimeAdapter { backend })
136}
137
138impl<T, B> BackendRuntime<T> for BackendRuntimeAdapter<B>
139where
140    T: Cacheable + Serialize + DeserializeOwned,
141    T::Id: Serialize + DeserializeOwned,
142    B: CacheBackend<T>,
143{
144    fn get<'a>(
145        &'a self,
146        keyspace: &'a BackendKeyspace,
147        id: &'a T::Id,
148    ) -> BoxFuture<'a, Result<Option<T>, BackendError>> {
149        Box::pin(self.backend.get(keyspace, id))
150    }
151
152    fn put<'a>(
153        &'a self,
154        keyspace: &'a BackendKeyspace,
155        id: &'a T::Id,
156        value: &'a T,
157        ttl: Option<Duration>,
158    ) -> BoxFuture<'a, Result<(), BackendError>> {
159        Box::pin(self.backend.put(keyspace, id, value, ttl))
160    }
161
162    fn invalidate<'a>(
163        &'a self,
164        keyspace: &'a BackendKeyspace,
165        id: &'a T::Id,
166    ) -> BoxFuture<'a, Result<(), BackendError>> {
167        Box::pin(self.backend.invalidate(keyspace, id))
168    }
169
170    fn invalidation_stream(&self, keyspace: BackendKeyspace) -> BackendInvalidationStream<T::Id> {
171        self.backend.invalidation_stream(keyspace)
172    }
173}
174
175pub(crate) fn keyspace_storage_key<T>(
176    keyspace: &BackendKeyspace,
177    id: &T::Id,
178) -> Result<String, BackendError>
179where
180    T: Cacheable,
181    T::Id: Serialize,
182{
183    let id_json = serde_json::to_vec(id)?;
184    let id_part = format!("id_{}", encode_hex(&id_json));
185    Ok(format!(
186        "{}{}",
187        keyspace_storage_key_prefix(keyspace),
188        id_part
189    ))
190}
191
192pub(crate) fn keyspace_storage_key_prefix(keyspace: &BackendKeyspace) -> String {
193    let namespace = match &keyspace.namespace {
194        Some(ns) => format!("ns_{}", encode_hex(ns.as_bytes())),
195        None => "ns_none".to_owned(),
196    };
197    let type_part = format!("ty_{}", encode_hex(keyspace.type_name.as_bytes()));
198    format!("{namespace}/{type_part}/")
199}
200
201pub(crate) fn encode_hex(bytes: &[u8]) -> String {
202    const HEX: &[u8; 16] = b"0123456789abcdef";
203    let mut encoded = String::with_capacity(bytes.len() * 2);
204    for &byte in bytes {
205        encoded.push(HEX[(byte >> 4) as usize] as char);
206        encoded.push(HEX[(byte & 0x0f) as usize] as char);
207    }
208    encoded
209}