use super::{CacheKey, CacheMeta};
use crate::eviction::{CacheEntryId, CacheEntryKey, CacheEntryKeyRef};
use crate::key::CompactCacheKey;
use crate::trace::SpanHandle;
use async_trait::async_trait;
use pingora_error::Result;
use std::any::Any;
use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Debug, Clone, Copy)]
pub enum PurgeType {
Eviction,
Invalidation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PurgeAction {
Delete,
Expire,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PurgeTarget<'a> {
Active(&'a CompactCacheKey),
Exact(&'a CacheEntryKey),
}
impl Display for PurgeTarget<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Active(key) => write!(f, "active entry for {key}"),
Self::Exact(entry) => write!(f, "{entry}"),
}
}
}
impl<'a> PurgeTarget<'a> {
pub fn key(self) -> &'a CompactCacheKey {
match self {
Self::Active(key) => key,
Self::Exact(entry) => entry.key(),
}
}
pub fn removed_entry(self, id: Option<CacheEntryId>) -> CacheEntryKeyRef<'a> {
match self {
Self::Active(key) => CacheEntryKeyRef::from_entry_id(key, id),
Self::Exact(entry) => {
debug_assert!(
id.is_none() || id == entry.entry_id(),
"purge outcome ID {id:?} must match exact target ID {:?}",
entry.entry_id()
);
entry.into()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_removed_entry_accepts_absent_or_matching_id() {
let id = CacheEntryId::new(42);
let entry = CacheEntryKey::identified(CompactCacheKey::default(), id);
let target = PurgeTarget::Exact(&entry);
assert_eq!(target.removed_entry(None), (&entry).into());
assert_eq!(target.removed_entry(Some(id)), (&entry).into());
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "must match exact target ID")]
fn exact_removed_entry_rejects_mismatched_id() {
let entry = CacheEntryKey::identified(CompactCacheKey::default(), CacheEntryId::new(42));
PurgeTarget::Exact(&entry).removed_entry(Some(CacheEntryId::new(43)));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PurgeOutcome {
NotFound,
Purged(Option<CacheEntryId>),
Expired,
}
#[async_trait]
pub trait Storage {
async fn lookup(
&'static self,
key: &CacheKey,
trace: &SpanHandle,
) -> Result<Option<(CacheMeta, HitHandler)>>;
async fn lookup_streaming_write(
&'static self,
key: &CacheKey,
_streaming_write_tag: Option<&[u8]>,
trace: &SpanHandle,
) -> Result<Option<(CacheMeta, HitHandler)>> {
self.lookup(key, trace).await
}
async fn get_miss_handler(
&'static self,
key: &CacheKey,
meta: &CacheMeta,
trace: &SpanHandle,
) -> Result<MissHandler>;
async fn purge(
&'static self,
target: PurgeTarget<'_>,
purge_type: PurgeType,
trace: &SpanHandle,
) -> Result<PurgeOutcome>;
async fn expire(
&'static self,
target: PurgeTarget<'_>,
trace: &SpanHandle,
) -> Result<PurgeOutcome> {
self.purge(target, PurgeType::Invalidation, trace).await
}
async fn update_meta(
&'static self,
key: &CacheKey,
meta: &CacheMeta,
trace: &SpanHandle,
) -> Result<bool>;
fn support_streaming_partial_write(&self) -> bool {
false
}
fn as_any(&self) -> &(dyn Any + Send + Sync + 'static);
}
#[async_trait]
pub trait HandleHit {
async fn read_body(&mut self) -> Result<Option<bytes::Bytes>>;
async fn finish(
self: Box<Self>, storage: &'static (dyn Storage + Sync),
key: &CacheKey,
trace: &SpanHandle,
) -> Result<()>;
fn can_seek(&self) -> bool {
false
}
fn can_seek_multipart(&self) -> bool {
self.can_seek()
}
fn seek(&mut self, _start: usize, _end: Option<usize>) -> Result<()> {
todo!("seek() needs to be implemented")
}
fn seek_multipart(&mut self, start: usize, end: Option<usize>) -> Result<()> {
self.seek(start, end)
}
fn should_count_access(&self) -> bool {
true
}
fn get_eviction_weight(&self) -> usize {
0
}
fn entry_id(&self) -> Option<CacheEntryId> {
None
}
fn as_any(&self) -> &(dyn Any + Send + Sync);
fn as_any_mut(&mut self) -> &mut (dyn Any + Send + Sync);
}
pub type HitHandler = Box<dyn HandleHit + Sync + Send>;
pub enum MissFinishType {
Created(usize),
Appended(usize, Option<usize>),
}
#[async_trait]
pub trait HandleMiss {
async fn write_body(&mut self, data: bytes::Bytes, eof: bool) -> Result<()>;
async fn finish(
self: Box<Self>, ) -> Result<MissFinishType>;
fn streaming_write_tag(&self) -> Option<&[u8]> {
None
}
fn entry_id(&self) -> Option<CacheEntryId> {
None
}
}
pub type MissHandler = Box<dyn HandleMiss + Sync + Send>;
pub mod streaming_write {
#[derive(Debug, Clone, Copy)]
pub struct U64WriteId([u8; 8]);
impl U64WriteId {
pub fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
}
impl From<u64> for U64WriteId {
fn from(value: u64) -> U64WriteId {
U64WriteId(value.to_be_bytes())
}
}
impl From<U64WriteId> for u64 {
fn from(value: U64WriteId) -> u64 {
u64::from_be_bytes(value.0)
}
}
impl TryFrom<&[u8]> for U64WriteId {
type Error = std::array::TryFromSliceError;
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
Ok(U64WriteId(value.try_into()?))
}
}
#[derive(Debug, Clone, Copy)]
pub struct U32WriteId([u8; 4]);
impl U32WriteId {
pub fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
}
impl From<u32> for U32WriteId {
fn from(value: u32) -> U32WriteId {
U32WriteId(value.to_be_bytes())
}
}
impl From<U32WriteId> for u32 {
fn from(value: U32WriteId) -> u32 {
u32::from_be_bytes(value.0)
}
}
impl TryFrom<&[u8]> for U32WriteId {
type Error = std::array::TryFromSliceError;
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
Ok(U32WriteId(value.try_into()?))
}
}
}