Skip to content

Latest commit

 

History

History
116 lines (86 loc) · 4.58 KB

CACHE.md

File metadata and controls

116 lines (86 loc) · 4.58 KB

Cache component

PSR-like cache pools live in serenade-cache.

Piece Role
CacheItem / ArrayCacheItem Key, hit flag, Arc value, optional TTL, optional tags
CacheItemPool / ArrayAdapter In-memory get/save/delete/clear; tag invalidation
FilesystemAdapter Disk-backed pool (directory + prefix); TTL in file header
CacheMarshaller / BytesMarshaller Wire encoding (Vec<u8>, String) for durable adapters
RedisAdapter (feature redis) Redis via redis-rs + r2d2
cache.pool tag DI tag; RegisterDefaultCachePoolPass seeds cache.app with ArrayAdapter

Tag invalidation

Symfony-shaped tags on items, invalidation on the pool:

use std::sync::Arc;
use serenade_cache::{ArrayAdapter, ArrayCacheItem, CacheItem, CacheItemPool};

let pool = ArrayAdapter::new();
let mut item = ArrayCacheItem::miss("product:42");
item.set(Arc::new(String::from("payload")));
item.tag(&["product", "catalog"]);
pool.save(item)?;

pool.invalidate_tags(&["product"])?; // removes every item tagged `product`
  • Empty tag names are ignored when tagging or invalidating
  • Re-saving an item without tags clears its previous tag links
  • ArrayAdapter keeps an in-memory tag index
  • FilesystemAdapter embeds tags in the on-disk record (format v2) and indexes them under {prefix}/.tags/
  • RedisAdapter (feature redis) stores tag membership in Redis SETs ({prefix}\0tag\0… / {prefix}\0tags\0…)

Filesystem adapter

No feature flag. Open with a directory and optional prefix subdirectory (default serenade):

use std::sync::Arc;
use serenade_cache::{
    BytesMarshaller, CacheItemPool, FilesystemAdapter, FilesystemAdapterConfig,
};

let pool = FilesystemAdapter::open(
    FilesystemAdapterConfig::new("/var/cache/myapp").with_prefix("serenade"),
    Arc::new(BytesMarshaller),
)?;
  • One file per key under {directory}/{prefix}/{hex(key)}.cache
  • File header stores version + absolute expiry (Unix millis); 0 means no TTL
  • Format v2 also stores invalidation tags; tag membership lives in {prefix}/.tags/{hex(tag)}.idx
  • Expired items are deleted on read (get_item / miss path), including tag unlinking
  • clear removes *.cache files and the .tags index directory (not the whole disk)
  • Values must be marshallable (BytesMarshaller: String / Vec<u8>); other types: serialize in the app first
  • Atomic write via temp file + rename

Default DI pass still registers ArrayAdapter. Apps register FilesystemAdapter (or Redis) themselves under cache.pool / cache.app.

Redis adapter

Enable with --features redis on serenade-cache.

  • URL: RedisAdapterConfig::new("redis://127.0.0.1:6379/0")
  • Logical key cart:1 becomes Redis key {prefix}cart:1 (default prefix serenade:)
  • TTL uses SET … PX
  • Tags: SET {prefix}\0tags\0{key} (tag names) and SET {prefix}\0tag\0{tag} (member keys); invalidate_tags deletes members
  • clear runs SCAN MATCH {prefix}* then DEL (never FLUSHDB), including tag metadata keys
  • Uses the same BytesMarshaller as the filesystem adapter

CacheItemPool is sync. Async HTTP callers should use spawn_blocking (or call from sync code).

Local Redis

make redis-up
make redis-test
make redis-down

Compose file: docker/compose.yml (redis:7-alpine on port 6379). Override with REDIS_URL.

Values

BytesMarshaller stores tagged Vec<u8> and String payloads. Other types: serialize in the app, then CacheItem::set(Arc::new(bytes)).

HTTP cache headers (wire)

Application item pools (serenade-cache) are not the same as HTTP caching. Helpers live in serenade-http:

Piece Role
HttpCacheHeaders Build Cache-Control, ETag, Last-Modified, Vary on a Response
etag / weak_etag Quote strong / weak validators
maybe_not_modified Conditional GET/HEAD → 304 when validators match
use serenade_http::{
    HttpCacheHeaders, Method, Request, Response, etag, maybe_not_modified,
};

let response = HttpCacheHeaders::public_max_age(60)
    .with_etag(etag("v1"))
    .with_vary("Accept-Encoding")
    .apply(Response::text(200, "body"));

let request = Request::new(Method::Get, "/page").with_header("If-None-Match", "\"v1\"");
let response = maybe_not_modified(&request, response); // 304, empty body
  • If-None-Match takes precedence over If-Modified-Since when both are sent
  • 304 keeps cache-control, etag, last-modified, vary, and expires from the full response
  • Does not configure Varnish, ESI, or CDN products