Skip to content
jaemkPublic

About

Rust cache structures and easy function memoization

Topics

Resources

Contributing

Stars

2.1k stars

Watchers

9 watching

Forks

Repository files navigation

cached

Build Status crates.io docs

Memoize Rust functions with one attribute. In-memory, sharded, Redis, and disk caches.

use cached::macros::cached;

/// Cache up to 1,000 users for 60 seconds each.
/// `Err` results are not cached, so a failed request is retried on the next call.
#[cached(max_size = 1_000, ttl_secs = 60)]
fn get_user(id: u64) -> Result<User, ApiError> {
    api::fetch_user(id) // slow network call
}

let user = get_user(1)?; // calls the API
let user = get_user(1)?; // returned from the cache

The same attribute works on async fn (with the async feature), and #[concurrent_cached] swaps in a sharded store, Redis, or an on-disk cache without changing the function.

[dependencies]
cached = "4"

Contents

Memoizing functions

The macros cache a function's return value keyed by its arguments. On each call the cache is checked first, and the function body only runs on a miss. Each macro also generates {fn}_no_cache (calls the body directly) and {fn}_prime_cache (recomputes and stores a fresh value).

The #[cached] macro

The general-purpose macro. Unbounded by default; add max_size for LRU eviction and ttl_secs for expiry.

use cached::macros::cached;

#[cached]
fn fib(n: u64) -> u64 {
    if n == 0 || n == 1 {
        return n;
    }
    fib(n - 1) + fib(n - 2)
}

assert_eq!(fib(80), 23_416_728_348_467_685);

The #[once] macro

Caches a single value regardless of arguments, for global config or tokens that are refreshed periodically.

use cached::macros::once;

#[once(ttl_secs = 300)]
fn config() -> Config {
    load_config() // re-run at most once every 5 minutes
}

The #[concurrent_cached] macro

Uses a sharded store with per-shard locks instead of one lock around the whole cache, so concurrent callers on different keys do not contend. It also backs the Redis and disk caches.

use cached::macros::concurrent_cached;

#[concurrent_cached(max_size = 10_000)]
fn lookup(id: u64) -> Row {
    query(id)
}

Common attributes

Attribute Effect
max_size = N LRU eviction past N entries
ttl_secs = N / ttl_millis = N Expire entries after a fixed duration
refresh = true Reset an entry's TTL on every hit
expires = true Each value decides its own expiry (see Per-value expiry)
key = "Type", convert = { expr } Build the cache key from the arguments yourself
sync_writes = "by_key" Concurrent first calls for the same key run the body once
result_fallback = true On Err, return the last cached Ok value
name = "NAME" Name of the generated cache static (defaults to the function name in caps)
in_impl = true Cache a method inside an impl block

See the macros module for the full attribute reference, including synchronization options and argument and return-type requirements.

Result and Option returns

A function returning Result<T, E> caches only Ok values, and one returning Option<T> caches only Some. Opt back in with cache_err = true or cache_none = true. Detection matches the literal Result / Option type names, so a type alias is cached as a plain value.

Async functions

Enable the async feature and annotate an async fn. The feature is runtime-agnostic and does not depend on tokio.

use cached::macros::cached;

#[cached(max_size = 100, ttl_secs = 30)]
async fn get(url: String) -> Body {
    fetch(&url).await
}

Redis and disk caches

#[concurrent_cached] can store results in Redis (redis_* features) or on disk with redb (redb_store feature). Values are serialized, so they must implement serde::Serialize and serde::DeserializeOwned, and the function must return Result so store errors have somewhere to go.

use cached::macros::concurrent_cached;

/// Reads the connection string from `CACHED_REDIS_CONNECTION_STRING`.
#[concurrent_cached(redis = true, ttl_secs = 300, map_error = |e| MyError(format!("{e:?}")))]
async fn report(id: u64) -> Result<String, MyError> {
    fetch_report(id).await
}
use cached::macros::concurrent_cached;

/// Stored under the system cache directory unless `disk_dir` is set.
#[concurrent_cached(disk = true, map_error = |e| MyError(format!("{e:?}")))]
fn thumbnail(n: u64) -> Result<String, MyError> {
    Ok(render(n))
}

See examples/redis-async-tokio.rs and examples/disk.rs for complete programs, including explicit store construction with ty and create.

Using cache stores directly

Every store the macros use is also a plain type. Import the store and the prelude, which brings the cache traits into scope:

use cached::LruCache;
use cached::prelude::*;

let mut cache: LruCache<String, u32> = LruCache::new(100);
cache.set("a".to_string(), 1);
assert_eq!(cache.get("a"), Some(&1));
assert_eq!(cache.len(), 1);

Single-owner stores like LruCache take &mut self and need your own lock to share across threads. The Sharded* stores lock internally and can be shared directly:

use cached::ShardedLruCache;

let cache: ShardedLruCache<String, u32> = ShardedLruCache::new(1_000);
cache.set("a".to_string(), 1);
assert_eq!(cache.get("a"), Some(1));

Builders configure TTLs, eviction callbacks, and the rest:

use cached::LruTtlCache;
use cached::time::Duration;

let cache: LruTtlCache<u64, String> = LruTtlCache::builder()
    .max_size(1_000)
    .ttl(Duration::from_secs(60))
    .on_evict(|key, _value| println!("evicted {key}"))
    .build()
    .unwrap();

Choosing a store

Need Single-owner Concurrent
Unbounded UnboundCache ShardedUnboundCache
Size limit (LRU) LruCache ShardedLruCache
Fixed TTL TtlCache ShardedTtlCache
Size limit + fixed TTL LruTtlCache ShardedLruTtlCache
Per-value expiry ExpiringCache ShardedExpiringCache
Size limit + per-value expiry ExpiringLruCache ShardedExpiringLruCache
Shared across processes RedisCache, AsyncRedisCache
Persisted to disk RedbCache

TtlSortedCache is a TTL cache ordered by expiry with an optional size limit. Custom stores work with the macros by implementing Cached (for #[cached] / #[once]) or ConcurrentCached (for #[concurrent_cached]).

The stores module documents each store's eviction, locking, and on_evict behavior in detail.

Per-value expiry

When each value knows its own lifetime (an OAuth token, an HTTP response with Cache-Control), implement Expires and set expires = true:

use cached::macros::cached;
use cached::Expires;
use cached::time::{Duration, Instant};

#[derive(Clone)]
struct Token {
    value: String,
    expires_at: Instant,
}

impl Expires for Token {
    fn is_expired(&self) -> bool {
        Instant::now() >= self.expires_at
    }
}

#[cached(expires = true)]
fn token(scope: String) -> Token {
    Token { value: format!("token-{scope}"), expires_at: Instant::now() + Duration::from_secs(3600) }
}

See examples/expires_per_key.rs.

Feature flags

Feature Default Enables
proc_macro yes #[cached], #[once], #[concurrent_cached]
time_stores yes TTL stores and the ttl_* macro attributes (disable for wasm32-unknown-unknown without time support)
ahash yes ahash as the default hasher
async Async functions and async store traits (runtime-agnostic)
async_core Async traits only, without the async-lock dependency
redis_store Synchronous Redis store
redis_tokio / redis_smol Async Redis on tokio or smol
redis_tokio_native_tls / redis_tokio_rustls redis_tokio with TLS
redis_smol_native_tls / redis_smol_rustls redis_smol with TLS
redis_connection_manager Auto-reconnecting Redis connections (pair with a runtime feature)
redis_async_cache Redis client-side caching over RESP3 (pair with a runtime feature)
redb_store Disk store backed by redb

Requirements

  • Rust 1.92 or later.
  • Arguments must be Hash + Eq + Clone, or supply key and convert to build a key that is. Floats are the common case that needs convert, e.g. convert = { format!("{x:.6}") }.
  • Return values must be Clone; they are cloned on every hit. Return Arc<T> when T is expensive to clone.
  • Cached functions should not have side effects, since a hit skips the body.
  • Generic functions need key and convert to produce a concrete key type.

Further reading

Upgrading

Migration guides for each release are in docs/migrations. The major ones:

License: MIT

About

Rust cache structures and easy function memoization

Topics

Resources

Contributing

Stars

2.1k stars

Watchers

9 watching

Forks

Releases

Packages

Used by

Contributors

Languages