# cached
[](https://github.com/jaemk/cached/actions/workflows/build.yml)
[](https://crates.io/crates/cached)
[](https://docs.rs/cached)
> 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]` macros are thread-safe with the backing
function-cache wrapped in a mutex/rwlock. `#[concurrent_cached]` functions are thread-safe via the
store's own internal synchronization: sharded stores use per-shard `parking_lot::RwLock`; Redis and
disk stores rely on their respective server/file-system concurrency.
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]`).
- See [`cached::stores` docs](https://docs.rs/cached/latest/cached/stores/index.html) cache stores available.
- See [`macros` docs](https://docs.rs/cached/latest/cached/macros/index.html) for more macro examples.
> **Upgrading from 1.x?** 2.0 contains breaking changes (new `cache_remove_entry` required method,
> `Result`/`Option` caching behavior flipped to smart-by-default, `result`/`option` attributes
> removed, and more). See the
> [2.0 migration guide](https://github.com/jaemk/cached/blob/master/docs/migrations/1.1-to-2.0-human.md)
> for a step-by-step walkthrough.
>
> **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](https://github.com/jaemk/cached/blob/master/docs/migrations/0.x-to-1.0-human.md)
> for a step-by-step walkthrough, or the
> [agent-oriented guide](https://github.com/jaemk/cached/blob/master/docs/migrations/0.x-to-1.0.md)
> 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`](https://docs.rs/cached/latest/cached/struct.TtlCache.html), [`LruTtlCache`](https://docs.rs/cached/latest/cached/struct.LruTtlCache.html), [`TtlSortedCache`](https://docs.rs/cached/latest/cached/struct.TtlSortedCache.html), [`ShardedTtlCache`](https://docs.rs/cached/latest/cached/type.ShardedTtlCache.html), and [`ShardedLruTtlCache`](https://docs.rs/cached/latest/cached/type.ShardedLruTtlCache.html)).
Also required when using `#[concurrent_cached(ttl = …)]` on the default in-memory path.
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`](https://docs.rs/cached/latest/cached/macros/index.html) module for more samples, and the
[`examples`](https://github.com/jaemk/cached/tree/master/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.
**Macro quick reference**
| Use case | Annotated signature |
|---|---|
| **`#[cached]`** | |
| Unbounded memoize (default) | `#[cached] fn fib(n: u64) -> u64` |
| LRU-bounded — evict past N entries | `#[cached(max_size = 1_000)] fn lookup(id: u32) -> Row` |
| TTL — expire results after N seconds | `#[cached(ttl = 60)] fn config() -> Config` |
| LRU + TTL | `#[cached(max_size = 500, ttl = 300)] fn search(q: String) -> Vec<Hit>` |
| Don't cache `None` returns (implicit for `Option<T>`) | `#[cached] fn find(id: u64) -> Option<User>` |
| Don't cache `Err` returns (implicit for `Result<T, E>`) | `#[cached] fn load(id: u64) -> Result<Data, E>` |
| Force-cache `None` returns | `#[cached(cache_none = true)] fn find(id: u64) -> Option<User>` |
| Force-cache `Err` returns | `#[cached(cache_err = true)] fn load(id: u64) -> Result<Data, E>` |
| Serve stale value when function returns `Err` | `#[cached(result_fallback = true, ttl = 60)] fn fetch(id: u64) -> Result<Data, E>` |
| Per-value expiry (value carries its own TTL) | `#[cached(expires = true)] fn token(scope: String) -> Token` |
| Deduplicate concurrent first calls for same key | `#[cached(ttl = 30, sync_writes = "by_key")] fn expensive(id: u64) -> Payload` |
| Async | `#[cached(max_size = 100)] async fn remote(id: u64) -> Data` |
| **`#[once]`** | |
| Compute and cache a global value forever | `#[once] fn app_config() -> Config` |
| Refresh a global value periodically | `#[once(ttl = 300, sync_writes = true)] fn pubkey() -> Key` |
| Optional global — skip caching if `None` (implicit) | `#[once] fn feature_flag() -> Option<Flag>` |
| **`#[concurrent_cached]`** | |
| Thread-safe sharded memoize (no global lock per call) | `#[concurrent_cached] fn compute(x: u64) -> u64` |
| Sharded with LRU | `#[concurrent_cached(max_size = 1_000)] fn lookup(id: u64) -> Row` |
| Sharded with TTL | `#[concurrent_cached(ttl = 60)] fn fetch(url: String) -> Body` |
| Sharded LRU + TTL with custom shard count | `#[concurrent_cached(max_size = 1_000, ttl = 60, shards = 32)] fn query(id: u64) -> Row` |
| Per-value expiry, thread-safe | `#[concurrent_cached(expires = true)] fn session(id: u32) -> Token` |
| Per-value expiry with LRU bound | `#[concurrent_cached(expires = true, max_size = 1_000)] fn session(id: u32) -> Token` |
| Cache only successful results (implicit for `Result<T, E>`) | `#[concurrent_cached] fn load(id: u64) -> Result<Row, DbError>` |
| Don't cache `None` returns (implicit for `Option<T>`) | `#[concurrent_cached] fn find(id: u64) -> Option<Row>` |
| Serve stale value when function returns `Err` | `#[concurrent_cached(result_fallback = true, ttl = 60)] fn fetch(id: u64) -> Result<Data, E>` |
| Persist results to disk | `#[concurrent_cached(disk = true, map_error = \|e\| MyErr(e))] fn crunch(n: u64) -> Result<Data, MyErr>` |
| Redis-backed async cache | `#[concurrent_cached(ty = "AsyncRedisCache<u64, String>", create = r#"{ ... }"#, map_error = \|e\| MyErr(e))] async fn api(id: u64) -> Result<Resp, MyErr>` |
On `#[cached]` and `#[concurrent_cached]`, the preferred attribute is `max_size = N` (mirroring the `max_size` builder/constructor methods on the stores). The legacy `size = N` is still accepted as a deprecated alias, but emits a deprecation warning nudging you toward `max_size = N`. Either spelling works; setting both on one annotation is a compile error.
For the default in-memory sharded stores, `#[concurrent_cached]` accepts any return type — plain values, `Option<T>`, or `Result<T, E>`.
Plain values are always cached as-is. `Option<T>` returns skip caching `None` by default; use `cache_none = true` to also cache `None` values. `Result<T, E>` only caches `Ok` values; `Err` is returned without being stored. Use `cache_err = true` to also cache `Err` values.
The macro detects `Result<T, E>` by matching the exact identifier `Result` (including fully-qualified paths such as `std::result::Result<T, E>`). Type aliases are not resolved at macro-expansion time, so any alias — even one whose name ends with `Result` (e.g. `type MyResult<T> = Result<T, E>`) — is treated as a plain value and its `Err` variant is cached. Use `Result<T, E>` directly when you need Ok-only caching behavior.
The same applies to `Option<T>` detection: a type alias such as `type MaybeRow<T> = Option<T>` is treated as a plain value and its `None` variant is cached. Use `Option<T>` directly when you need `None`-skipping behavior.
On the default in-memory path, do **not** specify `map_error` — the sharded stores are infallible and supplying it is a compile error.
For `disk` and `redis` stores, `Result<T, E>` is required and `map_error` must convert the store's error into your `E`.
**Store comparison**
| Store | Eviction policy | Size limit | TTL | Refresh on hit | `on_evict` | Concurrent | Async |
|---|---|---|---|---|---|---|---|
| [`UnboundCache`](https://docs.rs/cached/latest/cached/struct.UnboundCache.html) | None (unbounded) | No | No | N/A | On explicit remove | No | Yes |
| [`LruCache`](https://docs.rs/cached/latest/cached/struct.LruCache.html) | LRU | Yes | No | N/A | Yes | No | Yes |
| [`TtlCache`](https://docs.rs/cached/latest/cached/struct.TtlCache.html) | TTL (insert time) | No | Global | Optional | Yes | No | Yes |
| [`LruTtlCache`](https://docs.rs/cached/latest/cached/struct.LruTtlCache.html) | LRU + TTL | Yes | Global | Optional | Yes | No | Yes |
| [`TtlSortedCache`](https://docs.rs/cached/latest/cached/struct.TtlSortedCache.html) | TTL (expiry-ordered) | Optional | Global | No | Yes | No | Yes |
| [`ExpiringLruCache`](https://docs.rs/cached/latest/cached/struct.ExpiringLruCache.html) | LRU + value-defined | Yes | Per-value | N/A | Yes | No | Yes |
| [`ExpiringCache`](https://docs.rs/cached/latest/cached/struct.ExpiringCache.html) | Value-defined | No | Per-value | N/A | Yes | No | Yes |
| [`ShardedCache`](https://docs.rs/cached/latest/cached/type.ShardedCache.html) | None (unbounded) | No | No | N/A | On explicit remove | Yes (`Arc`) | Yes |
| [`ShardedLruCache`](https://docs.rs/cached/latest/cached/type.ShardedLruCache.html) | LRU | Yes | No | N/A | Yes | Yes (`Arc`) | Yes |
| [`ShardedTtlCache`](https://docs.rs/cached/latest/cached/type.ShardedTtlCache.html) | TTL (insert time) | No | Global | Optional | Yes | Yes (`Arc`) | Yes |
| [`ShardedLruTtlCache`](https://docs.rs/cached/latest/cached/type.ShardedLruTtlCache.html) | LRU + TTL | Yes | Global | Optional | Yes (†) | Yes (`Arc`) | Yes |
| [`ShardedExpiringCache`](https://docs.rs/cached/latest/cached/type.ShardedExpiringCache.html) | Value-defined | No | Per-value | N/A | Yes | Yes (`Arc`) | Yes |
| [`ShardedExpiringLruCache`](https://docs.rs/cached/latest/cached/type.ShardedExpiringLruCache.html) | LRU + value-defined | Yes | Per-value | N/A | Yes | Yes (`Arc`) | Yes |
> "On explicit remove" — `on_evict` fires only on `cache_remove`; there is no capacity eviction or TTL expiry trigger for these stores.
> † `ShardedLruTtlCacheBuilder::on_evict` requires `K: 'static + V: 'static`; see the builder docs for details.
`TtlCache`/`LruTtlCache`/`TtlSortedCache`/`ShardedTtlCache`/`ShardedLruTtlCache` require the `time_stores` feature.
`ShardedCache` and its variants are partitioned across power-of-two shards (default: `available_parallelism() × 4`, clamped to 8–1024; the 8–1024 clamp applies only to this computed default — an explicit `shards = N` is rounded up to a power of two but never clamped) each protected by a `parking_lot::RwLock`. Shard structs are padded to 128-byte alignment (covering Intel adjacent-line prefetch and Apple Silicon 128-byte L1 lines) to eliminate false sharing; on a 64-shard deployment this amounts to ~8 KB of padding overhead per cache array. The outer type is an `Arc` — cloning is a reference share, not a deep copy (use `deep_clone()` for an independent copy; note that `deep_clone()` is an inherent method on each concrete sharded type, not part of any trait). They implement `ConcurrentCached`/`ConcurrentCachedAsync` and are the default store selected by `#[concurrent_cached]`.
For sharded LRU variants, eviction is enforced independently per shard. `max_size = N` is divided across shards with ceiling division. Use the builder's `per_shard_max_size` method for an exact per-shard cap (builder-only; `#[concurrent_cached]` does not expose a `per_shard_max_size` attribute — use `shards` to control parallelism and `max_size` for total capacity). **Capacity Fragmentation Warning**: To protect against premature evictions due to hash collisions in extremely small caches (where a shard capacity could drop to 1-2 entries), when sharding is active (`shards > 1`) we enforce a minimum capacity of `16` entries **per shard** (e.g., minimum total capacity of `128` on a single-core machine with 8 shards, or `256` on a 4-core machine with 16 shards). If you require smaller, strict limits under low capacities, configure `shards = 1` or specify `per_shard_max_size` directly (builder-only; not available via `#[concurrent_cached]`).
Because LRU caches require updating access recency, `ShardedLruCache`, `ShardedLruTtlCache`, and `ShardedExpiringLruCache` must acquire an exclusive **write lock** on accessed shards during read hits, which can lead to contention under highly concurrent read-heavy workloads. Unbounded `ShardedCache`, time-only `ShardedTtlCache` (when `refresh_on_hit` is disabled — enabling it promotes read hits to exclusive write locks), and expiring `ShardedExpiringCache` require only a **shared read lock** on read hits, avoiding this contention. To mitigate contention on LRU variants, consider increasing the number of `shards` to distribute writes.
> **`*Base` types:** Each sharded store has a corresponding `*Base` generic (`ShardedCacheBase<K, V, H>`, `ShardedLruCacheBase<K, V, H>`, etc.) parameterized on a custom [`ShardHasher`]. The named aliases (`ShardedCache`, `ShardedLruCache`, …) use the default hasher and are what most users should reach for. Use the `*Base` types only when implementing a custom `ShardHasher` for non-standard shard routing.
**Behavioral guarantees**
- Non-sharded in-memory stores (`UnboundCache`, `LruCache`, `TtlCache`, etc.) are not internally
synchronized. Macro-generated `#[cached]`/`#[once]` functions wrap them in locks; users
managing these stores directly must add their own synchronization when sharing across threads.
`Sharded*` stores are internally synchronized (per-shard `parking_lot::RwLock`) and implement
`ConcurrentCached`/`ConcurrentCachedAsync` — no external lock is needed.
Direct sharded-store method syntax is synchronous because these stores expose inherent
`cache_get` / `cache_set` / `cache_remove` helpers. Use Universal Function Call Syntax (UFCS)
for async trait calls (e.g., `cached::ConcurrentCachedAsync::cache_get(&*STORE, &key).await.expect("ShardedCache is infallible")`), where `&*STORE` dereferences a `LazyLock<Store>` or `OnceCell<Store>` static to obtain a `&Store` reference.
- `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.
- `cache_remove` fires the `on_evict` callback (if set) and counts as an eviction for
every successful removal, across all stores that track evictions. `ShardedCache` is the
exception: it has no evictions counter and always returns `None` from
`metrics().evictions`, though its `on_evict` callback still fires. The `on_evict` column
above marks the unbounded stores where explicit removal is the *only* eviction trigger. For stores with
expiry, removing a present-but-already-expired entry still evicts and fires `on_evict`,
but `cache_remove` returns `None`; use `cache_delete` or `cache_remove_entry` when you
need to know whether an entry was physically removed.
- `cache_clear()` is fast and side-effect-free: it does **not** fire `on_evict` and does
not increment the evictions counter. Use `cache_clear_with_on_evict()` when you need the
callback to fire for every removed entry (e.g., to release resources tracked via `on_evict`).
Note: neither `clear()` nor `cache_clear_with_on_evict()` is part of `ConcurrentCached`
or its async counterpart — `clear()` is exposed as an inherent method on each concrete
sharded store type, and `cache_clear_with_on_evict()` is inherent-only as well; generic code
parameterized over `ConcurrentCached` cannot call either.
- Bounded caches enforce capacity on insertion. Time-bounded caches enforce freshness on lookup.
- Redis and disk stores serialize values and return owned values. Non-sharded in-memory stores
return references from direct store APIs; sharded stores return owned `Option<V>` values
(cloned under a shard lock). Macro-generated functions clone cached return values in all cases.
- Macro-generated `#[cached]` / `#[once]` cache statics use `RwLock` by default. Named cache
statics for those macros should be inspected with `.read()` or `.write()` unless
`sync_lock = "mutex"` is set. Named `#[concurrent_cached]` statics hold a self-synchronizing
store directly: sync functions use `LazyLock<Store>`, and async functions use
`OnceCell<Store>`.
- `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.
- Sharded stores implement `ConcurrentCached`/`ConcurrentCachedAsync` instead of
`Cached`/`CachedAsync`. Generic code parameterized over `Cached<K, V>` cannot accept sharded
stores; use a `ConcurrentCached<K, V>` bound or a concrete type instead.
Sharded stores also do not implement `CachedIter` or `CachedPeek`. Code that is generic over
`CachedIter<K, V>` or uses `.iter()` / `cache_peek` must use non-sharded stores instead.
The four expiry-capable sharded stores ([`ShardedTtlCache`], [`ShardedLruTtlCache`],
[`ShardedExpiringCache`], [`ShardedExpiringLruCache`]) implement [`ConcurrentCloneCached`],
which provides `cache_get_with_expiry_status` for reading stale entries without evicting them.
**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 `max_size` is also specified. For `#[once]`, this stores a single value whose expiry is polled on each call.
For concurrent (multi-thread, no external lock) use, the sharded equivalents [`ShardedExpiringCache`] and [`ShardedExpiringLruCache`] provide the same per-value expiry with internally-synchronized sharded storage. Use `#[concurrent_cached(expires = true)]` to select them automatically.
> **Memory note:** `ExpiringCache` and `ShardedExpiringCache` are unbounded and only remove
> expired entries when the same key is accessed again. `CachedIter::iter()` (implemented on the
> non-sharded `ExpiringCache` / `ExpiringLruCache` only, not on the sharded variants) filters
> expired entries from the iterator but does not remove them from the map. For high-cardinality workloads,
> call `evict()` periodically (bring [`CacheEvict`] into scope: `use cached::CacheEvict;`; note
> that `evict()` on sharded TTL and expiring stores requires `K: Clone`) or
> prefer `ExpiringLruCache` / `ShardedExpiringLruCache` with a `max_size` bound.
```rust
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::builder().build().unwrap();
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, max_size = N)]`
let mut lru = ExpiringLruCache::builder().max_size(10).build().unwrap();
lru.cache_set("key1", Response {
payload: "a".to_string(),
expires_at: now + Duration::from_secs(1),
});
```
----
The basic usage looks like:
```rust,no_run,ignore
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)
}
# pub fn main() { }
```
----
```rust,no_run,ignore
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::builder().max_size(100).build().unwrap() }",
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
}
# pub fn main() { }
```
----
```rust,no_run,ignore
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.
# #[cfg(feature = "time_stores")]
#[once(ttl =10, sync_writes = true)]
fn keyed(a: String) -> Option<usize> {
if a == "a" {
Some(a.len())
} else {
None
}
}
# pub fn main() { }
```
----
```compile_fail
use cached::macros::cached;
/// Cannot use sync_writes and result_fallback together
#[cached(
ttl = 1,
sync_writes = "default",
result_fallback = true
)]
fn doesnt_compile() -> Result<String, ()> {
Ok("a".to_string())
}
```
----
```rust,no_run,ignore
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`.
/// Redis and disk stores require `Result<T, E>`; supply a `map_error` closure
/// to convert store errors into your error type.
#[concurrent_cached(
map_error = r##"|e| ExampleError::RedisError(format!("{:?}", e))"##,
ty = "AsyncRedisCache<u64, String>",
create = r##" {
AsyncRedisCache::builder("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())
}
```
----
```rust,no_run,ignore
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.
/// Disk stores require `Result<T, E>`; supply a `map_error` closure
/// to convert store errors into your error type.
#[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())
}
```
----
```rust,no_run,ignore
use cached::macros::concurrent_cached;
/// Memoize with the default in-memory sharded store — no `map_error`, `ty`,
/// or `create` needed. Add `max_size` for LRU eviction or `ttl` for time-based
/// expiry (requires the `time_stores` feature).
///
/// `#[concurrent_cached]` does **not** support `sync_writes`.
/// For `Option<T>` returns, `None` is skipped by default (use `cache_none = true` to cache it).
/// For `Result<T, E>` returns, only `Ok` values are cached by default (use `cache_err = true`
/// to also cache `Err`). `result_fallback = true` is supported (requires `ttl`): on an `Err`
/// return, the last cached `Ok` value for the same key is returned instead. The stale value
/// is held in the primary cache slot and re-cached with a fresh TTL window on `Err`; no
/// secondary store is created.
#[concurrent_cached]
fn slow_double(x: u64) -> u64 {
std::thread::sleep(cached::time::Duration::from_millis(10));
x * 2
}
/// LRU capacity of 1 000 entries spread across shards.
#[concurrent_cached(max_size = 1000)]
fn slow_triple(x: u64) -> u64 {
x * 3
}
/// Only cache successful lookups — `Err` is returned but not stored.
#[concurrent_cached]
fn load_user(id: u64) -> Result<String, std::io::Error> {
Ok(format!("user_{id}"))
}
```
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 in-memory `#[concurrent_cached]` (sharded stores — the default), must implement `Clone`.
Any return type is accepted: plain `T`, `Option<T>`, or `Result<T, E>`. `Option<T>` skips
caching `None` by default; use `cache_none = true` to also cache `None`. When the
return type is `Result<T, E>`, only `Ok(v)` is stored — `Err` values are returned but not cached.
Use `cache_err = true` to also cache `Err` values.
- For I/O-backed stores used by `#[concurrent_cached]` (Redis and disk), must be `Result<T, E>`
where `T: Clone + serde::Serialize + serde::DeserializeOwned` (the store serializes it).
`map_error` must be supplied to convert the store's error into `E`.
- 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 in-memory `#[concurrent_cached]` (sharded stores), must implement `Hash + Eq + Clone`. The
macro's default key construction always clones function arguments, so `K: Clone` is required on
every in-memory path. (When using `convert` to supply an already-owned key, only the store's
own bounds apply: `K: Hash + Eq` for unbounded/TTL-only variants, `K: Hash + Eq + Clone` for LRU
variants — except when `result_fallback = true` is also set, which always requires `K: Clone`
regardless of store variant because the generated code clones the key into the fallback store.)
- For I/O-backed stores used by `#[concurrent_cached]` (Redis and disk), must either be owned and
implement `Display + Clone`, or a `convert` expression must be used to produce a key of a
`Display + Clone` type. `Clone` is needed so removal APIs can return the stored key.
- Arguments and return values will be `cloned` in the process of insertion and retrieval. For Redis and
disk stores, keys are additionally formatted into `String`s 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.
License: MIT