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::storesdocs cache stores available. - See
macrosdocs for more macro examples.
Upgrading from 1.x? 2.0 contains breaking changes (new
cache_remove_entryrequired method,Result/Optioncaching behavior flipped to smart-by-default,result/optionattributes removed, and more). See the 2.0 migration guide 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 for a step-by-step walkthrough, or the agent-oriented guide for automated migration tooling.
Features
default: Includeproc_macro,ahash, andtime_storesfeaturesproc_macro: Include proc macrosahash: Enable the optionalahashhasher as default hashing algorithm.async_core: Include runtime-agnostic async traits used by async cache storesasync: Include support for async functions and async cache stores using Tokio synchronizationasync_tokio_rt_multi_thread: Enabletokio's optionalrt-multi-threadfeature.redis_store: Include Redis cache storeredis_smol: Include async Redis support usingsmolandsmoltls support, impliesredis_storeandasyncredis_tokio: Include async Redis support usingtokioandtokiotls support, impliesredis_storeandasyncredis_connection_manager: Enable the optionalconnection-managerfeature ofredis. Any async redis caches created will use a connection manager instead of aMultiplexedConnection. Impliesasync(Tokio runtime) andredis_store, but does not enable TLS. Addredis_tokioalongside 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 optionalahashfeature ofredisdisk_store: Include disk cache storewasm: Enable WASM support. Note that this feature is incompatible withtokio'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,TtlSortedCache,ShardedTtlCache, andShardedLruTtlCache). 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-unknownwithout 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.
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 |
None (unbounded) | No | No | N/A | On explicit remove | No | Yes |
LruCache |
LRU | Yes | No | N/A | Yes | No | Yes |
TtlCache |
TTL (insert time) | No | Global | Optional | Yes | No | Yes |
LruTtlCache |
LRU + TTL | Yes | Global | Optional | Yes | No | Yes |
TtlSortedCache |
TTL (expiry-ordered) | Optional | Global | No | Yes | No | Yes |
ExpiringLruCache |
LRU + value-defined | Yes | Per-value | N/A | Yes | No | Yes |
ExpiringCache |
Value-defined | No | Per-value | N/A | Yes | No | Yes |
ShardedCache |
None (unbounded) | No | No | N/A | On explicit remove | Yes (Arc) |
Yes |
ShardedLruCache |
LRU | Yes | No | N/A | Yes | Yes (Arc) |
Yes |
ShardedTtlCache |
TTL (insert time) | No | Global | Optional | Yes | Yes (Arc) |
Yes |
ShardedLruTtlCache |
LRU + TTL | Yes | Global | Optional | Yes (†) | Yes (Arc) |
Yes |
ShardedExpiringCache |
Value-defined | No | Per-value | N/A | Yes | Yes (Arc) |
Yes |
ShardedExpiringLruCache |
LRU + value-defined | Yes | Per-value | N/A | Yes | Yes (Arc) |
Yes |
"On explicit remove" —
on_evictfires only oncache_remove; there is no capacity eviction or TTL expiry trigger for these stores. †ShardedLruTtlCacheBuilder::on_evictrequiresK: '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.
*Basetypes: Each sharded store has a corresponding*Basegeneric (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*Basetypes only when implementing a customShardHasherfor 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-shardparking_lot::RwLock) and implementConcurrentCached/ConcurrentCachedAsync— no external lock is needed. Direct sharded-store method syntax is synchronous because these stores expose inherentcache_get/cache_set/cache_removehelpers. Use Universal Function Call Syntax (UFCS) for async trait calls (e.g.,cached::ConcurrentCachedAsync::cache_get(&*STORE, &key).await.expect("ShardedCache is infallible")), where&*STOREdereferences aLazyLock<Store>orOnceCell<Store>static to obtain a&Storereference. Cached::get(and its legacy aliascache_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 aslenmay include expired values unless a store documents otherwise. cache_removefires theon_evictcallback (if set) and counts as an eviction for every successful removal, across all stores that track evictions.ShardedCacheis the exception: it has no evictions counter and always returnsNonefrommetrics().evictions, though itson_evictcallback still fires. Theon_evictcolumn 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 fireson_evict, butcache_removereturnsNone; usecache_deleteorcache_remove_entrywhen you need to know whether an entry was physically removed.cache_clear()is fast and side-effect-free: it does not fireon_evictand does not increment the evictions counter. Usecache_clear_with_on_evict()when you need the callback to fire for every removed entry (e.g., to release resources tracked viaon_evict). Note: neitherclear()norcache_clear_with_on_evict()is part ofConcurrentCachedor its async counterpart —clear()is exposed as an inherent method on each concrete sharded store type, andcache_clear_with_on_evict()is inherent-only as well; generic code parameterized overConcurrentCachedcannot 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 useRwLockby default. Named cache statics for those macros should be inspected with.read()or.write()unlesssync_lock = "mutex"is set. Named#[concurrent_cached]statics hold a self-synchronizing store directly: sync functions useLazyLock<Store>, and async functions useOnceCell<Store>. CachedPeekprovides non-mutating lookups that do not update recency, refresh TTLs, or record metrics.CachedReadis narrower and is only implemented where shared-lock lookups can preserve normal read-side semantics without recency or refresh mutation.- Sharded stores implement
ConcurrentCached/ConcurrentCachedAsyncinstead ofCached/CachedAsync. Generic code parameterized overCached<K, V>cannot accept sharded stores; use aConcurrentCached<K, V>bound or a concrete type instead. Sharded stores also do not implementCachedIterorCachedPeek. Code that is generic overCachedIter<K, V>or uses.iter()/cache_peekmust use non-sharded stores instead. The four expiry-capable sharded stores ([ShardedTtlCache], [ShardedLruTtlCache], [ShardedExpiringCache], [ShardedExpiringLruCache]) implement [ConcurrentCloneCached], which providescache_get_with_expiry_statusfor 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:
ExpiringCacheandShardedExpiringCacheare unbounded and only remove expired entries when the same key is accessed again.CachedIter::iter()(implemented on the non-shardedExpiringCache/ExpiringLruCacheonly, not on the sharded variants) filters expired entries from the iterator but does not remove them from the map. For high-cardinality workloads, callevict()periodically (bring [CacheEvict] into scope:use cached::CacheEvict;; note thatevict()on sharded TTL and expiring stores requiresK: Clone) or preferExpiringLruCache/ShardedExpiringLruCachewith amax_sizebound.
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:
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() { }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() { }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() { }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())
}
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())
}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())
}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 implementClone - For in-memory
#[concurrent_cached](sharded stores — the default), must implementClone. Any return type is accepted: plainT,Option<T>, orResult<T, E>.Option<T>skips cachingNoneby default; usecache_none = trueto also cacheNone. When the return type isResult<T, E>, onlyOk(v)is stored —Errvalues are returned but not cached. Usecache_err = trueto also cacheErrvalues. - For I/O-backed stores used by
#[concurrent_cached](Redis and disk), must beResult<T, E>whereT: Clone + serde::Serialize + serde::DeserializeOwned(the store serializes it).map_errormust be supplied to convert the store's error intoE.
- For in-memory stores (
- Function arguments:
- For in-memory stores (
#[cached]/#[once]), must either be owned and implementHash + Eq + Clone, or aconvertexpression must be specified on the macro to produce a key of aHash + Eq + Clonetype. - For in-memory
#[concurrent_cached](sharded stores), must implementHash + Eq + Clone. The macro's default key construction always clones function arguments, soK: Cloneis required on every in-memory path. (When usingconvertto supply an already-owned key, only the store's own bounds apply:K: Hash + Eqfor unbounded/TTL-only variants,K: Hash + Eq + Clonefor LRU variants — except whenresult_fallback = trueis also set, which always requiresK: Cloneregardless 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 implementDisplay + Clone, or aconvertexpression must be used to produce a key of aDisplay + Clonetype.Cloneis needed so removal APIs can return the stored key.
- For in-memory stores (
- Arguments and return values will be
clonedin the process of insertion and retrieval. For Redis and disk stores, keys are additionally formatted intoStrings and values are de/serialized. - Macro-defined functions should not be used to produce side-effectual results!
- Macro-defined functions cannot live directly under
implblocks since macros expand to a static initialization and one or more function definitions. - Macro-defined functions cannot accept
Selftypes as a parameter.
License: MIT