cacheflight 0.2.1

Cache-backed deduplication for async Rust services with stale-while-revalidate support
Documentation
mod common;

use cacheflight::{CacheFlight, LookupState, Result};
use common::MemoryCache;
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};
use std::time::{Duration, Instant};
use tokio::{sync::Notify, time::sleep};

#[tokio::main]
async fn main() -> Result<()> {
    let cache = MemoryCache::new();
    let cf = CacheFlight::new(cache)
        .stale_while_revalidate(Duration::from_millis(150), Duration::from_secs(2));

    let versions = Arc::new(AtomicUsize::new(0));

    let initial = cf
        .run("dashboard:home", {
            let versions = versions.clone();
            move || {
                let versions = versions.clone();
                async move {
                    let version = versions.fetch_add(1, Ordering::SeqCst) + 1;
                    sleep(Duration::from_millis(60)).await;
                    Ok(format!("dashboard-version-{version}").into_bytes())
                }
            }
        })
        .await?;

    println!(
        "initial request: state={:?}, value={}",
        initial.state(),
        String::from_utf8_lossy(initial.value())
    );

    sleep(Duration::from_millis(200)).await;

    let refresh_started = Arc::new(Notify::new());
    let release_refresh = Arc::new(Notify::new());

    let refresh_work = {
        let versions = versions.clone();
        let refresh_started = refresh_started.clone();
        let release_refresh = release_refresh.clone();

        move || {
            let versions = versions.clone();
            let refresh_started = refresh_started.clone();
            let release_refresh = release_refresh.clone();

            async move {
                let version = versions.fetch_add(1, Ordering::SeqCst) + 1;

                refresh_started.notify_waiters();
                release_refresh.notified().await;

                Ok(format!("dashboard-version-{version}").into_bytes())
            }
        }
    };

    let started = Instant::now();
    let stale = cf.run("dashboard:home", refresh_work.clone()).await?;

    println!(
        "stale request: state={:?}, returned in {:?}, value={}",
        stale.state(),
        started.elapsed(),
        String::from_utf8_lossy(stale.value())
    );
    assert_eq!(stale.state(), LookupState::Stale);

    refresh_started.notified().await;

    let during_refresh = cf.run("dashboard:home", refresh_work.clone()).await?;

    println!(
        "second stale request during refresh: state={:?}, value={}",
        during_refresh.state(),
        String::from_utf8_lossy(during_refresh.value())
    );
    assert_eq!(during_refresh.state(), LookupState::Stale);

    release_refresh.notify_waiters();

    let refreshed = loop {
        let result = cf
            .run("dashboard:home", || async {
                unreachable!("the background refresh should update the cache")
            })
            .await?;

        if result.value() == b"dashboard-version-2" {
            break result;
        }

        sleep(Duration::from_millis(10)).await;
    };

    println!(
        "after background refresh: state={:?}, value={}",
        refreshed.state(),
        String::from_utf8_lossy(refreshed.value())
    );
    println!();
    println!("This is the stale-while-revalidate contract:");
    println!("1. callers do not wait once the entry is stale but still usable");
    println!("2. only one background refresh is started");
    println!("3. later callers see the refreshed value once it lands");

    Ok(())
}