salvo-caching

📁 salvo-rs/salvo-skills 📅 3 days ago
1
总安装量
1
周安装量
#49657
全站排名
安装命令
npx skills add https://github.com/salvo-rs/salvo-skills --skill salvo-caching

Agent 安装分布

amp 1
opencode 1
cursor 1
kimi-cli 1
github-copilot 1

Skill 文档

Salvo Caching Strategies

This skill helps implement caching in Salvo applications for better performance.

HTTP Cache Headers

Set cache control headers to enable browser and proxy caching:

use salvo::prelude::*;

#[handler]
async fn cached_response(res: &mut Response) -> &'static str {
    // Cache for 1 hour
    res.headers_mut().insert(
        "Cache-Control",
        "public, max-age=3600".parse().unwrap()
    );

    "This response will be cached by browsers"
}

Cache-Control Directives

// Public caching (can be cached by proxies)
res.headers_mut().insert("Cache-Control", "public, max-age=3600".parse().unwrap());

// Private caching (only browser can cache)
res.headers_mut().insert("Cache-Control", "private, max-age=3600".parse().unwrap());

// No caching
res.headers_mut().insert("Cache-Control", "no-store".parse().unwrap());

// Stale while revalidate (serve stale while refreshing)
res.headers_mut().insert(
    "Cache-Control",
    "public, max-age=3600, stale-while-revalidate=86400".parse().unwrap()
);

ETag for Validation

use salvo::prelude::*;
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;

#[handler]
async fn with_etag(req: &mut Request, res: &mut Response) -> Result<Json<Data>, StatusError> {
    let data = get_data().await?;

    // Generate ETag from content
    let mut hasher = DefaultHasher::new();
    format!("{:?}", data).hash(&mut hasher);
    let etag = format!("\"{}\"", hasher.finish());

    // Check If-None-Match header
    if let Some(if_none_match) = req.header::<String>("If-None-Match") {
        if if_none_match == etag {
            res.status_code(StatusCode::NOT_MODIFIED);
            return Err(StatusError::not_modified());
        }
    }

    res.headers_mut().insert("ETag", etag.parse().unwrap());
    Ok(Json(data))
}

In-Memory Response Cache

use salvo::prelude::*;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};

#[derive(Clone)]
struct CacheEntry {
    data: String,
    expires_at: Instant,
}

#[derive(Clone)]
struct ResponseCache {
    store: Arc<RwLock<HashMap<String, CacheEntry>>>,
    ttl: Duration,
}

impl ResponseCache {
    fn new(ttl: Duration) -> Self {
        Self {
            store: Arc::new(RwLock::new(HashMap::new())),
            ttl,
        }
    }

    fn get(&self, key: &str) -> Option<String> {
        let store = self.store.read().ok()?;
        let entry = store.get(key)?;

        if Instant::now() < entry.expires_at {
            Some(entry.data.clone())
        } else {
            None
        }
    }

    fn set(&self, key: String, data: String) {
        if let Ok(mut store) = self.store.write() {
            store.insert(key, CacheEntry {
                data,
                expires_at: Instant::now() + self.ttl,
            });
        }
    }
}

#[handler]
async fn cache_middleware(
    req: &mut Request,
    depot: &mut Depot,
    res: &mut Response,
    ctrl: &mut FlowCtrl
) {
    let cache = depot.obtain::<ResponseCache>().unwrap();
    let cache_key = format!("{}:{}", req.method(), req.uri().path());

    // Try cache first
    if let Some(cached) = cache.get(&cache_key) {
        res.headers_mut().insert("X-Cache", "HIT".parse().unwrap());
        res.render(cached);
        return;
    }

    // Process request
    ctrl.call_next(req, depot, res).await;
    res.headers_mut().insert("X-Cache", "MISS".parse().unwrap());

    // Cache successful responses
    // Note: In production, extract body from response properly
}

Using Moka for Caching

[dependencies]
moka = { version = "0.12", features = ["future"] }
use salvo::prelude::*;
use moka::future::Cache;
use std::sync::Arc;
use std::time::Duration;

type AppCache = Cache<String, String>;

async fn create_cache() -> AppCache {
    Cache::builder()
        .max_capacity(10_000)
        .time_to_live(Duration::from_secs(300))
        .build()
}

#[handler]
async fn cached_handler(req: &mut Request, depot: &mut Depot) -> Result<String, StatusError> {
    let cache = depot.obtain::<AppCache>().unwrap();
    let key = req.uri().path().to_string();

    // Try cache
    if let Some(cached) = cache.get(&key).await {
        return Ok(cached);
    }

    // Compute result
    let result = expensive_computation().await?;

    // Store in cache
    cache.insert(key, result.clone()).await;

    Ok(result)
}

#[tokio::main]
async fn main() {
    let cache = create_cache().await;

    let router = Router::new()
        .hoop(affix_state::inject(cache))
        .get(cached_handler);

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

Database Query Caching

use salvo::prelude::*;
use moka::future::Cache;
use serde::{Deserialize, Serialize};
use std::time::Duration;

#[derive(Clone, Serialize, Deserialize)]
struct User {
    id: i64,
    name: String,
    email: String,
}

type UserCache = Cache<i64, User>;

async fn create_user_cache() -> UserCache {
    Cache::builder()
        .max_capacity(1000)
        .time_to_live(Duration::from_secs(60))
        .build()
}

#[handler]
async fn get_user(req: &mut Request, depot: &mut Depot) -> Result<Json<User>, StatusError> {
    let id = req.param::<i64>("id").ok_or_else(|| StatusError::bad_request())?;
    let cache = depot.obtain::<UserCache>().unwrap();
    let pool = depot.obtain::<PgPool>().unwrap();

    // Check cache
    if let Some(user) = cache.get(&id).await {
        return Ok(Json(user));
    }

    // Query database
    let user = sqlx::query_as::<_, User>("SELECT id, name, email FROM users WHERE id = $1")
        .bind(id)
        .fetch_optional(pool)
        .await
        .map_err(|_| StatusError::internal_server_error())?
        .ok_or_else(|| StatusError::not_found())?;

    // Cache result
    cache.insert(id, user.clone()).await;

    Ok(Json(user))
}

Cache Invalidation

use moka::future::Cache;

struct CacheService {
    user_cache: Cache<i64, User>,
}

impl CacheService {
    // Invalidate single entry
    async fn invalidate_user(&self, id: i64) {
        self.user_cache.invalidate(&id).await;
    }

    // Invalidate all entries
    async fn invalidate_all_users(&self) {
        self.user_cache.invalidate_all();
    }

    // Invalidate matching entries
    async fn invalidate_users_by_ids(&self, ids: &[i64]) {
        for id in ids {
            self.user_cache.invalidate(id).await;
        }
    }
}

// Invalidate on update
#[handler]
async fn update_user(
    req: &mut Request,
    depot: &mut Depot
) -> Result<StatusCode, StatusError> {
    let id = req.param::<i64>("id").unwrap();

    // Update in database...

    // Invalidate cache
    let cache = depot.obtain::<UserCache>().unwrap();
    cache.invalidate(&id).await;

    Ok(StatusCode::OK)
}

Conditional Requests

use salvo::prelude::*;
use time::OffsetDateTime;

#[handler]
async fn conditional_get(req: &mut Request, res: &mut Response) -> Result<Json<Data>, StatusError> {
    let data = get_data().await?;
    let last_modified = data.updated_at;

    // Check If-Modified-Since
    if let Some(since) = req.header::<String>("If-Modified-Since") {
        // Parse and compare timestamps
        // Return 304 if not modified
    }

    res.headers_mut().insert(
        "Last-Modified",
        last_modified.format(&time::format_description::well_known::Rfc2822)
            .unwrap()
            .parse()
            .unwrap()
    );

    Ok(Json(data))
}

Complete Caching Example

use salvo::prelude::*;
use moka::future::Cache;
use serde::{Deserialize, Serialize};
use std::time::Duration;

#[derive(Clone, Serialize)]
struct Product {
    id: i64,
    name: String,
    price: f64,
}

type ProductCache = Cache<i64, Product>;

#[handler]
async fn get_product(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<Json<Product>, StatusError> {
    let id = req.param::<i64>("id").ok_or_else(|| StatusError::bad_request())?;
    let cache = depot.obtain::<ProductCache>().unwrap();

    // Check cache
    if let Some(product) = cache.get(&id).await {
        res.headers_mut().insert("X-Cache", "HIT".parse().unwrap());
        res.headers_mut().insert("Cache-Control", "public, max-age=60".parse().unwrap());
        return Ok(Json(product));
    }

    // Fetch from database (simulated)
    let product = Product {
        id,
        name: format!("Product {}", id),
        price: 99.99,
    };

    // Cache the result
    cache.insert(id, product.clone()).await;

    res.headers_mut().insert("X-Cache", "MISS".parse().unwrap());
    res.headers_mut().insert("Cache-Control", "public, max-age=60".parse().unwrap());

    Ok(Json(product))
}

#[tokio::main]
async fn main() {
    let cache: ProductCache = Cache::builder()
        .max_capacity(10_000)
        .time_to_live(Duration::from_secs(300))
        .build();

    let router = Router::new()
        .hoop(affix_state::inject(cache))
        .push(Router::with_path("products/{id}").get(get_product));

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

Best Practices

  1. Use appropriate TTL: Match cache duration to data freshness requirements
  2. Set cache headers: Enable browser and CDN caching
  3. Implement cache invalidation: Clear cache when data changes
  4. Use ETag for validation: Enable conditional requests
  5. Monitor cache hit rate: Track effectiveness
  6. Size cache appropriately: Balance memory usage and hit rate
  7. Cache at multiple layers: Browser, CDN, application, database
  8. Consider stale-while-revalidate: Serve stale content while refreshing