mod article;
mod availability;
mod availability_index;
mod hybrid;
mod hybrid_codec;
pub mod ttl;
#[cfg(test)]
mod mock_hybrid;
pub use article::{ArticleCache, CachedArticle};
pub use availability::{ArticleAvailability, BackendStatus, MAX_BACKENDS};
pub use availability_index::AvailabilityIndex;
pub use hybrid::{HybridArticleCache, HybridCacheConfig, HybridCacheStats};
use crate::protocol::StatusCode;
use crate::types::{BackendId, MessageId};
use smallvec::SmallVec;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum CacheIngestResponse {
Owned(Box<[u8]>),
Pooled(crate::pool::PooledBuffer),
Chunked(crate::pool::ChunkedResponse),
Inline(SmallVec<[u8; 128]>),
}
impl CacheIngestResponse {
#[must_use]
pub(crate) fn len(&self) -> usize {
match self {
Self::Owned(buf) => buf.len(),
Self::Pooled(buf) => buf.len(),
Self::Chunked(buf) => buf.len(),
Self::Inline(buf) => buf.len(),
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn status_code(&self) -> Option<StatusCode> {
match self {
Self::Owned(buf) => StatusCode::parse(buf),
Self::Pooled(buf) => StatusCode::parse(buf.as_ref()),
Self::Chunked(buf) => {
let mut prefix = SmallVec::<[u8; 128]>::new();
buf.copy_prefix_into(3, &mut prefix);
StatusCode::parse(&prefix)
}
Self::Inline(buf) => StatusCode::parse(buf),
}
}
}
#[cfg(test)]
impl PartialEq for CacheIngestResponse {
fn eq(&self, other: &Self) -> bool {
fn chunks<'a>(buf: &'a CacheIngestResponse) -> Box<dyn Iterator<Item = &'a [u8]> + 'a> {
match buf {
CacheIngestResponse::Owned(v) => Box::new(std::iter::once(v.as_ref())),
CacheIngestResponse::Pooled(v) => Box::new(std::iter::once(v.as_ref())),
CacheIngestResponse::Chunked(v) => Box::new(v.iter_chunks()),
CacheIngestResponse::Inline(v) => Box::new(std::iter::once(v.as_slice())),
}
}
if self.len() != other.len() {
return false;
}
let left = chunks(self).flat_map(|chunk| chunk.iter().copied());
let mut right = chunks(other).flat_map(|chunk| chunk.iter().copied());
left.eq(&mut right)
}
}
#[cfg(test)]
impl Eq for CacheIngestResponse {}
impl From<Vec<u8>> for CacheIngestResponse {
fn from(value: Vec<u8>) -> Self {
Self::Owned(value.into_boxed_slice())
}
}
impl From<crate::pool::PooledBuffer> for CacheIngestResponse {
fn from(value: crate::pool::PooledBuffer) -> Self {
Self::Pooled(value)
}
}
impl From<crate::pool::ChunkedResponse> for CacheIngestResponse {
fn from(value: crate::pool::ChunkedResponse) -> Self {
Self::Chunked(value)
}
}
impl From<SmallVec<[u8; 128]>> for CacheIngestResponse {
fn from(value: SmallVec<[u8; 128]>) -> Self {
Self::Inline(value)
}
}
impl From<&[u8]> for CacheIngestResponse {
fn from(value: &[u8]) -> Self {
if value.len() <= 128 {
Self::Inline(SmallVec::<[u8; 128]>::from_slice(value))
} else {
Self::Owned(value.into())
}
}
}
impl<const N: usize> From<&[u8; N]> for CacheIngestResponse {
fn from(value: &[u8; N]) -> Self {
Self::from(value.as_slice())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn cache_ingest_response_equality_spans_storage_forms() {
let bytes = b"220 0 <test@example.com>\r\nBody\r\n.\r\n";
let pool =
crate::pool::BufferPool::new(crate::types::BufferSize::try_new(1024).unwrap(), 1)
.with_capture_pool(8, 4);
let mut pooled = pool.acquire();
pooled.copy_from_slice(bytes);
let mut chunked = crate::pool::ChunkedResponse::default();
chunked.extend_from_slice(&pool, bytes);
let small = SmallVec::<[u8; 128]>::from_slice(bytes);
assert_eq!(
CacheIngestResponse::Owned(bytes.to_vec().into_boxed_slice()),
CacheIngestResponse::Pooled(pooled)
);
assert_eq!(
CacheIngestResponse::Chunked(chunked),
CacheIngestResponse::Inline(small)
);
}
#[tokio::test]
async fn cache_ingest_response_status_code_spans_storage_forms() {
let bytes = b"220 0 <test@example.com>\r\nBody\r\n.\r\n";
let pool =
crate::pool::BufferPool::new(crate::types::BufferSize::try_new(1024).unwrap(), 1)
.with_capture_pool(8, 4);
let mut pooled = pool.acquire();
pooled.copy_from_slice(bytes);
let mut chunked = crate::pool::ChunkedResponse::default();
chunked.extend_from_slice(&pool, bytes);
let small = SmallVec::<[u8; 128]>::from_slice(bytes);
assert_eq!(
CacheIngestResponse::Owned(bytes.to_vec().into_boxed_slice()).status_code(),
Some(StatusCode::new(220))
);
assert_eq!(
CacheIngestResponse::Pooled(pooled).status_code(),
Some(StatusCode::new(220))
);
assert_eq!(
CacheIngestResponse::Chunked(chunked).status_code(),
Some(StatusCode::new(220))
);
assert_eq!(
CacheIngestResponse::Inline(small).status_code(),
Some(StatusCode::new(220))
);
}
#[test]
fn cache_ingest_response_from_short_slice_uses_inline_storage() {
let buffer = CacheIngestResponse::from(b"223\r\n".as_slice());
assert!(matches!(buffer, CacheIngestResponse::Inline(_)));
assert_eq!(buffer.status_code(), Some(StatusCode::new(223)));
}
#[test]
fn cache_ingest_response_from_large_slice_stores_owned_slice() {
let bytes = [b'x'; 129];
let buffer = CacheIngestResponse::from(bytes.as_slice());
assert!(matches!(buffer, CacheIngestResponse::Owned(_)));
}
#[test]
fn cache_ingest_response_from_vec_stores_tight_owned_slice() {
let buffer = CacheIngestResponse::from(Vec::from(&b"220 1 <tight@example>\r\n.\r\n"[..]));
assert!(matches!(buffer, CacheIngestResponse::Owned(_)));
assert_eq!(buffer.status_code(), Some(StatusCode::new(220)));
}
#[tokio::test]
async fn unified_cache_records_typed_availability_without_payload() {
let cache = UnifiedCache::memory(1000, std::time::Duration::from_secs(60));
let msg_id = MessageId::new("<typed-availability@example>".to_string()).unwrap();
let backend_id = BackendId::from_index(1);
cache
.record_backend_has_status(
msg_id.clone(),
StatusCode::new(220),
backend_id,
ttl::CacheTier::new(2),
)
.await;
let entry = cache.get(&msg_id).await.expect("entry is recorded");
assert_eq!(entry.status_code(), StatusCode::new(220));
assert_eq!(
entry
.request_cache_metadata(&entry.availability())
.payload_kind(),
crate::protocol::RequestCachePayloadKind::AvailabilityOnly
);
assert_eq!(entry.payload_len().get(), 0);
assert!(entry.has_availability_info());
assert!(entry.should_try_backend(backend_id));
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheDisplayStats {
pub entry_count: u64,
pub size_bytes: u64,
pub hit_rate: f64,
pub disk: Option<DiskDisplayStats>,
}
#[derive(Debug, Clone, Default)]
pub struct DiskDisplayStats {
pub disk_hits: u64,
pub disk_hit_rate: f64,
pub capacity: u64,
pub bytes_written: u64,
pub bytes_read: u64,
pub write_ios: u64,
pub read_ios: u64,
}
pub trait CacheStatsProvider: Send + Sync {
fn display_stats(&self) -> CacheDisplayStats;
}
impl CacheStatsProvider for ArticleCache {
fn display_stats(&self) -> CacheDisplayStats {
CacheDisplayStats {
entry_count: self.entry_count(),
size_bytes: self.weighted_size(),
hit_rate: self.hit_rate(),
disk: None,
}
}
}
impl CacheStatsProvider for AvailabilityIndex {
fn display_stats(&self) -> CacheDisplayStats {
CacheDisplayStats {
entry_count: self.entry_count(),
size_bytes: self.used_bytes(),
hit_rate: self.hit_rate(),
disk: None,
}
}
}
impl CacheStatsProvider for HybridArticleCache {
fn display_stats(&self) -> CacheDisplayStats {
let stats = self.stats();
CacheDisplayStats {
entry_count: 0, size_bytes: stats.memory_capacity, hit_rate: stats.hit_rate(),
disk: Some(DiskDisplayStats {
disk_hits: stats.disk_hits,
disk_hit_rate: stats.disk_hit_rate(),
capacity: stats.disk_capacity,
bytes_written: stats.disk_write_bytes,
bytes_read: stats.disk_read_bytes,
write_ios: stats.disk_write_ios,
read_ios: stats.disk_read_ios,
}),
}
}
}
#[derive(Debug)]
pub enum UnifiedCache {
Availability(AvailabilityIndex),
Memory(ArticleCache),
Hybrid(HybridArticleCache),
}
impl UnifiedCache {
#[must_use]
pub fn availability(ttl: std::time::Duration) -> Self {
Self::Availability(AvailabilityIndex::with_ttl(ttl))
}
#[must_use]
pub fn memory(capacity: u64, ttl: std::time::Duration) -> Self {
Self::Memory(ArticleCache::new(capacity, ttl))
}
pub async fn hybrid(config: HybridCacheConfig) -> anyhow::Result<Self> {
Ok(Self::Hybrid(HybridArticleCache::new(config).await?))
}
#[must_use]
pub const fn records_backend_has_status(&self) -> bool {
!matches!(self, Self::Availability(_))
}
#[must_use]
pub const fn stores_payload_responses(&self) -> bool {
!matches!(self, Self::Availability(_))
}
pub async fn get(&self, message_id: &MessageId<'_>) -> Option<CachedArticle> {
match self {
Self::Availability(index) => index.get(message_id),
Self::Memory(cache) => cache.get(message_id).await,
Self::Hybrid(cache) => cache
.get(message_id)
.await
.map(hybrid_codec::DiskCachedArticle::into_cached_article),
}
}
pub async fn get_request_message_id(&self, message_id: &str) -> Option<CachedArticle> {
match self {
Self::Availability(index) => index.get_request_message_id(message_id),
Self::Memory(cache) => {
let key = message_id.strip_prefix('<')?.strip_suffix('>')?;
cache.get_by_cache_key(key).await
}
Self::Hybrid(cache) => {
let key = message_id.strip_prefix('<')?.strip_suffix('>')?;
cache
.get_by_cache_key(key)
.await
.map(hybrid_codec::DiskCachedArticle::into_cached_article)
}
}
}
pub async fn upsert_ingest(
&self,
message_id: MessageId<'_>,
buffer: impl Into<CacheIngestResponse>,
backend_id: BackendId,
tier: ttl::CacheTier,
) {
let buffer = buffer.into();
match self {
Self::Availability(_) => {}
Self::Memory(cache) => {
cache
.upsert_ingest(message_id, buffer, backend_id, tier)
.await;
}
Self::Hybrid(cache) => {
cache
.upsert_ingest(message_id, buffer, backend_id, tier)
.await;
}
}
}
pub async fn record_backend_missing(&self, message_id: MessageId<'_>, backend_id: BackendId) {
match self {
Self::Availability(index) => index.record_backend_missing(&message_id, backend_id),
Self::Memory(cache) => cache.record_backend_missing(message_id, backend_id).await,
Self::Hybrid(cache) => cache.record_missing(message_id, backend_id).await,
}
}
pub async fn record_backend_has_status(
&self,
message_id: MessageId<'_>,
status_code: StatusCode,
backend_id: BackendId,
tier: ttl::CacheTier,
) {
match self {
Self::Availability(_) => {}
Self::Memory(cache) => {
cache
.record_backend_has_status(message_id, status_code, backend_id, tier)
.await;
}
Self::Hybrid(cache) => {
cache
.record_has_status(message_id, status_code, backend_id, tier)
.await;
}
}
}
pub async fn sync_availability(
&self,
message_id: MessageId<'_>,
availability: &ArticleAvailability,
) {
match self {
Self::Availability(index) => index.sync_availability(&message_id, availability),
Self::Memory(cache) => cache.sync_availability(message_id, availability).await,
Self::Hybrid(cache) => cache.sync_availability(message_id, availability).await,
}
}
pub fn load_from_disk(&self, path: &std::path::Path) -> anyhow::Result<bool> {
match self {
Self::Availability(index) => index.load_from_path(path),
Self::Memory(_) | Self::Hybrid(_) => Ok(false),
}
}
pub fn save_to_disk(&self, path: &std::path::Path) -> anyhow::Result<bool> {
match self {
Self::Availability(index) => {
index.save_to_path(path)?;
Ok(true)
}
Self::Memory(_) | Self::Hybrid(_) => Ok(false),
}
}
#[must_use]
pub fn capacity(&self) -> u64 {
match self {
Self::Availability(index) => index.capacity_bytes(),
Self::Memory(cache) => cache.capacity(),
Self::Hybrid(cache) => cache.stats().memory_capacity,
}
}
#[must_use]
pub fn entry_count(&self) -> u64 {
match self {
Self::Availability(index) => index.entry_count(),
Self::Memory(cache) => cache.entry_count(),
Self::Hybrid(_cache) => 0, }
}
#[must_use]
pub fn weighted_size(&self) -> u64 {
match self {
Self::Availability(index) => index.used_bytes(),
Self::Memory(cache) => cache.weighted_size(),
Self::Hybrid(cache) => cache.stats().memory_capacity,
}
}
#[must_use]
pub fn hit_rate(&self) -> f64 {
match self {
Self::Availability(index) => index.hit_rate(),
Self::Memory(cache) => cache.hit_rate(),
Self::Hybrid(cache) => cache.stats().hit_rate(),
}
}
#[must_use]
pub const fn is_hybrid(&self) -> bool {
matches!(self, Self::Hybrid(_))
}
#[must_use]
pub const fn is_availability_only(&self) -> bool {
matches!(self, Self::Availability(_))
}
pub async fn sync(&self) {
match self {
Self::Availability(_) | Self::Hybrid(_) => {}
Self::Memory(cache) => cache.sync().await,
}
}
pub async fn close(&self) -> anyhow::Result<()> {
match self {
Self::Availability(_) => Ok(()),
Self::Memory(_) => Ok(()), Self::Hybrid(cache) => cache.close().await,
}
}
}
impl CacheStatsProvider for UnifiedCache {
fn display_stats(&self) -> CacheDisplayStats {
match self {
Self::Availability(index) => index.display_stats(),
Self::Memory(cache) => cache.display_stats(),
Self::Hybrid(cache) => cache.display_stats(),
}
}
}