use std::sync::Arc;
use std::time::Duration;
use crate::cache::{encode_counter, Cache};
use rainier_support::{BoxFuture, Error, Result};
pub const MIN_TTL: Duration = Duration::from_secs(60);
pub trait KvTransport: Send + Sync + 'static {
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>>;
fn put<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<()>>;
fn delete<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<()>>;
fn delete_all<'a>(&'a self, prefix: &'a str) -> BoxFuture<'a, Result<()>> {
let _ = prefix;
Box::pin(async {
Err(Error::internal(
"this KV transport cannot flush — KV has no flush operation, so it has to be a \
list-and-delete and not every transport implements one",
))
})
}
}
pub struct KvCache {
transport: Arc<dyn KvTransport>,
prefix: String,
}
impl KvCache {
pub fn new(transport: Arc<dyn KvTransport>) -> Self {
Self { transport, prefix: String::new() }
}
#[must_use = "this returns a configured cache rather than configuring in place"]
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
fn key(&self, key: &str) -> String {
format!("{}{}", self.prefix, key)
}
fn clamp(ttl: Option<Duration>) -> Option<Duration> {
ttl.map(|ttl| ttl.max(MIN_TTL))
}
}
impl Cache for KvCache {
fn name(&self) -> &str {
"cloudflare-kv"
}
fn is_shared(&self) -> bool {
true
}
fn supports_atomic_add(&self) -> bool {
false
}
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
Box::pin(async move { self.transport.get(&self.key(key)).await })
}
fn put<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { self.transport.put(&self.key(key), value, Self::clamp(ttl)).await })
}
fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move {
self.transport.delete(&self.key(key)).await?;
Ok(true)
})
}
fn flush(&self) -> BoxFuture<'_, Result<()>> {
Box::pin(async move { self.transport.delete_all(&self.prefix).await })
}
fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
Box::pin(async move {
let key = self.key(key);
let current = match self.transport.get(&key).await? {
Some(raw) => String::from_utf8_lossy(&raw).trim().parse::<i64>().unwrap_or(0),
None => 0,
};
let next = current.saturating_add(by);
self.transport.put(&key, &encode_counter(next), None).await?;
Ok(next)
})
}
fn add<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move {
let full = self.key(key);
if self.transport.get(&full).await?.is_some() {
return Ok(false);
}
self.transport.put(&full, value, Self::clamp(ttl)).await?;
Ok(true)
})
}
fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move {
let full = self.key(key);
match self.transport.get(&full).await? {
Some(current) if current == expected => {
self.transport.delete(&full).await?;
Ok(true)
}
_ => Ok(false),
}
})
}
}
impl std::fmt::Debug for KvCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KvCache").field("prefix", &self.prefix).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CacheExt, LockManager};
use std::collections::HashMap;
use std::sync::Mutex;
type Entry = (Vec<u8>, Option<Duration>);
#[derive(Default)]
struct FakeKv {
entries: Mutex<HashMap<String, Entry>>,
}
impl FakeKv {
fn ttl_of(&self, key: &str) -> Option<Duration> {
self.entries.lock().unwrap().get(key).and_then(|(_, ttl)| *ttl)
}
}
impl KvTransport for FakeKv {
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
Box::pin(async move {
Ok(self.entries.lock().unwrap().get(key).map(|(value, _)| value.clone()))
})
}
fn put<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.entries.lock().unwrap().insert(key.to_string(), (value.to_vec(), ttl));
Ok(())
})
}
fn delete<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.entries.lock().unwrap().remove(key);
Ok(())
})
}
}
fn cache() -> (KvCache, Arc<FakeKv>) {
let transport = Arc::new(FakeKv::default());
(KvCache::new(Arc::clone(&transport) as Arc<dyn KvTransport>), transport)
}
#[tokio::test]
async fn values_round_trip() {
let (cache, _) = cache();
cache.put("greeting", b"hello", None).await.unwrap();
assert_eq!(cache.get("greeting").await.unwrap().as_deref(), Some(&b"hello"[..]));
assert!(cache.has("greeting").await.unwrap());
}
#[tokio::test]
async fn a_short_ttl_is_raised_to_the_floor_rather_than_refused() {
let (cache, transport) = cache();
cache.put("k", b"v", Some(Duration::from_secs(10))).await.unwrap();
assert_eq!(transport.ttl_of("k"), Some(MIN_TTL));
}
#[tokio::test]
async fn a_long_ttl_is_left_alone() {
let (cache, transport) = cache();
cache.put("k", b"v", Some(Duration::from_secs(3600))).await.unwrap();
assert_eq!(transport.ttl_of("k"), Some(Duration::from_secs(3600)));
}
#[tokio::test]
async fn the_prefix_namespaces_every_key() {
let transport = Arc::new(FakeKv::default());
let cache =
KvCache::new(Arc::clone(&transport) as Arc<dyn KvTransport>).with_prefix("app:");
cache.put("k", b"v", None).await.unwrap();
assert!(transport.entries.lock().unwrap().contains_key("app:k"));
}
#[tokio::test]
async fn it_is_shared_but_cannot_hold_a_lock() {
let (cache, _) = cache();
assert!(cache.is_shared(), "KV is visible to every replica");
assert!(!cache.supports_atomic_add(), "and cannot compare-and-set");
}
#[tokio::test]
async fn a_lock_manager_over_it_reports_itself_unshared() {
let (cache, _) = cache();
let locks = LockManager::new(Arc::new(cache));
assert!(!locks.is_shared());
}
#[tokio::test]
async fn add_only_writes_when_the_key_is_absent() {
let (cache, _) = cache();
assert!(cache.add("k", b"first", None).await.unwrap());
assert!(!cache.add("k", b"second", None).await.unwrap());
assert_eq!(cache.get("k").await.unwrap().as_deref(), Some(&b"first"[..]));
}
#[tokio::test]
async fn counters_count() {
let (cache, _) = cache();
assert_eq!(cache.increment("hits", 1).await.unwrap(), 1);
assert_eq!(cache.increment("hits", 5).await.unwrap(), 6);
assert_eq!(cache.decrement("hits", 2).await.unwrap(), 4);
}
#[tokio::test]
async fn forgetting_removes_it() {
let (cache, _) = cache();
cache.put("k", b"v", None).await.unwrap();
assert!(cache.forget("k").await.unwrap());
assert!(cache.get("k").await.unwrap().is_none());
}
#[tokio::test]
async fn forget_if_compares_first() {
let (cache, _) = cache();
cache.put("lock", b"mine", None).await.unwrap();
assert!(!cache.forget_if("lock", b"somebody-elses").await.unwrap());
assert!(cache.forget_if("lock", b"mine").await.unwrap());
}
#[tokio::test]
async fn flushing_says_it_cannot_rather_than_pretending() {
let (cache, _) = cache();
let error = cache.flush().await.unwrap_err();
assert!(error.message().contains("no flush"), "{}", error.message());
}
}