use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{Context, Result};
use async_trait::async_trait;
use bytes::BytesMut;
use lazy_static::lazy_static;
use redis::{self, IntoConnectionInfo};
use rkyv::ser::{ScratchSpace, Serializer};
use rkyv::{Deserialize, Fallible, Infallible};
use tracing::{debug, info, trace, warn};
use crate::cache::DnsCache;
use crate::codec::message::{self, RequestInfo};
use crate::specs::message::Message;
use crate::specs::version_generated::VERSION_HASH;
lazy_static! {
static ref GET_WITH_TTL: redis::Script = redis::Script::new("return {redis.call('get',KEYS[1]), redis.call('ttl',KEYS[1])}");
}
pub struct Cache {
redis_conn: redis::Connection,
store_buf: BytesMut,
scratch_buf: BytesMut,
redis_key_prefix: String,
}
impl Cache {
pub fn new(url: &str, timeout: Duration) -> Result<Cache> {
let conn_info = url
.into_connection_info()
.with_context(|| format!("Failed to parse '{}' as Redis URL", url))?;
info!(
"Connecting to Redis with addr={} timeout={}ms",
conn_info.addr,
timeout.as_millis()
);
let redis_client = redis::Client::open(conn_info.clone())
.with_context(|| format!("Failed to create Redis client for {}", conn_info.addr))?;
let redis_conn = redis_client
.get_connection()
.with_context(|| format!("Redis connection failed for {}", conn_info.addr))?;
if timeout.as_millis() != 0 {
redis_conn.set_write_timeout(Some(timeout))?;
redis_conn.set_read_timeout(Some(timeout))?;
}
let mut scratch_buf = BytesMut::with_capacity(1024);
scratch_buf.resize(scratch_buf.capacity(), 0);
Ok(Cache {
redis_conn,
store_buf: BytesMut::with_capacity(1024),
scratch_buf,
redis_key_prefix: format!("kapiti_rkyv__{}__", VERSION_HASH),
})
}
fn key(self: &Cache, request_info: &RequestInfo) -> String {
format!(
"{}{}",
self.redis_key_prefix,
super::cache_key(request_info)
)
}
}
#[async_trait]
impl DnsCache for Cache {
async fn fetch(&mut self, request_info: RequestInfo) -> Result<Option<Message>> {
let cache_key = self.key(&request_info);
let response_option: Option<Vec<redis::Value>> = GET_WITH_TTL
.key(cache_key.clone())
.invoke(&mut self.redis_conn)
.with_context(|| format!("Reading cached response failed for {}", request_info.name))?;
if let Some(response_vec) = response_option {
return match (response_vec.get(0), response_vec.get(1)) {
(Some(redis::Value::Data(bytes)), Some(redis::Value::Int(raw_redis_ttl))) => {
if raw_redis_ttl <= &0 {
return Ok(None);
}
let redis_ttl: u32 = u32::try_from(*raw_redis_ttl).with_context(|| {
format!(
"Invalid TTL={} for cache_key='{}'",
raw_redis_ttl, cache_key
)
})?;
match rkyv::check_archived_root::<Message>(bytes) {
Err(e) => {
warn!("Ignoring and deleting corrupt Redis data at key='{}' (ttl={:?}, bytes={:?}): {}", cache_key, redis_ttl, bytes.len(), e);
let delete_result = redis::cmd("DEL")
.arg(&cache_key)
.query::<redis::Value>(&mut self.redis_conn);
if let Err(e) = delete_result {
warn!(
"Ignoring failed delete of corrupt Redis data at key='{}': {}",
cache_key, e
);
}
Ok(None)
}
Ok(archived) => {
let mut redis_response = archived.deserialize(&mut Infallible)?;
debug!(
"Cached response for cache_key='{}': (ttl={:?}, bytes={:?}) {}",
cache_key,
redis_ttl,
bytes.len(),
redis_response
);
if let Err(e) = message::update_cached_response(
&mut redis_response,
&request_info,
redis_ttl,
) {
warn!(
"Ignoring Redis response at cache_key='{}': {}",
cache_key, e
);
Ok(None)
} else {
Ok(Some(redis_response))
}
}
}
}
(Some(redis::Value::Nil), Some(redis::Value::Int(-2))) => {
debug!(
"Redis didn't have cached {:?} result for {}",
request_info.resource_type, request_info.name
);
Ok(None)
}
(_other_msg, _other_ttl) => {
warn!(
"Unexpected data in Redis lookup response, bad connection?: {:?}",
response_vec
);
Ok(None)
}
};
} else {
trace!(
"Redis didn't have cache {:?} result for {}",
request_info.resource_type,
request_info.name
);
Ok(None)
}
}
async fn store(&mut self, request_info: RequestInfo, response: Message) -> Result<()> {
match message::get_min_ttl_secs(&response) {
None => debug!(
"Skipping storage of {:?} response for {} with missing resources",
request_info.resource_type, request_info.name
),
Some(0) => debug!(
"Skipping storage of {:?} result for {} with TTL=0s",
request_info.resource_type, request_info.name
),
Some(response_min_ttl_secs) => {
{
let mut serializer =
BytesMutSerializer::new(&mut self.store_buf, &mut self.scratch_buf);
serializer.serialize_value(&response)?;
}
let cache_key = self.key(&request_info);
let store_result = redis::cmd("SETEX")
.arg(&cache_key)
.arg(response_min_ttl_secs as usize)
.arg(&self.store_buf[..])
.query::<redis::Value>(&mut self.redis_conn);
let size = self.store_buf.len();
self.store_buf.clear();
match store_result {
Ok(_) => debug!(
"Stored {} byte response for {:?} {} request to cache_key='{}' with TTL={}s",
size, request_info.resource_type, request_info.name, cache_key, response_min_ttl_secs
),
Err(e) => warn!(
"Failed to store response for {:?} {} request to cache_key='{}', continuing anyway: {:?}",
request_info.resource_type, request_info.name, cache_key, e
),
}
}
}
Ok(())
}
}
struct BytesMutSerializer<'a> {
store: &'a mut BytesMut,
scratch: rkyv::ser::serializers::BufferScratch<&'a mut BytesMut>,
}
impl<'a> BytesMutSerializer<'a> {
pub fn new(store: &'a mut BytesMut, scratch: &'a mut BytesMut) -> Self {
Self {
store,
scratch: rkyv::ser::serializers::BufferScratch::new(scratch),
}
}
}
impl<'a> Fallible for BytesMutSerializer<'a> {
type Error = std::io::Error;
}
impl<'a> Serializer for BytesMutSerializer<'a> {
fn pos(&self) -> usize {
self.store.len()
}
fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
self.store.extend_from_slice(bytes);
Ok(())
}
}
impl<'a> ScratchSpace for BytesMutSerializer<'a> {
unsafe fn push_scratch(
&mut self,
layout: core::alloc::Layout,
) -> Result<core::ptr::NonNull<[u8]>, Self::Error> {
self.scratch
.push_scratch(layout)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
unsafe fn pop_scratch(
&mut self,
ptr: core::ptr::NonNull<u8>,
layout: core::alloc::Layout,
) -> Result<(), Self::Error> {
self.scratch
.pop_scratch(ptr, layout)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
}