use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use memcache::{Client, CommandError, MemcacheError};
use crate::{Cache, CacheError};
pub struct MemcacheCache {
client: Arc<Client>,
}
impl MemcacheCache {
pub fn new(addr: &str) -> Result<Self, CacheError> {
Self::with_pool_size(addr, 4)
}
pub fn with_pool_size(addr: &str, size: u32) -> Result<Self, CacheError> {
let client = Client::with_pool_size(normalize(addr), size)
.map_err(|e| CacheError::ConnectionError(format!("failed to connect: {e}")))?;
Ok(Self { client: Arc::new(client) })
}
async fn blocking<T, F>(&self, op: F) -> Result<T, CacheError>
where
T: Send + 'static,
F: FnOnce(&Client) -> Result<T, MemcacheError> + Send + 'static,
{
let client = Arc::clone(&self.client);
tokio::task::spawn_blocking(move || op(&client))
.await
.map_err(|e| CacheError::ConnectionError(format!("blocking task failed: {e}")))?
.map_err(map_err)
}
}
#[async_trait]
impl Cache for MemcacheCache {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
let key = key.to_string();
self.blocking(move |c| c.get::<Vec<u8>>(&key)).await
}
async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<u64>) -> Result<(), CacheError> {
let key = key.to_string();
match ttl {
None => self.blocking(move |c| c.set(key.as_str(), value.as_slice(), 0)).await,
Some(0) => self.blocking(move |c| c.delete(&key).map(|_| ())).await,
Some(seconds) => {
let expiration = wire_expiration(seconds);
self.blocking(move |c| c.set(key.as_str(), value.as_slice(), expiration)).await
}
}
}
async fn delete(&self, key: &str) -> Result<(), CacheError> {
let key = key.to_string();
match self.blocking(move |c| c.delete(&key)).await? {
true => Ok(()),
false => Err(CacheError::NotFound),
}
}
async fn incr(&self, key: &str) -> Result<i64, CacheError> {
let key = key.to_string();
self.blocking(move |c| {
create_counter(c, &key)?;
Ok(c.increment(&key, 1)? as i64)
})
.await
}
}
fn create_counter(client: &Client, key: &str) -> Result<(), MemcacheError> {
match client.add(key, "0", 0) {
Ok(()) => Ok(()),
Err(MemcacheError::CommandError(CommandError::KeyExists)) => Ok(()),
Err(e) => Err(e),
}
}
fn normalize(addr: &str) -> String {
if addr.contains("://") { addr.to_string() } else { format!("memcache://{addr}") }
}
fn wire_expiration(seconds: u64) -> u32 {
const THIRTY_DAYS: u64 = 60 * 60 * 24 * 30;
if seconds > THIRTY_DAYS {
let now = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
return now.saturating_add(seconds).min(u32::MAX as u64) as u32;
}
seconds as u32
}
const NON_NUMERIC_COUNTER: u16 = 0x0006;
fn map_err(e: MemcacheError) -> CacheError {
let message = e.to_string();
match e {
MemcacheError::CommandError(CommandError::Unknown(code)) if code == NON_NUMERIC_COUNTER => {
CacheError::SerializeError(message)
}
MemcacheError::ClientError(_) => CacheError::SerializeError(message),
_ => CacheError::ConnectionError(message),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_host_port_gets_a_scheme() {
assert_eq!(normalize("127.0.0.1:11211"), "memcache://127.0.0.1:11211");
assert_eq!(normalize("memcache://host:11211"), "memcache://host:11211");
}
#[test]
fn short_ttls_are_offsets() {
assert_eq!(wire_expiration(1), 1);
assert_eq!(wire_expiration(60 * 60 * 24 * 30), 2_592_000);
}
#[test]
fn long_ttls_become_absolute_timestamps() {
let before = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let seconds = 60 * 60 * 24 * 31;
let absolute = wire_expiration(seconds) as u64;
assert!(absolute >= before + seconds, "{absolute} < {before} + {seconds}");
assert!(absolute <= before + seconds + 5, "{absolute}");
}
#[test]
fn absurd_ttls_saturate_instead_of_wrapping() {
assert_eq!(wire_expiration(u64::MAX), u32::MAX);
}
}