Skip to main content

Crate cached

Crate cached 

Source
Expand description

Build Status crates.io docs CodSpeed Badge

Caching structures and simplified function memoization

cached provides implementations of several caching structures as well as macros for defining memoized functions.

Memoized functions defined using #[cached]/#[once]/#[concurrent_cached] macros are thread-safe with the backing function-cache wrapped in a mutex/rwlock, or externally synchronized in the case of #[concurrent_cached]. By default, the function-cache is not locked for the duration of the function’s execution, so initial (on an empty cache) concurrent calls of long-running functions with the same arguments will each execute fully and each overwrite the memoized value as they complete. This mirrors the behavior of Python’s functools.lru_cache. To synchronize the execution and caching of un-cached arguments, specify #[cached(sync_writes = true)] / #[once(sync_writes = true)]; for #[cached], use sync_writes = "by_key" to synchronize duplicate keys through bucketed per-key locks (not supported by #[once] or #[concurrent_cached]).

Upgrading from a pre-1.0 release? 1.0 contains breaking changes (store renames, removed declarative macros, renamed macro/builder attributes, and a changed Redis key format). See the 1.0 migration guide for a step-by-step walkthrough, or the agent-oriented guide for automated migration tooling.

Features

  • default: Include proc_macro, ahash, and time_stores features
  • proc_macro: Include proc macros
  • ahash: Enable the optional ahash hasher as default hashing algorithm.
  • async_core: Include runtime-agnostic async traits used by async cache stores
  • async: Include support for async functions and async cache stores using Tokio synchronization
  • async_tokio_rt_multi_thread: Enable tokio’s optional rt-multi-thread feature.
  • redis_store: Include Redis cache store
  • redis_smol: Include async Redis support using smol and smol tls support, implies redis_store and async
  • redis_tokio: Include async Redis support using tokio and tokio tls support, implies redis_store and async
  • redis_connection_manager: Enable the optional connection-manager feature of redis. Any async redis caches created will use a connection manager instead of a MultiplexedConnection. Implies async (Tokio runtime) and redis_store, but does not enable TLS. Add redis_tokio alongside if TLS is required.
  • redis_async_cache: Enable Redis client-side caching over RESP3 for async Redis caches. When enabled standalone, this feature defaults to the Tokio async Redis path.
  • redis_ahash: Enable the optional ahash feature of redis
  • disk_store: Include disk cache store
  • wasm: Enable WASM support. Note that this feature is incompatible with tokio’s multi-thread runtime (async_tokio_rt_multi_thread) and all Redis features (redis_store, redis_smol, redis_tokio, redis_ahash)
  • time_stores: Include time-based cache stores (TtlCache, LruTtlCache, and TtlSortedCache). Disable this feature when targeting environments without system time support (e.g. wasm32-unknown-unknown without WASI or JS).

The procedural macros (#[cached], #[once], #[concurrent_cached]) offer a number of features, including async support. See the macros module for more samples, and the examples directory for runnable snippets. Project automation targets are documented by make help, and make check/help verifies that the help output stays in sync with supported Makefile targets.

Any custom cache that implements cached::Cached/cached::CachedAsync can be used with the #[cached]/#[once] macros in place of the built-ins. Any custom cache that implements cached::ConcurrentCached/cached::ConcurrentCachedAsync can be used with the #[concurrent_cached] macro.

Store comparison

StoreEviction policySize limitTTLRefresh on hiton_evictAsync
UnboundCacheNone (unbounded)NoNoN/AOn explicit removeYes
LruCacheLRUYesNoN/AYesYes
TtlCacheTTL (insert time)NoGlobalOptionalYesYes
LruTtlCacheLRU + TTLYesGlobalOptionalYesYes
TtlSortedCacheTTL (expiry-ordered)OptionalGlobalNoYesYes
ExpiringLruCacheLRU + value-definedYesPer-valueN/AYesYes
ExpiringCacheValue-definedNoPer-valueN/AYesYes

TtlCache/LruTtlCache/TtlSortedCache require the time_stores feature.

Behavioral guarantees

  • In-memory cache stores are not internally synchronized. Macro-defined functions wrap their backing stores in generated locks; users managing stores directly should add synchronization at the call site when sharing across threads.
  • Cached::get (and its legacy alias cache_get) requires mutable access because some stores update recency, expiration timestamps, or metrics during reads.
  • Expired values can remain allocated until a mutating operation, evict, or store-specific cleanup removes them. Methods such as len may include expired values unless a store documents otherwise.
  • Bounded caches enforce capacity on insertion. Time-bounded caches enforce freshness on lookup.
  • Redis and disk stores serialize values and return owned values; in-memory stores return references from direct store APIs and macro-generated functions clone cached return values.
  • Macro-generated cache statics use RwLock by default. Named cache statics should be inspected with .read() or .write() unless sync_lock = "mutex" is set.
  • CachedPeek provides non-mutating lookups that do not update recency, refresh TTLs, or record metrics. CachedRead is narrower and is only implemented where shared-lock lookups can preserve normal read-side semantics without recency or refresh mutation.

Per-Value Expiry via the Expires Trait

While standard timed stores (TtlCache, LruTtlCache, TtlSortedCache) enforce a single, global Time-To-Live (TTL) duration applied to all entries in the cache, ExpiringLruCache and ExpiringCache let each individual value determine its own expiration. This is accomplished by storing values that implement the Expires trait.

This approach is highly useful when caching payloads like OAuth tokens, HTTP responses with varying Cache-Control headers, or database records that contain their own absolute expiration timestamps.

When using the #[cached] or #[once] proc macros, add expires = true to opt into per-value expiry automatically. For #[cached], this selects ExpiringCache (unbounded) by default or ExpiringLruCache when size is also specified. For #[once], this stores a single value whose expiry is polled on each call.

Memory note: ExpiringCache is unbounded and only removes expired entries when the same key is accessed again. CachedIter::iter() filters expired entries from the iterator but does not remove them from the map. For high-cardinality workloads, call evict() periodically or prefer ExpiringLruCache with a size bound.

use cached::{Cached, Expires, ExpiringCache, ExpiringLruCache};
use cached::time::{Duration, Instant};

#[derive(Clone)]
struct Response {
    payload: String,
    expires_at: Instant,
}

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

let now = Instant::now();

// ExpiringCache — unbounded, default for `#[cached(expires = true)]`
let mut cache = ExpiringCache::new();
cache.cache_set("key1", Response {
    payload: "a".to_string(),
    expires_at: now + Duration::from_secs(1),
});
cache.cache_set("key2", Response {
    payload: "b".to_string(),
    expires_at: now + Duration::from_secs(3600),
});

// ExpiringLruCache — LRU-bounded, used with `#[cached(expires = true, size = N)]`
let mut lru = ExpiringLruCache::with_size(10);
lru.cache_set("key1", Response {
    payload: "a".to_string(),
    expires_at: now + Duration::from_secs(1),
});

The basic usage looks like:

use cached::macros::cached;

/// Defines a function named `fib` that uses a cache implicitly named `FIB`.
/// By default, the cache will be the function's name in all caps.
/// The following line is equivalent to #[cached(name = "FIB", unbound)]
#[cached]
fn fib(n: u64) -> u64 {
    if n == 0 || n == 1 { return n }
    fib(n-1) + fib(n-2)
}

use std::thread::sleep;
use cached::time::Duration;
use cached::macros::cached;
use cached::LruCache;

/// Use an explicit cache-type with a custom creation block and custom cache-key generating block
#[cached(
    ty = "LruCache<String, usize>",
    create = "{ LruCache::with_size(100) }",
    convert = r#"{ format!("{}{}", a, b) }"#
)]
fn keyed(a: &str, b: &str) -> usize {
    let size = a.len() + b.len();
    sleep(Duration::new(size as u64, 0));
    size
}

use cached::macros::once;

/// Only cache the initial function call.
/// Function will be re-executed after the cache
/// expires (according to `ttl` seconds).
/// When no (or expired) cache, concurrent calls
/// will synchronize (`sync_writes`) so the function
/// is only executed once.
#[once(ttl =10, option = true, sync_writes = true)]
fn keyed(a: String) -> Option<usize> {
    if a == "a" {
        Some(a.len())
    } else {
        None
    }
}

use cached::macros::cached;

/// Cannot use sync_writes and result_fallback together
#[cached(
    result = true,
    ttl = 1,
    sync_writes = "default",
    result_fallback = true
)]
fn doesnt_compile() -> Result<String, ()> {
    Ok("a".to_string())
}

use cached::macros::concurrent_cached;
use cached::AsyncRedisCache;
use cached::time::Duration;
use thiserror::Error;

#[derive(Error, Debug, PartialEq, Clone)]
enum ExampleError {
    #[error("error with redis cache `{0}`")]
    RedisError(String),
}

/// Cache the results of an async function in redis. Cache
/// keys will be prefixed with `cache_redis_prefix`.
/// A `map_error` closure must be specified to convert any
/// redis cache errors into the same type of error returned
/// by your function. All `concurrent_cached` functions must return `Result`s.
#[concurrent_cached(
    map_error = r##"|e| ExampleError::RedisError(format!("{:?}", e))"##,
    ty = "AsyncRedisCache<u64, String>",
    create = r##" {
        AsyncRedisCache::new("cached_redis_prefix", Duration::from_secs(1))
            .refresh(true)
            .build()
            .await
            .expect("error building example redis cache")
    } "##
)]
async fn async_cached_sleep_secs(secs: u64) -> Result<String, ExampleError> {
    std::thread::sleep(cached::time::Duration::from_secs(secs));
    Ok(secs.to_string())
}

use cached::macros::concurrent_cached;
use cached::DiskCache;
use thiserror::Error;

#[derive(Error, Debug, PartialEq, Clone)]
enum ExampleError {
    #[error("error with disk cache `{0}`")]
    DiskError(String),
}

/// Cache the results of a function on disk.
/// Cache files will be stored under the system cache dir
/// unless otherwise specified with `disk_dir` or the `create` argument.
/// A `map_error` closure must be specified to convert any
/// disk cache errors into the same type of error returned
/// by your function. All `concurrent_cached` functions must return `Result`s.
#[concurrent_cached(
    map_error = r##"|e| ExampleError::DiskError(format!("{:?}", e))"##,
    disk = true
)]
fn cached_sleep_secs(secs: u64) -> Result<String, ExampleError> {
    std::thread::sleep(cached::time::Duration::from_secs(secs));
    Ok(secs.to_string())
}

Functions defined via macros will have their results cached using the function’s arguments as a key, or a convert expression specified on the macro.

When a macro-defined function is called, the function’s cache is first checked for an already computed (and still valid) value before evaluating the function body.

Due to the requirements of storing arguments and return values in a global cache:

  • Function return types:
    • For in-memory stores (#[cached] / #[once]), must be owned and implement Clone
    • For I/O-backed stores used by #[concurrent_cached] (Redis and disk), must be owned, implement Clone (the generated code clones the successful value), and additionally implement serde::Serialize + serde::DeserializeOwned (the store serializes it)
  • Function arguments:
    • For in-memory stores (#[cached] / #[once]), must either be owned and implement Hash + Eq + Clone, or a convert expression must be specified on the macro to produce a key of a Hash + Eq + Clone type.
    • For I/O-backed stores used by #[concurrent_cached] (Redis and disk), must either be owned and implement Display, or a convert expression must be used to produce a key of a Display type.
  • Arguments and return values will be cloned in the process of insertion and retrieval. For Redis and disk stores, keys are additionally formatted into Strings and values are de/serialized.
  • Macro-defined functions should not be used to produce side-effectual results!
  • Macro-defined functions cannot live directly under impl blocks since macros expand to a static initialization and one or more function definitions.
  • Macro-defined functions cannot accept Self types as a parameter.

Re-exports§

pub use stores::AsyncRedisCache;redis_smol or redis_tokio
pub use stores::AsyncRedisCacheBuilder;redis_smol or redis_tokio
pub use stores::BuildError;
pub use stores::CacheEvict;
pub use stores::Expires;
pub use stores::ExpiringCache;
pub use stores::ExpiringCacheBuilder;
pub use stores::ExpiringLruCache;
pub use stores::ExpiringLruCacheBuilder;
pub use stores::LruCache;
pub use stores::LruCacheBuilder;
pub use stores::UnboundCache;
pub use stores::UnboundCacheBuilder;
pub use stores::DiskCache;disk_store
pub use stores::DiskCacheBuildError;disk_store
pub use stores::DiskCacheBuilder;disk_store
pub use stores::DiskCacheError;disk_store
pub use stores::HasEvict;time_stores
pub use stores::LruTtlCache;time_stores
pub use stores::LruTtlCacheBuilder;time_stores
pub use stores::NoEvict;time_stores
pub use stores::TtlCache;time_stores
pub use stores::TtlCacheBuilder;time_stores
pub use stores::TtlSortedCache;time_stores
pub use stores::TtlSortedCacheBuilder;time_stores
pub use stores::TtlSortedCacheError;time_stores
pub use stores::RedisCache;redis_store
pub use stores::RedisCacheBuildError;redis_store
pub use stores::RedisCacheBuilder;redis_store
pub use stores::RedisCacheError;redis_store

Modules§

macrosproc_macro
Procedural macros for defining functions that wrap a static-ref cache object.
stores

Structs§

CacheMetrics
A snapshot of cache hit/miss and size statistics.
Returnproc_macro
Used to wrap a function result so callers can see whether the result was cached.

Traits§

CacheTtltime_stores
TTL management for time-bounded cache stores.
Cached
Cache operations
CachedAsyncasync_core
CachedIter
Iteration over cache contents for stores that can expose borrowed entries.
CachedPeek
Non-mutating cache lookup for stores that can expose a value by shared reference.
CachedRead
Shared-reference cache lookup for stores that can preserve normal read semantics without an exclusive mutable borrow.
CloneCached
Extra cache operations for types that implement Clone.
ConcurrentCached
Cache operations on a store that manages its own synchronization (a shared, &self API with owned return values and a fallible Error). Implemented by RedisCache/DiskCache; implement it directly for a custom concurrent or IO-backed store (this is the ~10-line pattern the 1.0 migration guide recommends in place of the removed InMemoryAdapter):
ConcurrentCachedAsyncasync_core

Attribute Macros§

cachedproc_macro
Define a memoized function using a cache store that implements cached::Cached (and cached::CachedAsync for async functions)
concurrent_cachedproc_macro
Define a memoized function using a cache store that implements cached::ConcurrentCached (and cached::ConcurrentCachedAsync for async functions)
onceproc_macro
Define a memoized function using a cache store that implements cached::Cached (and cached::CachedAsync for async functions). Function arguments are not used to identify a cached value, only one value is cached unless a ttl expiry is specified.