autocache 0.5.0

automatic cache management
Documentation
use std::collections::BTreeSet;

use tracing::warn;

use crate::{cache::Cache, Entry};

pub struct TwoLevelCache<L1, L2> {
    local_cache: L1,
    redis_cache: L2,
}

impl<L1, L2> TwoLevelCache<L1, L2> {
    pub fn new(l1: L1, l2: L2) -> Self {
        Self {
            local_cache: l1,
            redis_cache: l2,
        }
    }
}

impl<L1, L2> Cache for TwoLevelCache<L1, L2>
where
    L1: Cache + Sync,
    L2: Cache<Key = L1::Key, Value = L1::Value> + Sync,
    L1::Key: Clone + Ord + Send + Sync,
    L1::Value: Clone + Send + Sync,
{
    type Key = L1::Key;
    type Value = L1::Value;

    async fn mget(&self, keys: &[Self::Key]) -> anyhow::Result<Vec<Entry<Self::Key, Self::Value>>> {
        let mut l1_entries = self.local_cache.mget(keys).await?;
        let l1_hit_keys = l1_entries
            .iter()
            .filter(|entry| !entry.is_expired())
            .map(|entry| entry.key.clone())
            .collect::<BTreeSet<_>>();
        let l1_missed_keys = keys
            .iter()
            .filter(|key| !l1_hit_keys.contains(*key))
            .cloned()
            .collect::<Vec<_>>();

        if l1_missed_keys.is_empty() {
            l1_entries.retain(|entry| !entry.is_expired());
            return Ok(l1_entries);
        }

        let mut l2_entries = match self.redis_cache.mget(&l1_missed_keys).await {
            Ok(entries) => entries,
            Err(error) => {
                warn!(
                    cache = self.redis_cache.name(),
                    missed_key_count = l1_missed_keys.len(),
                    error = %error,
                    "autocache: L2 cache read failed; falling back to available L1 entries"
                );
                return Ok(l1_entries);
            }
        };
        let fresh_l2_entries = l2_entries
            .iter()
            .filter(|entry| !entry.is_expired())
            .cloned()
            .collect::<Vec<_>>();
        if !fresh_l2_entries.is_empty() {
            let _ = self
                .local_cache
                .mset(&fresh_l2_entries)
                .await
                .inspect_err(|error| {
                    warn!(
                        cache = self.local_cache.name(),
                        entry_count = fresh_l2_entries.len(),
                        error = %error,
                        "autocache: failed to warm L1 cache with L2 entries"
                    );
                });
        }

        let l2_hit_keys = l2_entries
            .iter()
            .map(|entry| entry.key.clone())
            .collect::<BTreeSet<_>>();
        l1_entries.retain(|entry| !entry.is_expired() || !l2_hit_keys.contains(&entry.key));

        l1_entries.append(&mut l2_entries);

        Ok(l1_entries)
    }

    async fn mset(&self, entries: &[Entry<Self::Key, Self::Value>]) -> anyhow::Result<()> {
        self.redis_cache.mset(entries).await?;
        self.local_cache.mset(entries).await?;

        Ok(())
    }

    async fn mdel(&self, keys: &[Self::Key]) -> anyhow::Result<()> {
        let l1_result = self.local_cache.mdel(keys).await;
        let l2_result = self.redis_cache.mdel(keys).await;

        l1_result?;
        l2_result?;

        Ok(())
    }

    fn name(&self) -> &'static str {
        "twolevelcache"
    }

    fn set_ns(&self, ns: String) {
        self.redis_cache.set_ns(ns);
    }
}