NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
crates.io · #1473 most downloaded on crates.io
Generic cache implementations and simplified function memoization
Last release 1 months ago
05 Sep 2026
Ships unpredictably
gaps range from 8 days to 8 months
Rarely documented
notes for 5 of 39 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
48 releases · first in 2020
One column per quarter.
Pin the cargo-readme version CI installs by @jaemk in #321
ClaimRegistry: a single-flight guard for background refreshes by @jaemk in #323Full Changelog: v3.1.1...cached_proc_macro-v3.0.1
ShardedUnboundCache, ShardedLruCache, ShardedTtlCache, ShardedLruTtlCache,
ShardedExpiringCache, ShardedExpiringLruCache): get, remove, remove_entry, delete,
contains, and peek are all generic over the looked-up key form Q. cache.get(&k) where
k: &K (for example, for k in &keys { cache.get(&k) }) previously compiled through deref
coercion. It no longer does: Q unifies to &K first, and the call fails with the trait bound `String: Borrow<&String>` is not satisfied; same for k: &Box<K> and k: &Arc<K>, and
the same shape applies to remove, remove_entry, delete, contains, and peek. Migration:
drop the extra & (cache.get(k)), or deref explicitly (cache.get(&**boxed)). Single-owner
Cached methods (e.g. LruCache::cache_get) are unchanged.ShardHasher<K>'s key parameter is relaxed to ShardHasher<K: ?Sized>. Existing impls compile
unchanged, since this only widens what the trait accepts. But H: ShardHasher<K> no longer
implies K: Sized at a use site, which can shift inference in rare downstream generic code.BuildHasher::hash_one. The blanket ShardHasher impl for a
BuildHasher and every store's borrowed-key routing build a Hasher with build_hasher(),
hash the key and finish it, and DefaultShardHasher's hash_one override was removed so it
uses the provided default too. An overridden hash_one is no longer consulted for routing, and
the upper-32-bit distribution contract for a custom hasher applies to the Hasher returned by
build_hasher(). This matters for a BuildHasher whose hash_one dispatches on the static
type of its argument (ahash::RandomState does, on any nightly compiler): an owned newtype key
and its borrowed primitive form would otherwise route to different shards. Deliberate tradeoff:
ShardedUnboundCache, ShardedTtlCache, and ShardedExpiringCache back their shards with
HashMap<_, _, DefaultShardHasher> and std's HashMap probes via BuildHasher::hash_one, so
dropping the override means those three stores take ahash's unspecialized path on a nightly
build. Taken for correctness; the cost is nightly-only.cached::claim::{ClaimRegistry, Claim}: a single-flight claim on a key, for collapsing
concurrent refreshes of one key onto a single caller. ClaimRegistry::claim(key) returns
Some(Claim<K>) to the first caller and None to every later caller until that Claim is
dropped; the key is released from Drop, so completion, a panic, and cancellation (an
aborted async task) all release it, unlike a hand-rolled guard released at the end of the
body. This is not background refresh: the registry spawns nothing and awaits nothing, and
spawning the refresh stays with the caller, same as today. Additive, ungated (no new
dependency, no feature flag), and reachable via cached::claim:: or cached::prelude, not
the crate root, to keep Claim and ClaimRegistry out of rustc's nearest-match suggestions
for mistyped imports (design/0053).
Debug for ClaimRegistry<K> requires K: Debug + Clone: it copies the key set out from
under the registry mutex and formats after releasing it, so no user Debug code and no
writer lock is held while the registry is locked. ClaimRegistry::new already requires
K: Clone, so this only constrains generic code that names the type without building one.CacheSetMaxSize and ConcurrentCacheSetMaxSize traits, reaching set_max_size /
try_set_max_size from generic code holding a T: CacheSetMaxSize (or
ConcurrentCacheSetMaxSize) bound instead of a concrete store type. No new capability: both
methods already existed as inherent-only methods and already evicted eagerly on shrink, firing
on_evict per removed entry; this only adds a route through a generic bound. Implemented by
LruCache, LruTtlCache, ExpiringLruCache, TtlSortedCache (single-owner) and
ShardedLruCache, ShardedLruTtlCache, ShardedExpiringLruCache (sharded). Not implemented by
the unbounded stores (UnboundCache, TtlCache, ExpiringCache and their sharded forms), which
have no live capacity to resize, or by RedisCache / AsyncRedisCache / RedbCache, which have
no client-side capacity.CacheClearWithOnEvict and ConcurrentCacheClearWithOnEvict traits, reaching
cache_clear_with_on_evict from generic code the same way. Implemented by all 7 single-owner
in-memory stores and all 6 sharded stores (every in-memory store that has an on_evict
callback); not implemented by the three IO stores, which have no on_evict mechanism. The crate
doc previously described this method as inherent-only and unreachable from generic code; that
gap is now closed.get, remove, remove_entry, delete, contains, and
peek now accept any borrowed form of the key (K: Borrow<Q>), matching the single-owner
stores: sharded_cache.get("a") now works on a ShardedLruCache<String, _> without allocating
a String first. Bounded on H: ShardHasher<Q>, so a hand-written ShardHasher router keeps
these six methods at every key type it implements, not just where it also implements
BuildHasher. This relies on an unenforced contract: every ShardHasher impl on the router
must agree, shard_hash(&k) == shard_hash(k.borrow()), for K: Borrow<Q>. A router with two or
more disagreeing ShardHasher impls routes an owned insert and its borrowed lookup to different
shards, so get/peek/contains miss a present entry and remove/delete silently no-op; see
the "Contract: a router's impls must agree with each other" section of the ShardHasher docs.
set and get_or_set_with are unchanged:
they take the key by value because they insert it. See "Breaking Changes" above for the one case
this is not additive for.cached::prelude now also exports CacheSetMaxSize, CacheClearWithOnEvict,
ConcurrentCacheSetMaxSize, and ConcurrentCacheClearWithOnEvict. This cannot cause E0034 for
any store in this crate: every store implementing these traits already has set_max_size /
cache_clear_with_on_evict as an inherent method, and inherent methods take call-site priority
over trait methods before a trait is even considered. It can bite a downstream store type that
implements CacheSetMaxSize (or one of the other three traits) with no inherent method of its
own, if a same-named extension trait is also in scope: combined with use cached::prelude::*,
that yields E0034 (multiple applicable items in scope), fixable with UFCS.The sharded map stores (ShardedUnboundCache, ShardedTtlCache, ShardedExpiringCache) now
route their intra-shard probe through DefaultShardHasher instead of a shard HashMap's own
BuildHasher::hash_one. hash_one is an overridable provided method that can dispatch on the
static type of its argument (ahash::RandomState's does, on a nightly compiler that enables
ahash's specialize cfg), so an owned newtype key and its borrowed primitive form were not
guaranteed to route to the same shard. Known limitation: the single-owner stores
(UnboundCache, TtlCache, ExpiringCache, TtlSortedCache) still back their maps with
DefaultHashBuilder = ahash::RandomState by default and are not covered by this fix; on an
affected nightly compiler, work around it by building with .hasher(std::hash::RandomState::new()).
The LruCache-backed family (LruCache, LruTtlCache, ExpiringLruCache) is not affected:
LruCache indexes its entries with a HashTable<usize> probed through LruCache::hash, which
hand-builds the Hasher precisely to avoid this.
TtlSortedCache::cache_clear_with_on_evict now counts an eviction per removed entry even when
no on_evict callback is configured, matching the trait contract and the other implementors.
Previously it took an early-return fast path that cleared the store without counting anything.
Observable to anyone metering evictions on that store.
#[cached] and #[once] no longer trip clippy's clone_on_copy on rust 1.100+ (nightly at
the time of writing), where the lint also covers <T as Clone>::clone(&x) calls. It fired
whenever the cached value type is Copy (the unwrapped T of a Result/Option return, or
the whole return type otherwise), and was reported against the user's own signature:
warning: using `clone` on type `u32` which implements the `Copy` trait
--> src/lib.rs:4:28
|
4 | pub fn copy_ret(x: u32) -> u32 {
| ^^^ help: try dereferencing it: `*#[cached]`
The generated clone was spanned verbatim at the user's return type, so clippy took it for
hand-written code. The clone now resolves inside the macro expansion while keeping the return
type's location, so the Clone bound diagnostic for non-Clone return types is unchanged
(design/0043). Note that if you suppressed this
with #[expect(clippy::clone_on_copy)], remove it: the expectation is now unfulfilled, which
is itself an error under -D warnings. A plain #[allow] is harmless.
proc_macro feature. The fence
used cached::macros, which does not exist without that feature, so
cargo test --no-default-features --doc failed to compile it; CI's no-default-features row
runs --tests only, so this went uncaught there. Gated rather than marked ignore (like its
two neighboring macro fences), so it still compiles when the feature is on; the rendered
README is unchanged, since cargo-readme strips the hidden lines.docs: show expires = true with Result and Option returns in expires_per_key example by @jaemk in #269
CachedGetOrSetAsync by @jaemk in #276sync_writes_buckets, drop expiring-store struct bounds, derive ConnectionString eq, store format and doc fixes by @jaemk in #284set_max_size to sharded LRU stores, rename RedbCacheBuilder::disk_dir, add resilience example by @jaemk in #285ExpiringLruCache::retain, docs and spec fixes by @jaemk in #286SetMaxSizeError::ZeroSize to ZeroMaxSize, document redis/redb format stability, correct spec drift by @jaemk in #287Acquire load for sharded metrics().capacity, document concurrent set_max_size semantics, sync specs by @jaemk in #288Error types, add cache_contains, peek/reset aliases by @jaemk in #290cache_contains, peek-based contains parity, remove inherent TtlSortedCache::set_ttl by @jaemk in #291CacheSetError, rename TtlSortedCache insert* to set*, wrap order-method values in CacheValue by @jaemk in #293peek, builder new(), ext alias parity, concurrent cache_try_get_or_set_with, retain on map stores by @jaemk in #294cache_contains implementor listing and docsrs gate, note Arc<T> return pattern, add peek recency test by @jaemk in #295Clone error for #[cached] return types, async_core docsrs badges, doc and spec corrections by @jaemk in #296TtlSortedCache::set_with builder, ConcurrentCachePeek, ConcurrentCachedExt::try_get_or_set_with by @jaemk in #297retain on TtlSortedCache and the six sharded stores by @jaemk in #298cache_set overwrite by @jaemk in #299max_size, make cache_contains dyn-callable by @jaemk in #300on_evict, clear Clone error for #[once], no-callback retain fast path by @jaemk in #301retain returns removed count, ConcurrentCachePeekAsync, MSRV 1.92 by @jaemk in #302tag-release.sh by @jaemk in #304*Base types, split refresh-on-hit traits, fix redis key collisions by @jaemk in #305serde feature, fix stale docs by @jaemk in #306on_evict key, migration guide corrections, proc_macro alias by @jaemk in #308Full Changelog: v2.0.2...v3.0.0
This entry describes the complete 2.0.2 -> 3.0.0 delta. The ten release candidates
(3.0.0-rc.1 through 3.0.0-rc.10) are folded in here, and API that was introduced and
then changed again across the candidates is recorded only in its final shipped form; the rc
git tags remain. The upgrade is documented step by step in the
migration guide, with the mechanical breaking-change
list in the agent-oriented guide.
redb 4.x set the 1.89 floor, and the async_core feature
(enabled by async) does not compile before 1.92: the two CachedGetOrSetAsync RPIT
default bodies hit a rustc borrowck limitation (rust-lang/rust#100013). Verified by
bisection (fails on 1.89.0, 1.90.0, 1.91.0; clean on 1.92.0). Non-async feature sets built
on 1.89, but rust-version is a single crate-level value, so the floor moves for every
feature set.cached_proc_macro_types moved to edition 2024, and its version now tracks cached in
lockstep rather than a standalone 1.0.DiskCache is renamed RedbCache (naming the backend, like RedisCache) and is backed by
redb 4.x instead of the unmaintained sled, dropping the
RustSec-flagged fxhash transitive dependency. Still pure-Rust (no C toolchain). There are
no DiskCache* aliases: rename DiskCache / DiskCacheBuilder / DiskCacheError /
DiskCacheBuildError to RedbCache* at the call site. The on-disk format changed and
DISK_FILE_VERSION was bumped, so existing caches are not read and entries are recomputed.RedbCache::connection() / connection_mut(), RedbCacheBuilder::connection_config, and
the connection_config macro attribute are removed; the backend handle is not exposed.DiskCacheBuilder::sync_to_disk_on_cache_change is renamed durable and the default flipped
from false to true (fsync per write), so a disk cache persists by default. durable(false)
uses Durability::None, which can lose writes on process exit or crash; call
RedbCache::flush() / async_flush() to force a durable commit.RedbCacheBuilder::disk_directory is renamed disk_dir, matching the disk_dir attribute on
#[concurrent_cached].ShardedCache is renamed ShardedUnboundCache (with ShardedCacheBuilder ->
ShardedUnboundCacheBuilder); the old name read as the umbrella for the whole sharded family
while naming only the unbounded variant. No deprecated alias.ShardedX<K, V, H = DefaultShardHasher>, mirroring HashMap<K, V, S = RandomState>. The 2.x
ShardedCacheBase pattern is gone: there is no ShardedUnboundCacheBase, ShardedLruCacheBase,
ShardedTtlCacheBase, ShardedLruTtlCacheBase, ShardedExpiringCacheBase, or
ShardedExpiringLruCacheBase. Migration is a mechanical rename dropping Base.cached::TimedEntry is now pub(crate), and the store() accessors on UnboundCache,
TtlCache, LruTtlCache, and ExpiringLruCache are removed. They exposed the internal
backing map and leaked the internal entry wrapper; use the public Cached API instead.get, set, remove, remove_entry, clear, len, is_empty,
delete, try_set, contains, hits, misses, metrics, and the short get_or_set_with
family) moved off Cached / ConcurrentCached onto the blanket extension traits CachedExt /
ConcurrentCachedExt. The core traits keep only the cache_-prefixed methods, so a custom
store implements a smaller surface. Callers using cached::prelude::* need no change; others
add use cached::CachedExt; / use cached::ConcurrentCachedExt;, or use the cache_ names.
Custom impl Cached / impl ConcurrentCached blocks must drop any short-alias methods.ConcurrentCachedAsync's cache operations carry an async_ prefix (async_cache_get,
async_cache_set, async_cache_remove, async_cache_remove_entry, async_cache_delete),
removing the E0034 "multiple applicable items" error when both concurrent traits are in scope.type Error, cache_size,
cache_is_empty) lives on ConcurrentCacheBase, the supertrait of both concurrent traits;
the global-TTL controls (ttl, set_ttl, try_set_ttl, unset_ttl) live on
ConcurrentCacheTtl, implemented only by the TTL-capable concurrent stores. len is removed
from the base trait as a duplicate of cache_size. Custom impls must move type Error (and
any size override) into an impl ConcurrentCacheBase block and TTL behavior into
impl ConcurrentCacheTtl.refresh_on_hit / set_refresh_on_hit live on their own CacheRefreshOnHit and
ConcurrentCacheRefreshOnHit traits rather than on CacheTtl / ConcurrentCacheTtl.
CacheRefreshOnHit is implemented by TtlCache and LruTtlCache;
ConcurrentCacheRefreshOnHit by RedisCache, AsyncRedisCache, RedbCache,
ShardedTtlCache, and ShardedLruTtlCache. TtlSortedCache implements neither: its
deadline-ordered index cannot refresh an entry's expiry on read, and its 2.x
set_refresh_on_hit was a no-op that discarded its argument. Both new traits are in the
prelude. The inherent refresh_on_hit / set_refresh_on_hit on TtlCache and LruTtlCache
are removed (they shadowed the trait methods and the setter returned ()), as are the
inherent TTL controls on the sharded TTL stores and TtlSortedCache::set_ttl: runtime TTL
control is trait-only.CacheTtl and CacheEvict are single-owner (&mut self) traits only, since &mut self is
unusable on a store held through Arc/static. Concurrent stores set TTL through
ConcurrentCacheTtl::set_ttl (&self) and evict through the new ConcurrentCacheEvict
(fn evict(&self) -> usize).Cached and ConcurrentCacheBase gained an associated type Error, bounded by
std::error::Error + Send + Sync + 'static. Every built-in in-memory store (UnboundCache,
LruCache, TtlCache, LruTtlCache, TtlSortedCache, ExpiringCache, ExpiringLruCache,
and the six sharded stores) is infallible: type Error = std::convert::Infallible. A TTL that
would overflow Instant bounds stores the entry with no expiry instead of failing, so
cache_try_set no longer has a dedicated error type; the 2.x TtlSortedCacheError and the
boxed Box<dyn std::error::Error> return are both gone.Cached::cache_get_or_set_with / cache_try_get_or_set_with (and their aliases) return
&V / Result<&V, E> instead of &mut V (#179). The new *_mut variants
(cache_get_or_set_with_mut, cache_try_get_or_set_with_mut, and the async spellings)
preserve the mutable-reference behavior. External impls must update their signatures and
implement the new required *_mut methods.CachedAsync trait is renamed CachedGetOrSetAsync, naming the job it actually does
(memoizing an async closure over a synchronous in-memory Cached store). Its four sync
passthroughs (async_cache_get / async_cache_set / async_cache_remove /
async_cache_clear) and the misleading Self: Cached bound are removed, and its get-or-set
methods use the async_cache_* namespace (async_cache_get_or_set_with,
async_cache_try_get_or_set_with, and their _mut variants).cache_clear / cache_reset on ConcurrentCached
(and the async counterparts), whose 2.x no-op Ok(()) defaults silently did nothing;
cache_peek_with_expiry_status on CloneCached / ConcurrentCloneCached, whose defaults
returned a wrong result that silently broke force_refresh + result_fallback; and
cache_contains / async_cache_contains on ConcurrentCached / ConcurrentCachedAsync,
which carry no V: Clone bound so contains works for non-Clone values.
ConcurrentCached::cache_contains has no where Self: Sized bound and is dyn-callable.SerializeCached::cache_set_ref and SerializeCachedAsync::async_cache_set_ref return
Result<(), Self::Error> instead of Result<Option<V>, Self::Error>, removing a per-write
read-and-decode round trip on the IO stores. Call cache_get first if you need the prior value.ShardHasher requires Clone as a supertrait, and any thread-safe std::hash::BuildHasher
now implements it through a blanket impl, so std::hash::RandomState and ahash::RandomState
are accepted directly by the sharded builders' .hasher(...). DefaultShardHasher implements
BuildHasher and reaches ShardHasher through that one blanket path, which also makes it
usable with HashMap::with_hasher and LruCacheBuilder::hasher. A type cannot implement both
BuildHasher and a hand-written ShardHasher (coherence rejects the pair), so a custom
shard-routing hasher must not implement BuildHasher.Expires::expires_at returns crate::time::Instant (web-time backed, correct under wasm)
instead of std::time::Instant, and CloneCached::cache_get_with_expiry_status requires
V: Clone, matching its peek sibling.cache_set over an existing key promotes that key to most-recently-used on LruCache,
LruTtlCache, ExpiringLruCache, ShardedLruCache, ShardedLruTtlCache, and
ShardedExpiringLruCache. In 2.x the value was replaced in place and the entry kept its
position, so this changes which entry a capacity eviction selects in overwrite-heavy
workloads. It also resolves a divergence where configuring an on_evict callback changed
eviction order on two sharded stores. cache_peek, cache_peek_with_expiry_status, and
cache_contains remain non-promoting; inserting a new key is unchanged. No public API writes
a value without touching recency.on_evict receives the displaced entry's own stored key rather than the caller's Eq-equal
instance, on every store and every removal path, matching HashMap::insert. Observable only
for key types whose Eq/Hash ignore part of the payload.on_evict, on every removal path (evict,
retain, cache_remove / cache_remove_entry, lazy expiry sweeps, cache_set over an
expired entry, capacity evictions, and the get-or-set families), so a panicking callback can
no longer remove an entry without counting it.retain returns usize (the number of entries removed) instead of () on all 13 stores that
have it. The count includes entries the predicate rejected and, on the expiry-aware stores,
entries removed for having expired regardless of the predicate. This diverges from
HashMap::retain deliberately, because this retain does strictly more than filter, and it
matches TtlSortedCache::retain_latest. There is no #[must_use], so existing
cache.retain(...); statements keep compiling; only call sites binding the result as ()
need a discard.set_max_size returns Option<usize> (the previous bound) on LruCache, LruTtlCache, and
ExpiringLruCache, unifying the return type with TtlSortedCache.ShardedLruCache,
ShardedLruTtlCache, ShardedExpiringLruCache) is capped by the requested max_size
instead of derived solely from available_parallelism(): on the default path the count is
next_power_of_two(max_size / 16).clamp(1, default_shard_count()). This changes the
observable capacity(), shards(), and shard_sizes() for small caches on high-core-count
hosts (ShardedLruCache::new(100) resolves to 8 shards / capacity 128, where a 64-core box
previously produced 256 shards / capacity 4096). An explicit .shards(n) is authoritative;
the per_shard_max_size path and the unbounded stores keep default_shard_count().ShardedTtlCache and ShardedLruTtlCache decide expiry against a clock sample taken before
the shard lock is acquired, so an entry that crosses its expiry while the caller queues for
the lock is judged live. This stays within the documented lazy-expiry contract, which makes
no promise of prompt removal.set_ttl applies to future inserts only; existing
entries keep their computed expiry, and refresh_on_hit recomputes expiry from the current
TTL at access time. A zero Duration passed to any set_ttl surface means "expiry disabled",
exactly equivalent to unset_ttl(): it no longer panics on the sharded stores and no longer
means "expire immediately" on TtlSortedCache. build() still rejects a zero TTL, and
try_set_ttl(0) still returns SetTtlError::ZeroTtl. For the redis stores a disabled TTL
writes keys without expiry (a plain SET) and the refresh path issues no EXPIRE.TtlSortedCache gains a set family in place of insert / insert_ttl / insert_evict /
insert_ttl_evict: set(k, v) plus the set_with(k, v) entry-setter builder, which chains
.ttl(Duration) / .ttl_secs(n) / .ttl_millis(n) for a per-entry override and .evict()
for the post-insertion sweep before the terminal .set() -> Option<V>. TtlSortedSetBuilder
is re-exported from the crate root.TtlSortedCache's get-or-set family no longer removes an expired entry before running the
initializer, so a cancelled or panicking initializer leaves the expired entry in place and
fires no on_evict; on success on_evict fires after the initializer. All four variants now
agree with each other and with TtlCache / LruTtlCache.iter_order / value_order on LruCache, LruTtlCache, and ExpiringLruCache return
CacheValue-wrapped values (Vec<(K, CacheValue<V, M>)> and Vec<CacheValue<V, M>>), one
shape across the LRU family. M is per-entry metadata: () for LruCache /
ExpiringLruCache, Option<Instant> for LruTtlCache (read through CacheValue::expires_at).
LruTtlCache no longer leaks bare (Option<Instant>, V) tuples. key_order is unchanged.cache_reset (and the concurrent counterparts) no longer preserves the preallocated backing
capacity: it clears and shrinks to initial_capacity, so later inserts may reallocate.
Recreate the cache instead of resetting it to retain the allocation.copy_from returns Result<_, BuildError> instead of panicking on invalid
configuration, and the Eq marker impls for UnboundCache and LruCache require V: Eq.get / set / remove / remove_entry / delete /
reset / contains / peek returning unwrapped values, so store.get(&k) is Option<V>
rather than Result<Option<V>, Infallible>. These take call-site priority over the
ConcurrentCached* trait methods, which return Result<_, Self::Error>; note that
s.set(k, v).unwrap() therefore compiles as Option::unwrap and panics on a first insert.
Use the cache_-prefixed trait methods, or UFCS, for the Result shape.capacity(n) is renamed initial_capacity(n) on UnboundCacheBuilder, TtlCacheBuilder,
TtlSortedCacheBuilder, and ExpiringCacheBuilder, where it pre-allocates without bounding
entry count. The name was ambiguous next to max_size(n) on the LRU builders.RedbCache::builder(name), RedisCache::builder(prefix), and AsyncRedisCache::builder(prefix)
take the primary required field as a positional argument. The 2.x ::new( entry points on
these three types are removed (they returned a builder, conflicting with the convention that
new() returns a ready store); the in-memory and sharded stores gained real
Type::new() / Type::new(required_field) constructors.LruTtlCacheBuilder and ShardedLruTtlCacheBuilder take the hasher in the third generic slot
and the eviction typestate marker last: LruTtlCacheBuilder<K, V, S = DefaultHashBuilder, E = NoEvict> and ShardedLruTtlCacheBuilder<K, V, H = DefaultShardHasher, E = NoEvict>.
LruTtlCacheBuilder had no hasher parameter in 2.x, so a 2.x annotation of
LruTtlCacheBuilder<K, V, HasEvict> names the hasher slot in 3.0 and must gain the hasher as
the third argument. Code naming only <K, V>, or reaching the hasher through .hasher(..),
is unaffected..ttl(...) stores keys without expiry. A TTL that is set
must be greater than zero, and RedisCacheBuildError::MissingRequired("ttl") is no longer
returned.RedisCacheBuilder::build() / AsyncRedisCacheBuilder::build() reject an empty prefix with
Build(BuildError::InvalidValue { field: "prefix", .. }). The prefix is what scopes
cache_clear to one logical cache; with an empty prefix, cache_clear matched
<namespace>:* and deleted the entries of every cache sharing the namespace.RedbCacheBuilder::build() validates cache_name as a filename component: empty, path
separators, path-traversal components, and any character invalid in a cross-platform filename
(: < > " | ? *, or an ASCII control byte) are rejected rather than silently
creating subdirectories or escaping the cache directory.refresh_on_hit: the refresh() alias is removed from
the in-memory TTL builders, and the redis/redb builders' refresh is renamed. The
#[cached(refresh = true)] attribute is unchanged.BuildError::InvalidTtl { ttl } is removed; a zero TTL at build time yields
BuildError::InvalidValue { field: "ttl", reason: "must be greater than zero" }.
RedisCacheBuildError::InvalidTtl and RedbCacheBuildError::InvalidTtl become
Build(BuildError), wrapping the inner error instead of duplicating it.Error suffix:
RedbCacheError::{StorageError, CacheDeserializationError, CacheSerializationError} became
{Storage, CacheDeserialization, CacheSerialization}; RedbCacheBuildError::ConnectionError
became Storage; RedisCacheError::{RedisCacheError, PoolError, CacheDeserializationError, CacheSerializationError} became {Redis, Pool, CacheDeserialization, CacheSerialization}.
RedbCacheError / RedbCacheBuildError are struct variants (named fields) matching the redis
enums, and CacheDeserialization carries a cached_value: Vec<u8> field.RedbCacheError, RedbCacheBuildError, RedisCacheError,
RedisCacheBuildError, BuildError, SetTtlError, SetMaxSizeError) are #[non_exhaustive],
so external matches need a wildcard arm.redis::, r2d2::, or redb:: types through
public fields or blanket From impls. Foreign causes are boxed behind
Box<dyn std::error::Error + Send + Sync> and read through source(), so a backing-crate
version bump is no longer a breaking change to these enums.Return<T>::value and Return<T>::was_cached are private fields
(cached_proc_macro_types). Use *r / r.into_inner() for the value and r.was_cached()
for the flag; struct pattern matches must switch to the accessors.CacheMetrics.size is renamed entry_count and is now Option<usize>, reporting None for
stores whose size is unknown (redis/redb) instead of a false 0. CacheMetrics is
#[non_exhaustive] and derives Default, so construct it by mutating
CacheMetrics::default() rather than with a struct literal.RedbCache::remove_expired_entries returns Result<usize, RedbCacheError> (the number
removed) instead of Result<(), RedbCacheError>, matching the evict traits.ttl attribute takes three mutually exclusive forms: ttl_secs = N (whole seconds,
replacing the 2.x bare-integer ttl = N), ttl_millis = N (milliseconds, new), and
ttl = "<Duration expr>" (a string-literal Duration expression). The old bare-integer form
produces an error directing you to ttl_secs. Builders gained matching .ttl_secs(n) /
.ttl_millis(n) methods (#149).size attribute is removed from #[cached] / #[concurrent_cached] (use
max_size = N; the macros detect size and emit a directed error), and the unbound
attribute is removed from #[cached] (a bare #[cached] already builds an UnboundCache).#[cached(refresh = true)] without a TTL is a compile error; it was previously ignored.
#[cached] also rejects result_fallback combined with with_cached_flag, and rejects an
explicit sync_writes_buckets when sync_writes is not "by_key" (the value was accepted
and silently ignored).#[cached] / #[once] reject the concurrent-store-only attributes (disk, redis,
map_error, shards, durable, disk_dir, cache_prefix_block) with a targeted redirect
to #[concurrent_cached], and #[once] rejects the #[cached]-only attributes
(result_fallback, refresh, max_size, ty, create, key, convert, sync_lock,
unsync_reads, sync_writes_buckets). #[concurrent_cached] rejects a custom ty without a
create block on the redis and disk paths, an async closure for map_error, and
cache_prefix_block on the disk path (it is redis-only).name starting with __cached (the prefix reserved for generated
bindings) and validate name as a Rust identifier.#[concurrent_cached]'s refresh attribute is a plain bool (was Option<bool>), so
refresh = false no longer conflicts with expires or a create block.redis_tokio and redis_smol enable the TLS-agnostic
connection path, so add redis_tokio_native_tls / redis_tokio_rustls (or the redis_smol
equivalents) to restore TLS. redis_connection_manager and redis_async_cache are
capability features depending only on redis/aio, so they are runtime-agnostic and must be
paired with a runtime feature; the connection manager is a per-cache .connection_manager(true)
opt-in rather than a feature that cfg-swapped every cache's connection type.disk_store feature is renamed redb_store. The wasm feature is removed (it gated
nothing; web-time provides wasm-compatible time types transparently), as are redis_ahash
and async_tokio_rt_multi_thread.async feature no longer implies tokio; it pulls only async-lock, and
cached::async_sync::{Mutex, RwLock, OnceCell} re-export from async-lock instead of
tokio::sync (OnceCell there has no const_new()). Async RedbCache runs blocking redb
work on the blocking crate's thread pool instead of tokio::spawn_blocking, and
RedbCacheError::BackgroundTaskFailed is removed. blocking is pulled by redb_store rather
than async, so redis-only and in-memory async builds do not pay for it.dep: syntax, so an optional dependency's name
is no longer silently usable as a feature; enable the named crate feature instead.rmp-serde) instead of JSON. Old 2.x JSON
entries are read transparently and rewritten as MessagePack on their next write. Redis TTLs
use PSETEX / PEXPIRE, so sub-second TTLs are honored to the millisecond (requires
Redis 2.6+).: -> %3A, % -> %25) and the key always has
three fields ({namespace}:{prefix}:{key}), so distinct triples always map to distinct keys;
an unescaped join previously let namespace="a:b", prefix="" collide with namespace="a", prefix="b". An empty prefix keeps its separator: ("ns", "", "k") encodes as ns::k. This
changes the on-wire key for any segment containing : or %; the value envelope's version
field does not cover key layout, so an old entry is not found after upgrading, is recomputed
and rewritten at the new key, and the old entry expires on its original TTL.RedisCache::connection_string() / AsyncRedisCache::connection_string() return a
ConnectionString newtype whose Display and Debug redact credentials; call .reveal()
for the raw URL.ahash feature enables ahash/runtime-rng on non-wasm targets, seeding hash maps from
the OS RNG instead of a compile-time seed (hash-flood resistance). wasm32 keeps the
compile-time seed. No source change required.cached::prelude re-exports the common traits plus the CacheMetrics struct for a single
glob import.UnboundCache, LruCache, TtlCache,
LruTtlCache, TtlSortedCache, ExpiringCache, and ExpiringLruCache gained a hasher type
parameter defaulted to DefaultHashBuilder and a .hasher(s) builder method, mirroring the
sharded stores. DefaultHashBuilder is re-exported from the crate root.Builder::new() on all 13 in-memory and sharded builders, matching the IO builders'
public constructors.CacheValue<V, M = ()>: the value-plus-metadata wrapper returned by the LRU-family order
methods, re-exported from the crate root. Deref<Target = V>, PartialEq<V> against bare
values, Display where V: Display, value() / into_value(), and expires_at() when
M = Option<Instant>. IntoValues::into_values() bulk-unwraps an iter_order() /
value_order() result back into a plain Vec<V>. The reverse comparison
bare_value == wrapped cannot be implemented: coherence forbids the blanket impl.retain(keep) across every in-memory store: UnboundCache, LruCache, TtlCache,
LruTtlCache, TtlSortedCache, ExpiringCache, ExpiringLruCache, and the six sharded
stores. Every removed entry fires on_evict; on the expiry-aware stores expired entries are
removed regardless of the predicate and every removal counts an eviction. The sharded form
locks one shard at a time (not atomic across shards), runs the predicate under the shard write
lock (so it must not re-enter the cache), fires on_evict after the lock is released, and
requires no K: Clone bound.ConcurrentCachePeek and ConcurrentCachePeekAsync: side-effect-free cache_peek /
async_cache_peek (plus peek / async_peek aliases) for concurrent stores, with no
recency promotion, no TTL refresh, no hit/miss metrics, and no lazy removal of expired
entries. Implemented by the six sharded stores, which also expose an inherent
peek(&self, &K) -> Option<V>. RedisCache, RedbCache, and AsyncRedisCache implement
neither: peek is an in-memory concept, and for an IO-backed store there is no client-side
state to skip while the operation remains a full round trip. Both traits are in the prelude.ConcurrentCachedAsyncExt, a blanket extension trait over ConcurrentCachedAsync with ten
async_-prefixed aliases (async_get, async_set, async_remove, async_remove_entry,
async_delete, async_contains, async_clear, async_reset, async_get_or_set_with,
async_try_get_or_set_with), mirroring ConcurrentCachedExt. In the prelude.ConcurrentCached::cache_try_get_or_set_with and its async counterpart (both provided):
fallible-init get-or-set returning Result<Result<V, E>, Self::Error>, store error outer,
closure error inner; nothing is stored on a closure Err.
ConcurrentCachedExt::try_get_or_set_with is the short alias. ConcurrentCached /
ConcurrentCachedAsync also gained defaulted cache_get_or_set_with (get-then-set,
non-atomic) and no-op-default cache_reset_metrics.ConcurrentCacheBase gained cache_hits / cache_misses /
cache_capacity / cache_evictions and a default metrics(), so a generic bound can read a
sharded store's metrics; CachedExt gained capacity / evictions / reset;
ConcurrentCachedExt gained len / is_empty / hits / misses / capacity /
evictions / clear / reset; CachedPeek::peek, CloneCached::peek_with_expiry_status,
and ConcurrentCloneCached::{get_with_expiry_status, peek_with_expiry_status} fill in the
remaining aliases.Cached::cache_contains (defaulted, get-based, overridden peek-based by the built-ins) and
inherent contains on the six sharded stores, giving contains both spellings on both trait
families. CachedExt::contains delegates to it, so contains no longer counts a hit/miss,
promotes recency, or refreshes TTL, and reports expired entries as absent.ExpiringLruCache::iter_order / key_order / value_order, completing LRU-family
introspection parity, and TtlSortedCache::capacity() -> Option<usize>.set_max_size(&self, usize) -> Option<usize> and try_set_max_size(&self, usize) -> Result<Option<usize>, SetMaxSizeError>.
Shrinking evicts LRU-excess entries per shard strictly by recency, fires on_evict, and
counts evictions; resize is not atomic across shards. LruCache, LruTtlCache, and
ExpiringLruCache gained the same pair (#180), and SetMaxSizeError (variants
ZeroMaxSize and CapacityOverflow) replaces the mix of BuildError and std::io::Error
the 2.x resize paths returned. CacheTtl::try_set_ttl is the matching strict TTL setter,
returning SetTtlError::ZeroTtl.per_shard_initial_capacity on the three unbounded sharded builders, the sharded counterpart
of initial_capacity.SerializeCached / SerializeCachedAsync with cache_set_ref / async_cache_set_ref,
implemented by RedisCache / AsyncRedisCache / RedbCache, letting serialize-backed stores
set an entry without taking ownership. #[concurrent_cached] calls the borrowed setter for
any store implementing them, avoiding a value clone per set (#196, #195).RedisCache / AsyncRedisCache implement cache_clear / async_cache_clear through a
namespace-scoped SCAN + batched DEL (O(n), scoped to the cache's prefix, not a server
flush), with glob metacharacters in the namespace/prefix escaped so they match literally
(#200). RedisCache and AsyncRedisCache also implement Clone; RedbCache does not.RedbCache::flush / async_flush force a durable commit, RedbCache::disk_path() returns
the backing file path, and RedbCache::async_remove_expired_entries runs the sweep on the
blocking thread pool so it is usable from async contexts.RedisCacheError / RedbCacheError and their build-error siblings expose
is_deserialization() -> bool, so callers can distinguish a codec failure from a storage or
network error without a full match. Debug is implemented for RedisCache,
AsyncRedisCache, and RedbCache, redacted to namespace/prefix/path/ttl/refresh.PartialEq / Eq for ExpiringCache and ExpiringLruCache, and PartialEq / Eq / Hash
for ConnectionString. NoEvict / HasEvict derive Clone, Copy, Debug, Default and
are documented at the crate root.Expires::expires_at(&self) -> Option<Instant> as a default method returning the value's
expiry instant when tracked. Advisory only: is_expired() remains the authoritative liveness
check, and existing impl Expires blocks get the default for free.force_refresh (a block expression over the arguments that bypasses the
cached value, #146), in_impl = true for methods inside impl blocks including self
receivers (#16, #140), companions_vis = "<vis>" to set the generated companions'
visibility, companions = false to suppress the {fn}_no_cache / {fn}_prime_cache
companions entirely, and ttl_millis (above). convert, create, force_refresh,
map_error, and cache_prefix_block accept unquoted Rust in addition to the quoted-string
form. map_error is optional on the disk and redis paths (the generated code uses
.map_err(Into::into)?, so E: From<RedbCacheError> / From<RedisCacheError>).
#[concurrent_cached] accepts result_fallback together with expires.#[cached] / #[concurrent_cached] accept reference arguments (&T,
Option<&T>) on the default-key path, deriving an owned key without a convert (#202,
#203); the crate root is resolved via proc-macro-crate, so a renamed or re-exported
cached dependency works (#157); macro-introduced bindings are hygienically named
__cached_*, so arguments named key, cache, or result no longer collide (#230,
#114); and a generic function without key + convert produces a clear error (#80).#[cached] / #[once] / #[concurrent_cached] on an
async fn without the async feature, a TTL attribute without time_stores, and
#[concurrent_cached(disk = true)] / (redis = true) without redb_store / a redis feature
all name the missing feature instead of surfacing errors from generated internals. A return
type that does not implement Clone produces exactly one error, spanned at the return type.#[doc(alias)] entries mapping the 2.x store names to their 3.0 types (SizedCache ->
LruCache, TimedCache -> TtlCache, TimedSizedCache -> LruTtlCache) for docs.rs search.bin/check-versions.sh
fails the release when a cached_proc_macro* dependency pin disagrees with that subcrate's
version, or when a stable cached would depend on a pre-release subcrate.sync_writes = "by_key" bucket selection seeds from a per-static RandomState instead of a
fixed-seed hasher, so an attacker who knows the key space cannot collapse the lock buckets to
force whole-cache serialization.cache_get path self-heal by default:
the entry is deleted and the call returns a miss so the cached function recomputes. Opt into
fail-closed behavior with .strict_deserialization(true).resolve_connection_string() returns a redacting
ConnectionString, the build path constructs sanitized synthetic errors (including the
r2d2 pool-build failure and the NotUnicode env-var value, which is the connection string
itself), and RedisCacheBuilder::connection_pool_connection_timeout bounds how long build
waits for a connection. The legacy-JSON backward read requires the exact version field value,
and client-side caching rejects a URL pinning RESP2 (which cannot deliver invalidation
messages, so accepting it would silently serve stale data).0700 and the database file
forced to 0600 on every open (not only at creation); a symlink at the resolved db path or
at a configured cache directory is rejected before opening; symlink and permission validation
runs for the XDG default candidates, not only the temp fallback; and a read-only or
group/world-writable candidate falls back to the temp directory.RedbCacheError::CacheDeserialization / RedisCacheError::CacheDeserialization render their
cached_value bytes as <N bytes redacted> in Debug, and are documented as potentially
sensitive.{fn}_prime_cache no longer deadlocks or blocks readers: it ran the function body while
holding the cache write lock, so a recursive prime re-locked the same static on the same
thread (parking_lot is non-reentrant) and any prime blocked every reader for the full
recompute. The body now runs before the lock is taken.#[cached(result_fallback = true)] no longer overwrites a newer cached value with a stale
one. The fallback was captured before the function body ran and written back unconditionally
on Err, so a slow failing call could clobber a value a concurrent call had refreshed, and on
a TTL store refresh its deadline. The fallback is now read under the same lock the write takes;
result_fallback rejects a non-disabled sync_writes, so no caller could serialize the
window themselves.TtlCache, LruTtlCache, and TtlSortedCache; several paths anchored before the factory, so
a factory slower than the TTL produced an already-stale entry. Refreshing an entry under an
overflowing TTL now clears the deadline, as a fresh insert already did.on_evict or counts an eviction
until the replacement factory succeeds; overwriting an expired entry fires on_evict and
counts uniformly across the timed and sharded stores; a panicking on_evict during capacity
eviction can no longer leave the cache over capacity; cache_clear_with_on_evict counts every
removed entry rather than degrading to a silent cache_clear without a callback; and
cache_remove samples expiry once, at removal, so a slow callback cannot turn a live entry
into a None return.retain / evict previously fired on_evict inside the selection scan
or removed entries eagerly while the user predicate ran, so a panicking predicate could remove
nothing while having already run cleanup callbacks, or silently drop every entry already
yielded. The sharded implementation records a Vec<bool> of decisions rather than cloned keys,
so no K: Clone bound is added.TtlSortedCache::set_and_get_mut no longer orphans a map row when the size trim it triggers
unwinds: the stamp was unlinked from the deadline index and re-inserted after the trim, so a
panic in between left the entry in the map but invisible to every index-driven sweep while
still counted by cache_size(). TtlSortedCache::set_with(..).evict() also performs the
expiry sweep when max_size is configured and the map is under the bound, where the opt-in
was previously discarded, and build reserves with try_reserve so a capacity-overflowing
max_size returns Err(BuildError) instead of aborting.RedisCache::cache_clear / async_cache_clear decode SCAN replies as bytes rather than
String. Redis keys are binary-safe, so a single non-UTF-8 key anywhere in the cache's scope
aborted the clear permanently: the offending key was never removed, so every retry failed
identically.RedbCache::cache_set no longer returns a displaced value that had already expired, and
RedbCacheBuilder::build() returns RedbCacheBuildError::Storage instead of panicking when
the backing file is damaged. A truncated tail is the ordinary result of a full disk or a
killed process, and the file is a disposable cache, so it must not take the application down.
(This cannot help under panic = "abort".)remove_expired_entries re-read and re-check inside the write transaction, and use a single
time snapshot for the scan and write passes; redb self-heal re-reads before deleting; and the
redis self-heal delete is conditional through a Lua script comparing stored bytes, so a
concurrent valid write racing the read is not discarded.RedbCache default-directory resolution self-heals a pre-existing cache directory created
with legacy permissions by an earlier version, which permanently failed the security
validation. The chmod only succeeds for the owner, so an attacker-owned or symlinked
directory still falls through to the next candidate.ShardedTtlCache,
ShardedLruTtlCache, ShardedExpiringCache, and ShardedExpiringLruCache previously
evaluated a displaced entry's expiry outside the lock or twice, so a value crossing the
threshold in that window fired on_evict without counting the eviction or produced a wrong
return value. deep_clone on the expiring sharded stores reads the hit/miss counters under
the shard read lock, so cloned metrics match cloned entries.LruCache::cache_reset uses a fallible allocation path (a grown max_size could request a
HashMap capacity past the allocation limit and panic), and internal LRU list pre-allocation
saturates instead of overflowing.#[once] generic-value-type guard compares whole idents, so
fn f<S: Into<String>>(..) -> String is no longer falsely rejected; a raw-identifier cache
name (e.g. r#type) builds a working static instead of panicking; attributes written
between the macro and the fn forward to every generated item, so #[cfg] gating stays in
lockstep; user lint attributes reach the generated *_prime_cache companion; and no generated
use places a name in a scope enclosing user code, so a user item named Cached or
CloneCached is no longer shadowed.key / convert / force_refresh values produce contextual errors explaining the
expected syntax instead of a bare syn "unexpected token".RedbCacheError, RedbCacheBuildError, RedisCacheError, and RedisCacheBuildError
Display output includes the underlying cause, which was previously reachable only through
Debug while the source type is documented as not public API.ConcurrentCacheTtl::refresh_on_hit reflects the configured flag: the concurrent stores
overrode only set_refresh_on_hit, so the getter always reported false through trait
dispatch.Cached for HashMap no longer requires S: Default, so HashMap<K, V, DefaultHashBuilder>
implements Cached on wasm.doc(cfg)) on the async_core-gated impls and on
AsyncRedisCacheBuilder::client_side_caching, which previously rendered as unconditionally
available.OnceLock) instead of probing it on every
construction. The LRU-family and expiry-aware stores sweep in one pass instead of collecting
keys first, the TTL stores sample the clock once per operation instead of once per entry
examined, and ExpiringCache is smaller per instance.get_or_set_with returning V directly, so the common case
needs no trait import or .unwrap().#[must_use] is applied across the pure-query trait methods (cache_size / len /
is_empty / metrics / hits / misses / ttl / refresh_on_hit / ...), the removal
methods on the concurrent traits, CacheEvict::evict / ConcurrentCacheEvict::evict,
CacheMetrics::hit_ratio, the order accessors, and the sharded builders. The short
remove / remove_entry aliases and the inherent sharded set / remove are deliberately
left un-annotated: on the inherent methods the attribute cannot fire on .unwrap() (which
consumes the value) and would fire on correct fire-and-forget calls.Return::set_was_cached is #[doc(hidden)] (macro plumbing); it remains pub and callable.KeyedCache moved under a #[doc(hidden)] pub mod __private, so it no longer appears as a
suggested import when a user references a removed legacy store name.hashbrown updated to 0.17 (internal). Dev-only: criterion 0.8, googletest 0.14.[lints] table, so a future-toolchain warning
firing in cached cannot break downstream builds, and specs/, local/, .cursorrules, and
Makefile are excluded from the published package.len / cache_size / iter / evict contract on lazy-eviction stores is documented in
one place: len returns the stored count without an expiry scan (so it may include expired
entries), iter omits expired entries from the view without removing them, and evict()
reclaims them and yields an accurate live count..unwrap() sharp edge.REDIS_VALUE_VERSION) and the redb
on-disk format (versioned file name, table name) are documented as stable for the 3.x series
on the store struct docs; changes bump the embedded version and are reserved for a major
release.Arc<T> return pattern for expensive-to-clone values is documented on the macros: the
cache stores the Arc, and hits clone only the pointer (#64).examples/resilience.rs covering sync_writes = "by_key",
result_fallback, and force_refresh, plus cache-invalidation (#21) and struct-method
(#236) examples.feat: bound trait Error types, add cache_contains , peek / reset aliases by @jaemk in #290
Error types, add cache_contains, peek/reset aliases by @jaemk in #290cache_contains, peek-based contains parity, remove inherent TtlSortedCache::set_ttl by @jaemk in #291CacheSetError, rename TtlSortedCache insert* to set*, wrap order-method values in CacheValue by @jaemk in #293peek, builder new(), ext alias parity, concurrent cache_try_get_or_set_with, retain on map stores by @jaemk in #294cache_contains implementor listing and docsrs gate, note Arc<T> return pattern, add peek recency test by @jaemk in #295Clone error for #[cached] return types, async_core docsrs badges, doc and spec corrections by @jaemk in #296TtlSortedCache::set_with builder, ConcurrentCachePeek, ConcurrentCachedExt::try_get_or_set_with by @jaemk in #297retain on TtlSortedCache and the six sharded stores by @jaemk in #298cache_set overwrite by @jaemk in #299max_size, make cache_contains dyn-callable by @jaemk in #300on_evict, clear Clone error for #[once], no-callback retain fast path by @jaemk in #301retain returns removed count, ConcurrentCachePeekAsync, MSRV 1.92 by @jaemk in #302Full Changelog: cached_proc_macro_types-v3.0.0-rc.8...v3.0.0-rc.10
docs: show expires = true with Result and Option returns in expires_per_key example by @jaemk in #269
CachedGetOrSetAsync by @jaemk in #276sync_writes_buckets, drop expiring-store struct bounds, derive ConnectionString eq, store format and doc fixes by @jaemk in #284set_max_size to sharded LRU stores, rename RedbCacheBuilder::disk_dir, add resilience example by @jaemk in #285ExpiringLruCache::retain, docs and spec fixes by @jaemk in #286SetMaxSizeError::ZeroSize to ZeroMaxSize, document redis/redb format stability, correct spec drift by @jaemk in #287Acquire load for sharded metrics().capacity, document concurrent set_max_size semantics, sync specs by @jaemk in #288Full Changelog: v2.0.2...v3.0.0-rc.8
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
The old size = N spelling keeps working as a deprecated alias that emits a deprecation warning (anchored at the size token). Setting both on one annot…
Upgrading from 1.1? See the 2.0 migration guide.
cached. Consumers already on Rust ≥ 1.85 are unaffected; those on 1.80–1.84 must update their toolchain. (The repository's rust-toolchain.toml pins the latest stable for local development and CI only — that pin does not propagate to consumers.)Cached::cache_remove_entry<Q>(&mut self, k: &Q) -> Option<(K, V)>: new required method on the Cached trait that removes an entry and returns the stored key and value. Unlike cache_remove, this returns Some even when the deleted entry was already expired, making it possible to distinguish "key absent" from "key present but expired". Always fires the store's on_evict callback (if set).ConcurrentCached::cache_remove_entry(&self, k: &K) -> Result<Option<(K, V)>, Self::Error>: same semantics on the concurrent trait; implemented for all nine concurrent stores (six sharded plus DiskCache / RedisCache / AsyncRedisCache). The seven non-sharded stores (UnboundCache, LruCache, etc.) gain cache_remove_entry via the Cached trait above.Cached::cache_delete<Q>(&mut self, k: &Q) -> bool: new default method on Cached that deletes an entry without returning it; returns true if an entry was physically removed (including expired entries), false if the key was absent. Implemented via cache_remove_entry.DiskCache and RedisCache / AsyncRedisCache now require K: Clone (in addition to existing bounds) for their ConcurrentCached / ConcurrentCachedAsync impls, which is needed to return the stored key from cache_remove_entry.ConcurrentCached / ConcurrentCachedAsync mutators now take &self instead of &mut self: set_refresh_on_hit, set_ttl, and unset_ttl are defined with a shared receiver, matching the internally-synchronized &self contract of the rest of these traits (cache_set, cache_remove, …). This lets you flip the refresh flag or change the TTL on a shared store (e.g. one behind an Arc or a static) without exclusive access. Implementors must update their method signatures (fn set_ttl(&self, …) etc.); the bundled DiskCache / RedisCache / AsyncRedisCache stores do this via interior mutability (parking_lot::Mutex + AtomicBool). The single-owner Cached and CacheTtl traits are unaffected and keep their &mut self mutators.ConcurrentCached::cache_size / ConcurrentCachedAsync::cache_size: new method fn cache_size(&self) -> Result<Option<usize>, Self::Error> reporting the number of entries, with a default of Ok(None). The default makes it non-breaking for existing external implementors and honest for stores that cannot cheaply produce a count: the six sharded stores override it to return Ok(Some(len)), while the external-store impls (DiskCache, RedisCache, AsyncRedisCache) keep the Ok(None) default because their backends (redb, Redis) expose no O(1) size. Sharded stores also retain their inherent len() / is_empty() for a non-Result count.#[cached], #[once], #[concurrent_cached])result = true removed from #[cached] and #[once]: All Result<T, E> return types now automatically skip caching Err values. Remove result = true from all #[cached] and #[once] annotations — the behavior is now the default. To force-cache Err values, use the new cache_err = true opt-in.option = true removed from #[cached] and #[once]: All Option<T> return types now automatically skip caching None values. Remove option = true from all #[cached] and #[once] annotations — the behavior is now the default. To force-cache None values, use the new cache_none = true opt-in.#[concurrent_cached] now supports Option<T> returns: previously only Result<T, E> was accepted; Option<T> and plain T: Clone returns are now natively supported on the default in-memory sharded path. Note: option = true was never a recognized attribute on #[concurrent_cached] (it was silently ignored in 1.x); the new cache_none = true is the explicit opt-in to cache None values.#[cached] / #[once] on fn() -> Option<T> without attributes: previously cached None as-is; now skips caching None. Add cache_none = true to preserve the old behavior.#[cached] / #[once] on fn() -> Result<T,E> without attributes: previously cached the full Result; now skips caching Err. Add cache_err = true to preserve the old behavior.result_fallback = true no longer requires result = true: the explicit result = true companion is dropped; result_fallback now auto-detects Result<T,E> return types.ty users storing Option<T> or Result<T,E> directly: if your cache store type holds Option<T> or Result<T,E> as the value, you must now add cache_none = true or cache_err = true respectively so the macro uses the full wrapper type rather than extracting the inner T.map_error on the default in-memory sharded path is now a compile error: previously map_error = "…" was silently accepted and ignored when the store was the infallible default. If you had map_error on a #[concurrent_cached] that uses no redis/disk/ty/create, remove it. If you still need map_error (because you are switching to a redis or disk backend), add the corresponding backend attribute.result_fallback = true and with_cached_flag = true are mutually exclusive on #[concurrent_cached]: using both together is now a compile error. The combination was never valid — result_fallback stores the inner Ok(T) value while with_cached_flag wraps it in Return<T> — but the error was previously inscrutable. Remove one of the two attributes.cache_none = true and with_cached_flag = true are mutually exclusive on #[cached], #[once], and #[concurrent_cached]: using both together is now a compile error. The combination was never valid — cache_none = true stores Option<T> as the cached value type while with_cached_flag = true stores the inner T — but the error was previously a confusing downstream type mismatch. Remove one of the two attributes.cache_remove on expiring stores now returns None for expired-but-present entries. Previously ExpiringCache, ExpiringLruCache, and expiry-aware sharded stores returned Some(value) for an already-expired entry; now returns None. The entry is still removed and on_evict still fires.ConcurrentCached::cache_delete (and its ConcurrentCachedAsync equivalent) now returns true for expired-but-physically-present entries. In 1.x the method returned false for such entries. Use cache_remove if you need to distinguish a live removal from an expired one.LruCache::retain now fires on_evict and increments cache_evictions() for each removed entry, matching the semantics of cache_remove. Previously retain was side-effect-free. Internal TTL and expiring wrapper stores (LruTtlCache, ExpiringLruCache) use a new crate-internal retain_silent for their eviction sweeps, so those stores continue to count evictions exactly once.DiskCacheBuildError gains a new InvalidTtl(BuildError) variant: any exhaustive match on DiskCacheBuildError must add an arm for InvalidTtl. This variant is returned when a DiskCacheBuilder is given a zero-duration TTL.RedisCacheBuildError gains a new InvalidTtl(BuildError) variant: same as above for RedisCacheBuildError. Returned when a RedisCacheBuilder is given a zero-duration TTL.build() returns Result, all store constructors removedX::builder().…setters….build()?. All direct, store-returning constructors are removed — new, with_capacity, with_max_size, with_ttl, with_ttl_and_capacity, with_ttl_and_refresh, with_max_size_and_ttl, with_max_size_and_ttl_and_refresh, every try_with_*, and the sharded new / with_shards / with_max_size[_and_shards] / with_ttl[_and_shards] / with_max_size_and_ttl[_and_shards] variants — across UnboundCache, LruCache, TtlCache, LruTtlCache, TtlSortedCache, ExpiringCache, ExpiringLruCache, and all six sharded stores. (DiskCache / RedisCache / AsyncRedisCache are unchanged: their new(...) / builder(...) already return a builder.) This removes the second, panic-prone construction path that duplicated the builder.Builder::build now returns Result<Store, BuildError> for every in-memory and sharded store. It previously returned the store directly and panicked on invalid configuration. Add ? or .unwrap(). (Disk/Redis build() already returned Result; unchanged.)try_build() is removed from all builders. Now that build() is the single fallible constructor the alias is redundant — replace every .try_build() with .build().TtlSortedCacheBuilder gains .capacity(n) — the preallocation hint formerly supplied via TtlSortedCache::with_ttl_and_capacity. It is distinct from .max_size(n), which is the eviction bound.Duration yields BuildError::InvalidTtl. The previously-permissive direct constructors (e.g. TtlCache::with_ttl(Duration::ZERO)) that accepted a zero TTL no longer exist.size → max_size naming (builder setter, macro attribute, runtime setters).size(n) → .max_size(n) (LRU-family stores and TtlSortedCache). The sharded builders' per-shard cap setter is per_shard_max_size.#[cached] / #[concurrent_cached] macro attribute size = N → max_size = N. The old size = N spelling keeps working as a deprecated alias that emits a deprecation warning (anchored at the size token). Setting both on one annotation is a compile error. See "New macro attributes" under Added below.TtlSortedCache runtime max-size setters: size_limit(n) → set_max_size(n) and try_size_limit(n) → try_set_max_size(n) (matching the set_ttl runtime-mutator convention). The error type also changed: try_set_max_size now returns Result<Option<usize>, cached::SetMaxSizeError> instead of std::io::Result<Option<usize>>; if you propagate the error with ? into an io::Error context, update the enclosing function's error type or convert explicitly.max_size = N attribute for #[cached] and #[concurrent_cached]: the preferred spelling of the LRU-bound attribute, mirroring the renamed max_size builder setter. The original size = N attribute continues to work as a deprecated alias — using it emits a deprecation warning (anchored at the size token) steering you to max_size. Specifying both size and max_size on the same annotation is a compile error.cache_err = true attribute for #[cached], #[once], and #[concurrent_cached]: opt-in to also cache Err values from Result<T, E> returns (requires a Result<T, E> return type; mutually exclusive with result_fallback).cache_none = true attribute for #[cached], #[once], and #[concurrent_cached]: opt-in to also cache None values from Option<T> returns (requires an Option<T> return type).result_fallback = true support for #[concurrent_cached]: on an Err return, the last cached Ok value for the same key is returned instead. The stale value is kept in the primary cache slot (via ConcurrentCloneCached::cache_get_with_expiry_status) and re-cached with a fresh TTL window on Err; no separate fallback store is created. Requires a TTL (ttl/ttl_secs/ttl_millis) (a compile error is emitted otherwise). Restricted to the default in-memory sharded path (not redis/disk). Mutually exclusive with cache_err and with_cached_flag.ShardedCache<K,V> (unbounded), ShardedLruCache<K,V> (LRU), ShardedTtlCache<K,V> (TTL, requires time_stores), ShardedLruTtlCache<K,V> (LRU + TTL, requires time_stores), ShardedExpiringCache<K,V> (per-value expiry, unbounded), and ShardedExpiringLruCache<K,V> (per-value expiry, LRU-bounded). All six wrap an Arc (cheap clone, Send + Sync), use power-of-two per-shard parking_lot::RwLocks with cache-line-padded shard structs to eliminate false sharing, and support builder APIs with on_evict callbacks, copy_from for live resharding, and metrics() / shard_sizes() for observability. Shard routing uses the ShardHasher<K> trait (default: DefaultShardHasher backed by ahash) as a zero-overhead type parameter, allowing custom partition logic without runtime overhead.#[concurrent_cached] now defaults to an in-memory sharded store when redis = true and disk = true are both absent and no custom ty/create is provided. Macro attributes max_size = N, ttl = T, shards = S, and expires = true select the matching variant. map_error must not be specified on this path — the stores are Infallible and have no errors to map (supply redis = true, disk = true, or a custom ty/create to use a fallible store).#[concurrent_cached] on the default in-memory sharded stores now accepts plain return types — any T: Clone, Option<T>, or Result<T, E>. redis, disk, and custom ty/create stores still require Result<T, E>.expires = true attribute support to #[concurrent_cached] macro to automatically select ShardedExpiringCache (unbounded) or ShardedExpiringLruCache (LRU-bounded when max_size is also set).ShardedExpiringCache and ShardedExpiringLruCache require cached values to implement the Expires trait; copy_from skips entries already reporting is_expired() == true. Both expose deep_clone for snapshot copies.cache_clear_with_on_evict() to all six sharded stores (ShardedCache, ShardedLruCache, ShardedTtlCache, ShardedLruTtlCache, ShardedExpiringCache, ShardedExpiringLruCache): fires the on_evict callback for every removed entry when a callback is configured, and (where applicable) increments the evictions counter (ShardedCache is unbounded and has no evictions counter). The plain clear() inherent method remains fast and side-effect-free; cache_clear_with_on_evict() is the opt-in alternative.cache_clear_with_on_evict() to all seven non-sharded stores (UnboundCache, LruCache, TtlCache, LruTtlCache, ExpiringCache, ExpiringLruCache, TtlSortedCache): fires the on_evict callback for every removed entry and (where applicable) increments the evictions counter. The plain cache_clear() method remains fast and side-effect-free; cache_clear_with_on_evict() is the opt-in alternative.StripedCounter — a 16-slot cache-line-padded atomic counter — for hit/miss metrics on UnboundCache and TtlSortedCache to reduce false sharing under concurrent cache_get_read. All other stores continue to use plain AtomicU64.ConcurrentCloneCached<K, V> trait: concurrent analogue of CloneCached for the four expiry-capable sharded stores (ShardedTtlCache, ShardedLruTtlCache, ShardedExpiringCache, ShardedExpiringLruCache). Provides cache_get_with_expiry_status(&self, key: &K) -> (Option<V>, bool) — returns the value without removing expired entries, enabling result_fallback to fall back to stale values in-place. Takes &self (not &mut self) since sharded stores are internally synchronized.Cached::{get,set,remove,remove_entry,delete} and ConcurrentCached::{get,set,remove,remove_entry,delete} delegate to the existing cache_* methods (the sync Cached trait gains remove_entry / delete to match ConcurrentCached); both the sharded and non-sharded TTL builders expose .refresh_on_hit(...) as the primary setter with .refresh(...) retained as an alias; DiskCache, RedisCache, and AsyncRedisCache expose ::builder(...) aliases (alongside their existing ::new(...) builder entry points). Note: DiskCache::new(...) / RedisCache::new(...) / AsyncRedisCache::new(...) are builder entry points -- they return a builder, not a ready-to-use store -- and are intentionally retained; only the in-memory and sharded store constructors that returned stores directly were removed.capacity() getter to LruCache, LruTtlCache, and ExpiringLruCache — and to their sharded counterparts ShardedLruCache, ShardedLruTtlCache, and ShardedExpiringLruCache — that returns the configured max-entry bound (distinct from cache_size(), which returns the current live entry count).BuildError::InvalidTtl { ttl } variant for a single consistently-worded zero-TTL rejection path across all builders.ConcurrentCachedAsync that get/set/remove/delete short aliases are intentionally absent to avoid worsening method-resolution ambiguity.TtlCache, LruTtlCache, TtlSortedCache, ShardedTtlCache, ShardedLruTtlCache, DiskCache, RedisCache, and AsyncRedisCache builders now all call the shared validate_ttl helper and return BuildError::InvalidTtl { ttl }. With construction now builder-only, a zero TTL is uniformly rejected at build time (there is no longer a permissive direct-constructor path).#[concurrent_cached] in-memory Infallible error shim map into the function's declared Result<_, E> error type, reject invalid store-selection attributes, and use UFCS for generated ConcurrentCached calls so sync functions compile even when both concurrent traits are in scope.CacheEvict for ShardedTtlCacheBase and ShardedLruTtlCacheBase, make sharded builders return BuildError instead of panicking on capacity/shard overflows, avoid unnecessary 'static bounds when building ShardedLruTtlCache without on_evict, optimize ShardedTtlCacheBase hits under refresh_on_hit by bypassing read-locks, and correct the sharded LRU capacity documentation.Instant type.TtlSortedCache::cache_get and cache_get_mut live hits to use a single hash-map lookup.cache_remove semantics: removing any present entry now fires the store's on_evict callback (if set) and increments evictions.#[concurrent_cached] return-type classification so generic plain return types like HashMap<K, V> are not mistaken for Result aliases.Result-return detection in all three macros to require the exact identifier Result rather than matching any identifier that ends with "Result". Type aliases such as type MyResult<T> = Result<T, E> are now treated as plain values (their Err variant is cached). Only the literal Result<T, E> and its fully-qualified forms (e.g. std::result::Result<T, E>) continue to trigger skip-on-Err / result_fallback semantics. This aligns with the existing Option-detection behavior and makes the macro surface consistent.remove_entry) rather than the lookup key to on_evict in ShardedTtlCache::cache_remove and ShardedExpiringCache::cache_get / cache_remove.#[concurrent_cached] now rejects map_error on the default in-memory sharded path with a compile error — the stores are Infallible and accepting map_error while silently ignoring it was misleading. Previously map_error on this path was accepted and the infallible path emitted .expect(…) regardless..clone() on the #[concurrent_cached] cache-hit return path for all three return-type variants.#[concurrent_cached(with_cached_flag = true)] on the default in-memory path for plain cached::Return<T> returns.build() panic messages on all sharded stores to include the underlying BuildError detail.ShardedLruTtlCacheBase::evict() to remove expired inner entries without calling cache_remove, preventing double-counting of evictions and double-firing of on_evict.Cached::cache_delete (now on Cached via cache_remove_entry) correctly returns true for entries that were present but already expired; previously cache_delete on ConcurrentCached returned false for expired entries.Add ExpiringCache (and ExpiringCacheBuilder) as a size-unbounded store where each value implements the Expires trait and determines its own expiration
ExpiringCache (and ExpiringCacheBuilder) as a size-unbounded store where each value implements the Expires trait and determines its own expiration.expires = true attribute to the #[cached] procedural macro: automatically selects ExpiringCache (unbounded) or ExpiringLruCache (LRU-bounded when size is also set), so the return type controls its own expiry via Expires. Compatible with result, option, result_fallback, sync_writes, key/convert, and size. Mutually exclusive with ttl, ty, create, with_cached_flag, unsync_reads, refresh, and unbound.expires = true attribute in the #[once] procedural macro to allow single-value functions to utilize value-defined expiration (Expires trait).src/stores/expiring_lru.rs covering the Expires trait and ExpiringLruCache's CachedIter::iter expired-filtering, Clone, std::fmt::Debug, cache_remove, and cache_clear.std::fmt::Debug and Clone for TtlSortedCache (and its internal Entry type) and ExpiringCache to ensure full Debug/Clone trait parity across all 7 core in-memory store types.UnboundCache, LruCache, TtlCache, LruTtlCache, TtlSortedCache) verifying Debug and Clone trait behaviors; UnboundCache and LruCache also verify PartialEq and Eq.try_build() methods (asserting expected BuildError outcomes for invalid capacities, sizes, or missing required attributes like ttl).std::fmt::Display representation for all BuildError variants in src/stores/mod.rs.benches/cache_benches.rs) for cache hits across all 7 core in-memory stores (UnboundCache, LruCache, TtlCache, LruTtlCache, ExpiringLruCache, ExpiringCache, TtlSortedCache), cache misses & inserts, eviction capacity overhead, and RwLock lock-synchronization (with and without CachedRead::cache_get_read unsynchronized reads).bench target to the Makefile to run the benchmark suite.examples/expires_per_key.rs demonstrating how to use the Expires trait with ExpiringLruCache and ExpiringCache for per-value expiration, including keyed caching via #[cached(expires = true)] and single-value caching via #[once(expires = true)].Expires, ExpiringCache, and ExpiringLruCache to src/lib.rs (automatically synced to README.md).> for a complete walkthrough of every breaking change (and an
Upgrading from 0.x? See the 1.0 migration guide for a complete walkthrough of every breaking change (and an agent-oriented version for automated tooling).
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →