- Argument-inference breakage on the six sharded stores' inherent lookup methods
(
ShardedUnboundCache,ShardedLruCache,ShardedTtlCache,ShardedLruTtlCache,ShardedExpiringCache,ShardedExpiringLruCache):get,remove,remove_entry,delete,contains, andpeekare all generic over the looked-up key formQ.cache.get(&k)wherek: &K(for example,for k in &keys { cache.get(&k) }) previously compiled through deref coercion. It no longer does:Qunifies to&Kfirst, and the call fails withthe trait bound `String: Borrow<&String>` is not satisfied; same fork: &Box<K>andk: &Arc<K>, and the same shape applies toremove,remove_entry,delete,contains, andpeek. Migration: drop the extra&(cache.get(k)), or deref explicitly (cache.get(&**boxed)). Single-ownerCachedmethods (e.g.LruCache::cache_get) are unchanged. ShardHasher<K>'s key parameter is relaxed toShardHasher<K: ?Sized>. Existing impls compile unchanged, since this only widens what the trait accepts. ButH: ShardHasher<K>no longer impliesK: Sizedat a use site, which can shift inference in rare downstream generic code.- Shard routing no longer consults
BuildHasher::hash_one. The blanketShardHasherimpl for aBuildHasherand every store's borrowed-key routing build aHasherwithbuild_hasher(), hash the key and finish it, andDefaultShardHasher'shash_oneoverride was removed so it uses the provided default too. An overriddenhash_oneis no longer consulted for routing, and the upper-32-bit distribution contract for a custom hasher applies to theHasherreturned bybuild_hasher(). This matters for aBuildHasherwhosehash_onedispatches on the static type of its argument (ahash::RandomStatedoes, on any nightly compiler): an owned newtype key and its borrowed primitive form would otherwise route to different shards. Deliberate tradeoff:ShardedUnboundCache,ShardedTtlCache, andShardedExpiringCacheback their shards withHashMap<_, _, DefaultShardHasher>and std'sHashMapprobes viaBuildHasher::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)returnsSome(Claim<K>)to the first caller andNoneto every later caller until thatClaimis dropped; the key is released fromDrop, 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 viacached::claim::orcached::prelude, not the crate root, to keepClaimandClaimRegistryout of rustc's nearest-match suggestions for mistyped imports (design/0053).Debug for ClaimRegistry<K>requiresK: Debug + Clone: it copies the key set out from under the registry mutex and formats after releasing it, so no userDebugcode and no writer lock is held while the registry is locked.ClaimRegistry::newalready requiresK: Clone, so this only constrains generic code that names the type without building one.CacheSetMaxSizeandConcurrentCacheSetMaxSizetraits, reachingset_max_size/try_set_max_sizefrom generic code holding aT: CacheSetMaxSize(orConcurrentCacheSetMaxSize) bound instead of a concrete store type. No new capability: both methods already existed as inherent-only methods and already evicted eagerly on shrink, firingon_evictper removed entry; this only adds a route through a generic bound. Implemented byLruCache,LruTtlCache,ExpiringLruCache,TtlSortedCache(single-owner) andShardedLruCache,ShardedLruTtlCache,ShardedExpiringLruCache(sharded). Not implemented by the unbounded stores (UnboundCache,TtlCache,ExpiringCacheand their sharded forms), which have no live capacity to resize, or byRedisCache/AsyncRedisCache/RedbCache, which have no client-side capacity.CacheClearWithOnEvictandConcurrentCacheClearWithOnEvicttraits, reachingcache_clear_with_on_evictfrom 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 anon_evictcallback); not implemented by the three IO stores, which have noon_evictmechanism. The crate doc previously described this method as inherent-only and unreachable from generic code; that gap is now closed.- The six sharded stores' inherent
get,remove,remove_entry,delete,contains, andpeeknow accept any borrowed form of the key (K: Borrow<Q>), matching the single-owner stores:sharded_cache.get("a")now works on aShardedLruCache<String, _>without allocating aStringfirst. Bounded onH: ShardHasher<Q>, so a hand-writtenShardHasherrouter keeps these six methods at every key type it implements, not just where it also implementsBuildHasher. This relies on an unenforced contract: everyShardHasherimpl on the router must agree,shard_hash(&k) == shard_hash(k.borrow()), forK: Borrow<Q>. A router with two or more disagreeingShardHasherimpls routes an owned insert and its borrowed lookup to different shards, soget/peek/containsmiss a present entry andremove/deletesilently no-op; see the "Contract: a router's impls must agree with each other" section of theShardHasherdocs.setandget_or_set_withare 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::preludenow also exportsCacheSetMaxSize,CacheClearWithOnEvict,ConcurrentCacheSetMaxSize, andConcurrentCacheClearWithOnEvict. This cannot cause E0034 for any store in this crate: every store implementing these traits already hasset_max_size/cache_clear_with_on_evictas 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 implementsCacheSetMaxSize(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 withuse 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 throughDefaultShardHasherinstead of a shardHashMap's ownBuildHasher::hash_one.hash_oneis 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'sspecializecfg), 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 withDefaultHashBuilder = ahash::RandomStateby default and are not covered by this fix; on an affected nightly compiler, work around it by building with.hasher(std::hash::RandomState::new()). TheLruCache-backed family (LruCache,LruTtlCache,ExpiringLruCache) is not affected:LruCacheindexes its entries with aHashTable<usize>probed throughLruCache::hash, which hand-builds theHasherprecisely to avoid this. -
TtlSortedCache::cache_clear_with_on_evictnow counts an eviction per removed entry even when noon_evictcallback 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'sclone_on_copyon 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 isCopy(the unwrappedTof aResult/Optionreturn, 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
Clonebound diagnostic for non-Clonereturn 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.
- Gate the per-entry-expiry macro example in the crate doc on the
proc_macrofeature. The fence usedcached::macros, which does not exist without that feature, socargo test --no-default-features --docfailed to compile it; CI's no-default-features row runs--testsonly, so this went uncaught there. Gated rather than markedignore(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.
Documentation and tests only. There is no API or behavior change.
- Document that a custom
tyon#[concurrent_cached]requires the cached function to returnResult<T, E>, with a compiling example on theConcurrentCachedtrait. The macro cannot see the store'sErrortype at expansion time, so it always emits the fallible path. Previously this was only discoverable by hitting the compile error, which is now also pinned by atests/uicase. - New example
examples/moka_custom_store.rs: adapting a third-party cache (moka) toConcurrentCached. The orphan rule makes a local newtype mandatory for any foreign store, and the example records what an API that does not line up method-for-method costs to adapt, including returning the replaced value fromcache_setwithout a get-then-set race.mokais a dev-dependency for the example only; it is not a dependency of the crate (#220).
CacheExpiryandConcurrentCacheExpirytraits, providingcache_peek_expires_at()/peek_expires_at(): a side-effect-free per-key read returning(Option<V>, Option<Instant>)instead of theboolcache_peek_with_expiry_statusreturns, so callers can implement a threshold-based refresh (refresh when the remaining TTL drops below N) directly against the deadline (#91). Additive and non-breaking: standalone traits, not new required methods onCloneCached/ConcurrentCloneCached. Implemented byTtlCache,LruTtlCache,TtlSortedCache,ExpiringCache,ExpiringLruCache,ShardedTtlCache,ShardedLruTtlCache,ShardedExpiringCache, andShardedExpiringLruCache. Both traits also provide a value-freecache_expires_at()/expires_at(), returning(bool, Option<Instant>)(presence, deadline) instead of cloning the value, for callers who only need the remaining time; it needs noV: Clonebound, since that bound moved off the impl blocks and onto the value-returning methods (cache_peek_expires_at/peek_expires_at). OnExpiringCache,ExpiringLruCache,ShardedExpiringCache, andShardedExpiringLruCachethe deadline comes fromExpires::expires_at(), whose default body returnsNone: for anExpiresimpl that only implementsis_expired(the crate's own documented recipe), the read reportsNonefor both live and expired entries, and a threshold-refresh policy built on it silently never fires.
-
New runnable example
examples/refresh_before_expiry.rs: recompute an entry once its remaining ttl drops below a threshold, usingcache_peek_expires_at(peek_expires_at) to read the deadline and{fn}_prime_cacheto refresh outside the cache write lock, so the stored value is replaced while still live and no caller reads an expired entry. Companion toexamples/stale_while_revalidate.rs, which handles the already-expired case. Covers the sync#[cached], async#[cached], and async#[concurrent_cached]static shapes. No API change. -
New runnable example
examples/stale_while_revalidate.rs: serve an expired value immediately and refresh it off the critical path, composed fromcache_peek_with_expiry_status(which returns an expired entry as(Some(value), true)without removing it) and{fn}_prime_cache(which runs the function body outside the cache write lock). Covers the sync#[cached], async#[cached], and async#[concurrent_cached]static shapes, and an in-flight guard that collapses concurrent refreshes for the same key, and a single-flight section addingsync_writes = "by_key"so the cold path deduplicates while stale reads still never block. No API change.
examples/stale_while_revalidate.rsreleased its in-flight refresh claim only when the refresh returned normally, so a panicking or aborted refresh left the key claimed for the rest of the process and pinned it to a stale value with no way back. The claim is now released fromDrop. Example code only; no API change.
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.
- MSRV raised from 1.85 to 1.92.
redb4.x set the 1.89 floor, and theasync_corefeature (enabled byasync) does not compile before 1.92: the twoCachedGetOrSetAsyncRPIT 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, butrust-versionis a single crate-level value, so the floor moves for every feature set. cached_proc_macro_typesmoved to edition 2024, and its version now trackscachedin lockstep rather than a standalone1.0.
Store renames and the disk backend (#237)
DiskCacheis renamedRedbCache(naming the backend, likeRedisCache) and is backed byredb4.x instead of the unmaintainedsled, dropping the RustSec-flaggedfxhashtransitive dependency. Still pure-Rust (no C toolchain). There are noDiskCache*aliases: renameDiskCache/DiskCacheBuilder/DiskCacheError/DiskCacheBuildErrortoRedbCache*at the call site. The on-disk format changed andDISK_FILE_VERSIONwas bumped, so existing caches are not read and entries are recomputed.RedbCache::connection()/connection_mut(),RedbCacheBuilder::connection_config, and theconnection_configmacro attribute are removed; the backend handle is not exposed.DiskCacheBuilder::sync_to_disk_on_cache_changeis renameddurableand the default flipped fromfalsetotrue(fsync per write), so a disk cache persists by default.durable(false)usesDurability::None, which can lose writes on process exit or crash; callRedbCache::flush()/async_flush()to force a durable commit.RedbCacheBuilder::disk_directoryis renameddisk_dir, matching thedisk_dirattribute on#[concurrent_cached].ShardedCacheis renamedShardedUnboundCache(withShardedCacheBuilder->ShardedUnboundCacheBuilder); the old name read as the umbrella for the whole sharded family while naming only the unbounded variant. No deprecated alias.- The six sharded stores are single types carrying a defaulted hasher parameter,
ShardedX<K, V, H = DefaultShardHasher>, mirroringHashMap<K, V, S = RandomState>. The 2.xShardedCacheBasepattern is gone: there is noShardedUnboundCacheBase,ShardedLruCacheBase,ShardedTtlCacheBase,ShardedLruTtlCacheBase,ShardedExpiringCacheBase, orShardedExpiringLruCacheBase. Migration is a mechanical rename droppingBase. cached::TimedEntryis nowpub(crate), and thestore()accessors onUnboundCache,TtlCache,LruTtlCache, andExpiringLruCacheare removed. They exposed the internal backing map and leaked the internal entry wrapper; use the publicCachedAPI instead.
- The short method aliases (
get,set,remove,remove_entry,clear,len,is_empty,delete,try_set,contains,hits,misses,metrics, and the shortget_or_set_withfamily) moved offCached/ConcurrentCachedonto the blanket extension traitsCachedExt/ConcurrentCachedExt. The core traits keep only thecache_-prefixed methods, so a custom store implements a smaller surface. Callers usingcached::prelude::*need no change; others adduse cached::CachedExt;/use cached::ConcurrentCachedExt;, or use thecache_names. Customimpl Cached/impl ConcurrentCachedblocks must drop any short-alias methods. ConcurrentCachedAsync's cache operations carry anasync_prefix (async_cache_get,async_cache_set,async_cache_remove,async_cache_remove_entry,async_cache_delete), removing theE0034"multiple applicable items" error when both concurrent traits are in scope.- The concurrent trait surface is split. Introspection (
type Error,cache_size,cache_is_empty) lives onConcurrentCacheBase, the supertrait of both concurrent traits; the global-TTL controls (ttl,set_ttl,try_set_ttl,unset_ttl) live onConcurrentCacheTtl, implemented only by the TTL-capable concurrent stores.lenis removed from the base trait as a duplicate ofcache_size. Custom impls must movetype Error(and any size override) into animpl ConcurrentCacheBaseblock and TTL behavior intoimpl ConcurrentCacheTtl. refresh_on_hit/set_refresh_on_hitlive on their ownCacheRefreshOnHitandConcurrentCacheRefreshOnHittraits rather than onCacheTtl/ConcurrentCacheTtl.CacheRefreshOnHitis implemented byTtlCacheandLruTtlCache;ConcurrentCacheRefreshOnHitbyRedisCache,AsyncRedisCache,RedbCache,ShardedTtlCache, andShardedLruTtlCache.TtlSortedCacheimplements neither: its deadline-ordered index cannot refresh an entry's expiry on read, and its 2.xset_refresh_on_hitwas a no-op that discarded its argument. Both new traits are in the prelude. The inherentrefresh_on_hit/set_refresh_on_hitonTtlCacheandLruTtlCacheare removed (they shadowed the trait methods and the setter returned()), as are the inherent TTL controls on the sharded TTL stores andTtlSortedCache::set_ttl: runtime TTL control is trait-only.CacheTtlandCacheEvictare single-owner (&mut self) traits only, since&mut selfis unusable on a store held throughArc/static. Concurrent stores set TTL throughConcurrentCacheTtl::set_ttl(&self) and evict through the newConcurrentCacheEvict(fn evict(&self) -> usize).CachedandConcurrentCacheBasegained an associatedtype Error, bounded bystd::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 overflowInstantbounds stores the entry with no expiry instead of failing, socache_try_setno longer has a dedicated error type; the 2.xTtlSortedCacheErrorand the boxedBox<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*_mutvariants (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*_mutmethods.- The 2.x
CachedAsynctrait is renamedCachedGetOrSetAsync, naming the job it actually does (memoizing an async closure over a synchronous in-memoryCachedstore). Its four sync passthroughs (async_cache_get/async_cache_set/async_cache_remove/async_cache_clear) and the misleadingSelf: Cachedbound are removed, and its get-or-set methods use theasync_cache_*namespace (async_cache_get_or_set_with,async_cache_try_get_or_set_with, and their_mutvariants). - New required methods on custom impls:
cache_clear/cache_resetonConcurrentCached(and the async counterparts), whose 2.x no-opOk(())defaults silently did nothing;cache_peek_with_expiry_statusonCloneCached/ConcurrentCloneCached, whose defaults returned a wrong result that silently brokeforce_refresh+result_fallback; andcache_contains/async_cache_containsonConcurrentCached/ConcurrentCachedAsync, which carry noV: Clonebound socontainsworks for non-Clonevalues.ConcurrentCached::cache_containshas nowhere Self: Sizedbound and is dyn-callable. SerializeCached::cache_set_refandSerializeCachedAsync::async_cache_set_refreturnResult<(), Self::Error>instead ofResult<Option<V>, Self::Error>, removing a per-write read-and-decode round trip on the IO stores. Callcache_getfirst if you need the prior value.ShardHasherrequiresCloneas a supertrait, and any thread-safestd::hash::BuildHashernow implements it through a blanket impl, sostd::hash::RandomStateandahash::RandomStateare accepted directly by the sharded builders'.hasher(...).DefaultShardHasherimplementsBuildHasherand reachesShardHasherthrough that one blanket path, which also makes it usable withHashMap::with_hasherandLruCacheBuilder::hasher. A type cannot implement bothBuildHasherand a hand-writtenShardHasher(coherence rejects the pair), so a custom shard-routing hasher must not implementBuildHasher.Expires::expires_atreturnscrate::time::Instant(web-time backed, correct under wasm) instead ofstd::time::Instant, andCloneCached::cache_get_with_expiry_statusrequiresV: Clone, matching its peek sibling.
cache_setover an existing key promotes that key to most-recently-used onLruCache,LruTtlCache,ExpiringLruCache,ShardedLruCache,ShardedLruTtlCache, andShardedExpiringLruCache. 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 anon_evictcallback changed eviction order on two sharded stores.cache_peek,cache_peek_with_expiry_status, andcache_containsremain non-promoting; inserting a new key is unchanged. No public API writes a value without touching recency.on_evictreceives the displaced entry's own stored key rather than the caller'sEq-equal instance, on every store and every removal path, matchingHashMap::insert. Observable only for key types whoseEq/Hashignore part of the payload.- Every store counts an eviction before firing
on_evict, on every removal path (evict,retain,cache_remove/cache_remove_entry, lazy expiry sweeps,cache_setover an expired entry, capacity evictions, and the get-or-set families), so a panicking callback can no longer remove an entry without counting it. retainreturnsusize(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 fromHashMap::retaindeliberately, because thisretaindoes strictly more than filter, and it matchesTtlSortedCache::retain_latest. There is no#[must_use], so existingcache.retain(...);statements keep compiling; only call sites binding the result as()need a discard.set_max_sizereturnsOption<usize>(the previous bound) onLruCache,LruTtlCache, andExpiringLruCache, unifying the return type withTtlSortedCache.- The default shard count of the LRU-bounded sharded stores (
ShardedLruCache,ShardedLruTtlCache,ShardedExpiringLruCache) is capped by the requestedmax_sizeinstead of derived solely fromavailable_parallelism(): on the default path the count isnext_power_of_two(max_size / 16).clamp(1, default_shard_count()). This changes the observablecapacity(),shards(), andshard_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; theper_shard_max_sizepath and the unbounded stores keepdefault_shard_count(). ShardedTtlCacheandShardedLruTtlCachedecide 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.- TTL stores track per-entry expiry, so
set_ttlapplies to future inserts only; existing entries keep their computed expiry, andrefresh_on_hitrecomputes expiry from the current TTL at access time. A zeroDurationpassed to anyset_ttlsurface means "expiry disabled", exactly equivalent tounset_ttl(): it no longer panics on the sharded stores and no longer means "expire immediately" onTtlSortedCache.build()still rejects a zero TTL, andtry_set_ttl(0)still returnsSetTtlError::ZeroTtl. For the redis stores a disabled TTL writes keys without expiry (a plainSET) and the refresh path issues noEXPIRE. TtlSortedCachegains asetfamily in place ofinsert/insert_ttl/insert_evict/insert_ttl_evict:set(k, v)plus theset_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>.TtlSortedSetBuilderis 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 noon_evict; on successon_evictfires after the initializer. All four variants now agree with each other and withTtlCache/LruTtlCache.iter_order/value_orderonLruCache,LruTtlCache, andExpiringLruCachereturnCacheValue-wrapped values (Vec<(K, CacheValue<V, M>)>andVec<CacheValue<V, M>>), one shape across the LRU family.Mis per-entry metadata:()forLruCache/ExpiringLruCache,Option<Instant>forLruTtlCache(read throughCacheValue::expires_at).LruTtlCacheno longer leaks bare(Option<Instant>, V)tuples.key_orderis unchanged.cache_reset(and the concurrent counterparts) no longer preserves the preallocated backing capacity: it clears and shrinks toinitial_capacity, so later inserts may reallocate. Recreate the cache instead of resetting it to retain the allocation.- Sharded
copy_fromreturnsResult<_, BuildError>instead of panicking on invalid configuration, and theEqmarker impls forUnboundCacheandLruCacherequireV: Eq. - The six sharded types expose inherent
get/set/remove/remove_entry/delete/reset/contains/peekreturning unwrapped values, sostore.get(&k)isOption<V>rather thanResult<Option<V>, Infallible>. These take call-site priority over theConcurrentCached*trait methods, which returnResult<_, Self::Error>; note thats.set(k, v).unwrap()therefore compiles asOption::unwrapand panics on a first insert. Use thecache_-prefixed trait methods, or UFCS, for theResultshape.
capacity(n)is renamedinitial_capacity(n)onUnboundCacheBuilder,TtlCacheBuilder,TtlSortedCacheBuilder, andExpiringCacheBuilder, where it pre-allocates without bounding entry count. The name was ambiguous next tomax_size(n)on the LRU builders.RedbCache::builder(name),RedisCache::builder(prefix), andAsyncRedisCache::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 thatnew()returns a ready store); the in-memory and sharded stores gained realType::new()/Type::new(required_field)constructors.LruTtlCacheBuilderandShardedLruTtlCacheBuildertake the hasher in the third generic slot and the eviction typestate marker last:LruTtlCacheBuilder<K, V, S = DefaultHashBuilder, E = NoEvict>andShardedLruTtlCacheBuilder<K, V, H = DefaultShardHasher, E = NoEvict>.LruTtlCacheBuilderhad no hasher parameter in 2.x, so a 2.x annotation ofLruTtlCacheBuilder<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.- The redis TTL is optional: omitting
.ttl(...)stores keys without expiry. A TTL that is set must be greater than zero, andRedisCacheBuildError::MissingRequired("ttl")is no longer returned. RedisCacheBuilder::build()/AsyncRedisCacheBuilder::build()reject an empty prefix withBuild(BuildError::InvalidValue { field: "prefix", .. }). The prefix is what scopescache_clearto one logical cache; with an empty prefix,cache_clearmatched<namespace>:*and deleted the entries of every cache sharing the namespace.RedbCacheBuilder::build()validatescache_nameas 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.- Builder refresh naming is unified on
refresh_on_hit: therefresh()alias is removed from the in-memory TTL builders, and the redis/redb builders'refreshis renamed. The#[cached(refresh = true)]attribute is unchanged. BuildError::InvalidTtl { ttl }is removed; a zero TTL at build time yieldsBuildError::InvalidValue { field: "ttl", reason: "must be greater than zero" }.RedisCacheBuildError::InvalidTtlandRedbCacheBuildError::InvalidTtlbecomeBuild(BuildError), wrapping the inner error instead of duplicating it.
- Error enum variants dropped their redundant
Errorsuffix:RedbCacheError::{StorageError, CacheDeserializationError, CacheSerializationError}became{Storage, CacheDeserialization, CacheSerialization};RedbCacheBuildError::ConnectionErrorbecameStorage;RedisCacheError::{RedisCacheError, PoolError, CacheDeserializationError, CacheSerializationError}became{Redis, Pool, CacheDeserialization, CacheSerialization}.RedbCacheError/RedbCacheBuildErrorare struct variants (named fields) matching the redis enums, andCacheDeserializationcarries acached_value: Vec<u8>field. - The public store error enums (
RedbCacheError,RedbCacheBuildError,RedisCacheError,RedisCacheBuildError,BuildError,SetTtlError,SetMaxSizeError) are#[non_exhaustive], so external matches need a wildcard arm. - The redis and redb error types no longer expose
redis::,r2d2::, orredb::types through public fields or blanketFromimpls. Foreign causes are boxed behindBox<dyn std::error::Error + Send + Sync>and read throughsource(), so a backing-crate version bump is no longer a breaking change to these enums. Return<T>::valueandReturn<T>::was_cachedare private fields (cached_proc_macro_types). Use*r/r.into_inner()for the value andr.was_cached()for the flag; struct pattern matches must switch to the accessors.CacheMetrics.sizeis renamedentry_countand is nowOption<usize>, reportingNonefor stores whose size is unknown (redis/redb) instead of a false0.CacheMetricsis#[non_exhaustive]and derivesDefault, so construct it by mutatingCacheMetrics::default()rather than with a struct literal.RedbCache::remove_expired_entriesreturnsResult<usize, RedbCacheError>(the number removed) instead ofResult<(), RedbCacheError>, matching theevicttraits.
- The
ttlattribute takes three mutually exclusive forms:ttl_secs = N(whole seconds, replacing the 2.x bare-integerttl = N),ttl_millis = N(milliseconds, new), andttl = "<Duration expr>"(a string-literal Duration expression). The old bare-integer form produces an error directing you tottl_secs. Builders gained matching.ttl_secs(n)/.ttl_millis(n)methods (#149). - The deprecated
sizeattribute is removed from#[cached]/#[concurrent_cached](usemax_size = N; the macros detectsizeand emit a directed error), and theunboundattribute is removed from#[cached](a bare#[cached]already builds anUnboundCache). #[cached(refresh = true)]without a TTL is a compile error; it was previously ignored.#[cached]also rejectsresult_fallbackcombined withwith_cached_flag, and rejects an explicitsync_writes_bucketswhensync_writesis 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 customtywithout acreateblock on the redis and disk paths, anasyncclosure formap_error, andcache_prefix_blockon the disk path (it is redis-only).- All three macros reject a
namestarting with__cached(the prefix reserved for generated bindings) and validatenameas a Rust identifier. #[concurrent_cached]'srefreshattribute is a plainbool(wasOption<bool>), sorefresh = falseno longer conflicts withexpiresor acreateblock.
- Redis TLS is a separate axis (#231):
redis_tokioandredis_smolenable the TLS-agnostic connection path, so addredis_tokio_native_tls/redis_tokio_rustls(or theredis_smolequivalents) to restore TLS.redis_connection_managerandredis_async_cacheare capability features depending only onredis/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. - The
disk_storefeature is renamedredb_store. Thewasmfeature is removed (it gated nothing;web-timeprovides wasm-compatible time types transparently), as areredis_ahashandasync_tokio_rt_multi_thread. - The
asyncfeature no longer impliestokio; it pulls onlyasync-lock, andcached::async_sync::{Mutex, RwLock, OnceCell}re-export fromasync-lockinstead oftokio::sync(OnceCellthere has noconst_new()). AsyncRedbCacheruns blocking redb work on theblockingcrate's thread pool instead oftokio::spawn_blocking, andRedbCacheError::BackgroundTaskFailedis removed.blockingis pulled byredb_storerather thanasync, so redis-only and in-memory async builds do not pay for it. - Optional dependencies are gated with Cargo's
dep:syntax, so an optional dependency's name is no longer silently usable as a feature; enable the named crate feature instead. - Redis values are serialized with MessagePack (
rmp-serde) instead of JSON. Old 2.x JSON entries are read transparently and rewritten as MessagePack on their next write. Redis TTLs usePSETEX/PEXPIRE, so sub-second TTLs are honored to the millisecond (requires Redis 2.6+). - Redis key segments are percent-escaped (
:->%3A,%->%25) and the key always has three fields ({namespace}:{prefix}:{key}), so distinct triples always map to distinct keys; an unescaped join previously letnamespace="a:b", prefix=""collide withnamespace="a", prefix="b". An empty prefix keeps its separator:("ns", "", "k")encodes asns::k. This changes the on-wire key for any segment containing:or%; the value envelope'sversionfield 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 aConnectionStringnewtype whoseDisplayandDebugredact credentials; call.reveal()for the raw URL.- The
ahashfeature enablesahash/runtime-rngon 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::preludere-exports the common traits plus theCacheMetricsstruct for a single glob import.- Custom hashers on the non-sharded in-memory stores:
UnboundCache,LruCache,TtlCache,LruTtlCache,TtlSortedCache,ExpiringCache, andExpiringLruCachegained a hasher type parameter defaulted toDefaultHashBuilderand a.hasher(s)builder method, mirroring the sharded stores.DefaultHashBuilderis 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,DisplaywhereV: Display,value()/into_value(), andexpires_at()whenM = Option<Instant>.IntoValues::into_values()bulk-unwraps aniter_order()/value_order()result back into a plainVec<V>. The reverse comparisonbare_value == wrappedcannot 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 fireson_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), fireson_evictafter the lock is released, and requires noK: Clonebound.ConcurrentCachePeekandConcurrentCachePeekAsync: side-effect-freecache_peek/async_cache_peek(pluspeek/async_peekaliases) 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 inherentpeek(&self, &K) -> Option<V>.RedisCache,RedbCache, andAsyncRedisCacheimplement 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 overConcurrentCachedAsyncwith tenasync_-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), mirroringConcurrentCachedExt. In the prelude.ConcurrentCached::cache_try_get_or_set_withand its async counterpart (both provided): fallible-init get-or-set returningResult<Result<V, E>, Self::Error>, store error outer, closure error inner; nothing is stored on a closureErr.ConcurrentCachedExt::try_get_or_set_withis the short alias.ConcurrentCached/ConcurrentCachedAsyncalso gained defaultedcache_get_or_set_with(get-then-set, non-atomic) and no-op-defaultcache_reset_metrics.- Metric and introspection parity:
ConcurrentCacheBasegainedcache_hits/cache_misses/cache_capacity/cache_evictionsand a defaultmetrics(), so a generic bound can read a sharded store's metrics;CachedExtgainedcapacity/evictions/reset;ConcurrentCachedExtgainedlen/is_empty/hits/misses/capacity/evictions/clear/reset;CachedPeek::peek,CloneCached::peek_with_expiry_status, andConcurrentCloneCached::{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 inherentcontainson the six sharded stores, givingcontainsboth spellings on both trait families.CachedExt::containsdelegates to it, socontainsno 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, andTtlSortedCache::capacity() -> Option<usize>.- Runtime capacity resizing on the sharded LRU stores:
set_max_size(&self, usize) -> Option<usize>andtry_set_max_size(&self, usize) -> Result<Option<usize>, SetMaxSizeError>. Shrinking evicts LRU-excess entries per shard strictly by recency, fireson_evict, and counts evictions; resize is not atomic across shards.LruCache,LruTtlCache, andExpiringLruCachegained the same pair (#180), andSetMaxSizeError(variantsZeroMaxSizeandCapacityOverflow) replaces the mix ofBuildErrorandstd::io::Errorthe 2.x resize paths returned.CacheTtl::try_set_ttlis the matching strict TTL setter, returningSetTtlError::ZeroTtl. per_shard_initial_capacityon the three unbounded sharded builders, the sharded counterpart ofinitial_capacity.SerializeCached/SerializeCachedAsyncwithcache_set_ref/async_cache_set_ref, implemented byRedisCache/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/AsyncRedisCacheimplementcache_clear/async_cache_clearthrough a namespace-scopedSCAN+ batchedDEL(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).RedisCacheandAsyncRedisCachealso implementClone;RedbCachedoes not.RedbCache::flush/async_flushforce a durable commit,RedbCache::disk_path()returns the backing file path, andRedbCache::async_remove_expired_entriesruns the sweep on theblockingthread pool so it is usable from async contexts.RedisCacheError/RedbCacheErrorand their build-error siblings exposeis_deserialization() -> bool, so callers can distinguish a codec failure from a storage or network error without a full match.Debugis implemented forRedisCache,AsyncRedisCache, andRedbCache, redacted to namespace/prefix/path/ttl/refresh.PartialEq/EqforExpiringCacheandExpiringLruCache, andPartialEq/Eq/HashforConnectionString.NoEvict/HasEvictderiveClone,Copy,Debug,Defaultand 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 existingimpl Expiresblocks get the default for free.- Macro attributes:
force_refresh(a block expression over the arguments that bypasses the cached value, #146),in_impl = truefor methods insideimplblocks includingselfreceivers (#16, #140),companions_vis = "<vis>"to set the generated companions' visibility,companions = falseto suppress the{fn}_no_cache/{fn}_prime_cachecompanions entirely, andttl_millis(above).convert,create,force_refresh,map_error, andcache_prefix_blockaccept unquoted Rust in addition to the quoted-string form.map_erroris optional on the disk and redis paths (the generated code uses.map_err(Into::into)?, soE: From<RedbCacheError>/From<RedisCacheError>).#[concurrent_cached]acceptsresult_fallbacktogether withexpires. - Macro ergonomics:
#[cached]/#[concurrent_cached]accept reference arguments (&T,Option<&T>) on the default-key path, deriving an owned key without aconvert(#202, #203); the crate root is resolved viaproc-macro-crate, so a renamed or re-exportedcacheddependency works (#157); macro-introduced bindings are hygienically named__cached_*, so arguments namedkey,cache, orresultno longer collide (#230, #114); and a generic function withoutkey+convertproduces a clear error (#80). - Compile-time missing-feature guards:
#[cached]/#[once]/#[concurrent_cached]on anasync fnwithout theasyncfeature, a TTL attribute withouttime_stores, and#[concurrent_cached(disk = true)]/(redis = true)withoutredb_store/ a redis feature all name the missing feature instead of surfacing errors from generated internals. A return type that does not implementCloneproduces 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.- The release workflow creates a git tag and GitHub release for each workspace crate that is
newly published (#245), and refuses to publish a half-bumped workspace:
bin/check-versions.shfails the release when acached_proc_macro*dependency pin disagrees with that subcrate's version, or when a stablecachedwould depend on a pre-release subcrate.
sync_writes = "by_key"bucket selection seeds from a per-staticRandomStateinstead of a fixed-seed hasher, so an attacker who knows the key space cannot collapse the lock buckets to force whole-cache serialization.- Corrupt or undecodable cached values on the redis/redb
cache_getpath 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). - Redis credential handling is structural:
resolve_connection_string()returns a redactingConnectionString, the build path constructs sanitized synthetic errors (including ther2d2pool-build failure and theNotUnicodeenv-var value, which is the connection string itself), andRedisCacheBuilder::connection_pool_connection_timeoutbounds how longbuildwaits 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). - redb disk hardening on Unix: the cache directory is created
0700and the database file forced to0600on 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::CacheDeserializationrender theircached_valuebytes as<N bytes redacted>inDebug, and are documented as potentially sensitive.
{fn}_prime_cacheno 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 onErr, 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_fallbackrejects a non-disabledsync_writes, so no caller could serialize the window themselves.- TTL expiry is anchored after the value factory resolves on every get-or-set path across
TtlCache,LruTtlCache, andTtlSortedCache; 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. - Eviction accounting: the try-path get-or-set no longer fires
on_evictor counts an eviction until the replacement factory succeeds; overwriting an expired entry fireson_evictand counts uniformly across the timed and sharded stores; a panickingon_evictduring capacity eviction can no longer leave the cache over capacity;cache_clear_with_on_evictcounts every removed entry rather than degrading to a silentcache_clearwithout a callback; andcache_removesamples expiry once, at removal, so a slow callback cannot turn a live entry into aNonereturn. - Sweeps are panic-safe and two-phase (select, remove, count, then notify) across the in-memory
and sharded stores.
retain/evictpreviously firedon_evictinside 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 aVec<bool>of decisions rather than cloned keys, so noK: Clonebound is added. TtlSortedCache::set_and_get_mutno 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 bycache_size().TtlSortedCache::set_with(..).evict()also performs the expiry sweep whenmax_sizeis configured and the map is under the bound, where the opt-in was previously discarded, andbuildreserves withtry_reserveso a capacity-overflowingmax_sizereturnsErr(BuildError)instead of aborting.RedisCache::cache_clear/async_cache_cleardecodeSCANreplies as bytes rather thanString. 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_setno longer returns a displaced value that had already expired, andRedbCacheBuilder::build()returnsRedbCacheBuildError::Storageinstead 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 underpanic = "abort".)- Read-then-write races closed on both IO stores: redb refresh-on-hit, expiry eviction, and
remove_expired_entriesre-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. RedbCachedefault-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.- Sharded expiry evaluation happens once, under the shard write lock:
ShardedTtlCache,ShardedLruTtlCache,ShardedExpiringCache, andShardedExpiringLruCachepreviously evaluated a displaced entry's expiry outside the lock or twice, so a value crossing the threshold in that window firedon_evictwithout counting the eviction or produced a wrong return value.deep_cloneon the expiring sharded stores reads the hit/miss counters under the shard read lock, so cloned metrics match cloned entries. LruCache::cache_resetuses a fallible allocation path (a grownmax_sizecould request aHashMapcapacity past the allocation limit and panic), and internal LRU list pre-allocation saturates instead of overflowing.- Macro correctness: the
#[once]generic-value-type guard compares whole idents, sofn f<S: Into<String>>(..) -> Stringis no longer falsely rejected; a raw-identifier cachename(e.g.r#type) builds a working static instead of panicking; attributes written between the macro and thefnforward to every generated item, so#[cfg]gating stays in lockstep; user lint attributes reach the generated*_prime_cachecompanion; and no generateduseplaces a name in a scope enclosing user code, so a user item namedCachedorCloneCachedis no longer shadowed. - Macro attribute errors span the offending attribute rather than the function name, and
malformed
key/convert/force_refreshvalues produce contextual errors explaining the expected syntax instead of a baresyn"unexpected token". RedbCacheError,RedbCacheBuildError,RedisCacheError, andRedisCacheBuildErrorDisplayoutput includes the underlying cause, which was previously reachable only throughDebugwhile the source type is documented as not public API.ConcurrentCacheTtl::refresh_on_hitreflects the configured flag: the concurrent stores overrode onlyset_refresh_on_hit, so the getter always reportedfalsethrough trait dispatch.Cached for HashMapno longer requiresS: Default, soHashMap<K, V, DefaultHashBuilder>implementsCachedon wasm.- docs.rs feature annotations (
doc(cfg)) on theasync_core-gated impls and onAsyncRedisCacheBuilder::client_side_caching, which previously rendered as unconditionally available.
- The in-memory and sharded stores are faster, with no contract change beyond the behavior
changes listed above. The sharded stores resolve a read hit in one hash lookup instead of two,
count evictions per shard rather than through a shared striped counter, and cache the host's
CPU topology (sampled once per process in a
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, andExpiringCacheis smaller per instance. - Sharded stores gained an inherent
get_or_set_withreturningVdirectly, 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 shortremove/remove_entryaliases and the inherent shardedset/removeare 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_cachedis#[doc(hidden)](macro plumbing); it remainspuband callable.KeyedCachemoved 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.hashbrownupdated to 0.17 (internal). Dev-only:criterion0.8,googletest0.14.- The published crate manifests no longer carry a
[lints]table, so a future-toolchain warning firing incachedcannot break downstream builds, andspecs/,local/,.cursorrules, andMakefileare excluded from the published package.
- The
len/cache_size/iter/evictcontract on lazy-eviction stores is documented in one place:lenreturns the stored count without an expiry scan (so it may include expired entries),iteromits expired entries from the view without removing them, andevict()reclaims them and yields an accurate live count. - The sharded inherent-vs-trait return-shape split is documented on all six sharded store types,
including the UFCS disambiguation and the
.unwrap()sharp edge. - The redis on-wire format (positional MessagePack array,
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. - The
Arc<T>return pattern for expensive-to-clone values is documented on the macros: the cache stores theArc, and hits clone only the pointer (#64). - New runnable example
examples/resilience.rscoveringsync_writes = "by_key",result_fallback, andforce_refresh, plus cache-invalidation (#21) and struct-method (#236) examples.
- Docs/tests only (no API change): document the
Expirestrait /expires = trueas the idiomatic way to set a dynamic, per-entry TTL (a lifetime computed at call time rather than the uniformttl = N), with a runnable example reference, and add a regression test for the runtime-argument-driven TTL case (#246).
- Fix
TtlSortedCacheBuilder: an explicit.capacity(n)is now honored even when.max_size(m)is also set. Previously themax_size-derivedm + 1preallocation ran first, and becauseHashMap::reservenever shrinks, a smaller.capacity(n)had no effect. The explicit capacity now takes precedence as the preallocation hint whilemax_sizecontinues to bound entry count (#266).
Upgrading from 1.1? See the 2.0 migration guide.
- MSRV raised from 1.80 to 1.85, and the crates moved to the 2024 edition. Edition 2024 was stabilized in Rust 1.85, so this is the new minimum a downstream project needs to build
cached. Consumers already on Rust ≥ 1.85 are unaffected; those on 1.80–1.84 must update their toolchain. (The repository'srust-toolchain.tomlpins 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 theCachedtrait that removes an entry and returns the stored key and value. Unlikecache_remove, this returnsSomeeven when the deleted entry was already expired, making it possible to distinguish "key absent" from "key present but expired". Always fires the store'son_evictcallback (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 plusDiskCache/RedisCache/AsyncRedisCache). The seven non-sharded stores (UnboundCache,LruCache, etc.) gaincache_remove_entryvia theCachedtrait above.Cached::cache_delete<Q>(&mut self, k: &Q) -> bool: new default method onCachedthat deletes an entry without returning it; returnstrueif an entry was physically removed (including expired entries),falseif the key was absent. Implemented viacache_remove_entry.DiskCacheandRedisCache/AsyncRedisCachenow requireK: Clone(in addition to existing bounds) for theirConcurrentCached/ConcurrentCachedAsyncimpls, which is needed to return the stored key fromcache_remove_entry.ConcurrentCached/ConcurrentCachedAsyncmutators now take&selfinstead of&mut self:set_refresh_on_hit,set_ttl, andunset_ttlare defined with a shared receiver, matching the internally-synchronized&selfcontract 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 anArcor astatic) without exclusive access. Implementors must update their method signatures (fn set_ttl(&self, …)etc.); the bundledDiskCache/RedisCache/AsyncRedisCachestores do this via interior mutability (parking_lot::Mutex+AtomicBool). The single-ownerCachedandCacheTtltraits are unaffected and keep their&mut selfmutators.ConcurrentCached::cache_size/ConcurrentCachedAsync::cache_size: new methodfn cache_size(&self) -> Result<Option<usize>, Self::Error>reporting the number of entries, with a default ofOk(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 returnOk(Some(len)), while the external-store impls (DiskCache,RedisCache,AsyncRedisCache) keep theOk(None)default because their backends (redb, Redis) expose no O(1) size. Sharded stores also retain their inherentlen()/is_empty()for a non-Resultcount.
result = trueremoved from#[cached]and#[once]: AllResult<T, E>return types now automatically skip cachingErrvalues. Removeresult = truefrom all#[cached]and#[once]annotations — the behavior is now the default. To force-cacheErrvalues, use the newcache_err = trueopt-in.option = trueremoved from#[cached]and#[once]: AllOption<T>return types now automatically skip cachingNonevalues. Removeoption = truefrom all#[cached]and#[once]annotations — the behavior is now the default. To force-cacheNonevalues, use the newcache_none = trueopt-in.#[concurrent_cached]now supportsOption<T>returns: previously onlyResult<T, E>was accepted;Option<T>and plainT: Clonereturns are now natively supported on the default in-memory sharded path. Note:option = truewas never a recognized attribute on#[concurrent_cached](it was silently ignored in 1.x); the newcache_none = trueis the explicit opt-in to cacheNonevalues.#[cached]/#[once]onfn() -> Option<T>without attributes: previously cachedNoneas-is; now skips cachingNone. Addcache_none = trueto preserve the old behavior.#[cached]/#[once]onfn() -> Result<T,E>without attributes: previously cached the fullResult; now skips cachingErr. Addcache_err = trueto preserve the old behavior.result_fallback = trueno longer requiresresult = true: the explicitresult = truecompanion is dropped;result_fallbacknow auto-detectsResult<T,E>return types.- Custom-
tyusers storingOption<T>orResult<T,E>directly: if your cache store type holdsOption<T>orResult<T,E>as the value, you must now addcache_none = trueorcache_err = truerespectively so the macro uses the full wrapper type rather than extracting the innerT. map_erroron the default in-memory sharded path is now a compile error: previouslymap_error = "…"was silently accepted and ignored when the store was the infallible default. If you hadmap_erroron a#[concurrent_cached]that uses noredis/disk/ty/create, remove it. If you still needmap_error(because you are switching to aredisordiskbackend), add the corresponding backend attribute.result_fallback = trueandwith_cached_flag = trueare mutually exclusive on#[concurrent_cached]: using both together is now a compile error. The combination was never valid —result_fallbackstores the innerOk(T)value whilewith_cached_flagwraps it inReturn<T>— but the error was previously inscrutable. Remove one of the two attributes.cache_none = trueandwith_cached_flag = trueare mutually exclusive on#[cached],#[once], and#[concurrent_cached]: using both together is now a compile error. The combination was never valid —cache_none = truestoresOption<T>as the cached value type whilewith_cached_flag = truestores the innerT— but the error was previously a confusing downstream type mismatch. Remove one of the two attributes.
cache_removeon expiring stores now returnsNonefor expired-but-present entries. PreviouslyExpiringCache,ExpiringLruCache, and expiry-aware sharded stores returnedSome(value)for an already-expired entry; now returnsNone. The entry is still removed andon_evictstill fires.ConcurrentCached::cache_delete(and itsConcurrentCachedAsyncequivalent) now returnstruefor expired-but-physically-present entries. In 1.x the method returnedfalsefor such entries. Usecache_removeif you need to distinguish a live removal from an expired one.LruCache::retainnow fireson_evictand incrementscache_evictions()for each removed entry, matching the semantics ofcache_remove. Previouslyretainwas side-effect-free. Internal TTL and expiring wrapper stores (LruTtlCache,ExpiringLruCache) use a new crate-internalretain_silentfor their eviction sweeps, so those stores continue to count evictions exactly once.DiskCacheBuildErrorgains a newInvalidTtl(BuildError)variant: any exhaustivematchonDiskCacheBuildErrormust add an arm forInvalidTtl. This variant is returned when aDiskCacheBuilderis given a zero-duration TTL.RedisCacheBuildErrorgains a newInvalidTtl(BuildError)variant: same as above forRedisCacheBuildError. Returned when aRedisCacheBuilderis given a zero-duration TTL.
- Every store is now built exactly one way:
X::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, everytry_with_*, and the shardednew/with_shards/with_max_size[_and_shards]/with_ttl[_and_shards]/with_max_size_and_ttl[_and_shards]variants — acrossUnboundCache,LruCache,TtlCache,LruTtlCache,TtlSortedCache,ExpiringCache,ExpiringLruCache, and all six sharded stores. (DiskCache/RedisCache/AsyncRedisCacheare unchanged: theirnew(...)/builder(...)already return a builder.) This removes the second, panic-prone construction path that duplicated the builder. Builder::buildnow returnsResult<Store, BuildError>for every in-memory and sharded store. It previously returned the store directly and panicked on invalid configuration. Add?or.unwrap(). (Disk/Redisbuild()already returnedResult; unchanged.)try_build()is removed from all builders. Now thatbuild()is the single fallible constructor the alias is redundant — replace every.try_build()with.build().TtlSortedCacheBuildergains.capacity(n)— the preallocation hint formerly supplied viaTtlSortedCache::with_ttl_and_capacity. It is distinct from.max_size(n), which is the eviction bound.- Zero TTL is now always rejected. Because every store is built through its (validating) builder, a zero
DurationyieldsBuildError::InvalidTtl. The previously-permissive direct constructors (e.g.TtlCache::with_ttl(Duration::ZERO)) that accepted a zero TTL no longer exist.
- Builder setter
.size(n)→.max_size(n)(LRU-family stores andTtlSortedCache). The sharded builders' per-shard cap setter isper_shard_max_size. - The
#[cached]/#[concurrent_cached]macro attributesize = N→max_size = N. The oldsize = Nspelling keeps working as a deprecated alias that emits a deprecation warning (anchored at thesizetoken). Setting both on one annotation is a compile error. See "New macro attributes" under Added below. TtlSortedCacheruntime max-size setters:size_limit(n)→set_max_size(n)andtry_size_limit(n)→try_set_max_size(n)(matching theset_ttlruntime-mutator convention). The error type also changed:try_set_max_sizenow returnsResult<Option<usize>, cached::SetMaxSizeError>instead ofstd::io::Result<Option<usize>>; if you propagate the error with?into anio::Errorcontext, update the enclosing function's error type or convert explicitly.
max_size = Nattribute for#[cached]and#[concurrent_cached]: the preferred spelling of the LRU-bound attribute, mirroring the renamedmax_sizebuilder setter. The originalsize = Nattribute continues to work as a deprecated alias — using it emits a deprecation warning (anchored at thesizetoken) steering you tomax_size. Specifying bothsizeandmax_sizeon the same annotation is a compile error.cache_err = trueattribute for#[cached],#[once], and#[concurrent_cached]: opt-in to also cacheErrvalues fromResult<T, E>returns (requires aResult<T, E>return type; mutually exclusive withresult_fallback).cache_none = trueattribute for#[cached],#[once], and#[concurrent_cached]: opt-in to also cacheNonevalues fromOption<T>returns (requires anOption<T>return type).result_fallback = truesupport for#[concurrent_cached]: on anErrreturn, the last cachedOkvalue for the same key is returned instead. The stale value is kept in the primary cache slot (viaConcurrentCloneCached::cache_get_with_expiry_status) and re-cached with a fresh TTL window onErr; 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 withcache_errandwith_cached_flag.
- Add six fully-concurrent, sharded in-memory cache stores:
ShardedCache<K,V>(unbounded),ShardedLruCache<K,V>(LRU),ShardedTtlCache<K,V>(TTL, requirestime_stores),ShardedLruTtlCache<K,V>(LRU + TTL, requirestime_stores),ShardedExpiringCache<K,V>(per-value expiry, unbounded), andShardedExpiringLruCache<K,V>(per-value expiry, LRU-bounded). All six wrap anArc(cheap clone,Send + Sync), use power-of-two per-shardparking_lot::RwLocks with cache-line-padded shard structs to eliminate false sharing, and support builder APIs withon_evictcallbacks,copy_fromfor live resharding, andmetrics()/shard_sizes()for observability. Shard routing uses theShardHasher<K>trait (default:DefaultShardHasherbacked 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 whenredis = trueanddisk = trueare both absent and no customty/createis provided. Macro attributesmax_size = N,ttl = T,shards = S, andexpires = trueselect the matching variant.map_errormust not be specified on this path — the stores areInfallibleand have no errors to map (supplyredis = true,disk = true, or a customty/createto use a fallible store).#[concurrent_cached]on the default in-memory sharded stores now accepts plain return types — anyT: Clone,Option<T>, orResult<T, E>.redis,disk, and customty/createstores still requireResult<T, E>.- Add
expires = trueattribute support to#[concurrent_cached]macro to automatically selectShardedExpiringCache(unbounded) orShardedExpiringLruCache(LRU-bounded whenmax_sizeis also set). ShardedExpiringCacheandShardedExpiringLruCacherequire cached values to implement theExpirestrait;copy_fromskips entries already reportingis_expired() == true. Both exposedeep_clonefor snapshot copies.
- Add
cache_clear_with_on_evict()to all six sharded stores (ShardedCache,ShardedLruCache,ShardedTtlCache,ShardedLruTtlCache,ShardedExpiringCache,ShardedExpiringLruCache): fires theon_evictcallback for every removed entry when a callback is configured, and (where applicable) increments the evictions counter (ShardedCacheis unbounded and has no evictions counter). The plainclear()inherent method remains fast and side-effect-free;cache_clear_with_on_evict()is the opt-in alternative. - Add
cache_clear_with_on_evict()to all seven non-sharded stores (UnboundCache,LruCache,TtlCache,LruTtlCache,ExpiringCache,ExpiringLruCache,TtlSortedCache): fires theon_evictcallback for every removed entry and (where applicable) increments the evictions counter. The plaincache_clear()method remains fast and side-effect-free;cache_clear_with_on_evict()is the opt-in alternative. - Add
StripedCounter— a 16-slot cache-line-padded atomic counter — for hit/miss metrics onUnboundCacheandTtlSortedCacheto reduce false sharing under concurrentcache_get_read. All other stores continue to use plainAtomicU64. - Add
ConcurrentCloneCached<K, V>trait: concurrent analogue ofCloneCachedfor the four expiry-capable sharded stores (ShardedTtlCache,ShardedLruTtlCache,ShardedExpiringCache,ShardedExpiringLruCache). Providescache_get_with_expiry_status(&self, key: &K) -> (Option<V>, bool)— returns the value without removing expired entries, enablingresult_fallbackto fall back to stale values in-place. Takes&self(not&mut self) since sharded stores are internally synchronized. - Add API consistency aliases:
Cached::{get,set,remove,remove_entry,delete}andConcurrentCached::{get,set,remove,remove_entry,delete}delegate to the existingcache_*methods (the syncCachedtrait gainsremove_entry/deleteto matchConcurrentCached); both the sharded and non-sharded TTL builders expose.refresh_on_hit(...)as the primary setter with.refresh(...)retained as an alias;DiskCache,RedisCache, andAsyncRedisCacheexpose::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. - Add an inherent
capacity()getter toLruCache,LruTtlCache, andExpiringLruCache— and to their sharded counterpartsShardedLruCache,ShardedLruTtlCache, andShardedExpiringLruCache— that returns the configured max-entry bound (distinct fromcache_size(), which returns the current live entry count). - Add
BuildError::InvalidTtl { ttl }variant for a single consistently-worded zero-TTL rejection path across all builders. - Document on
ConcurrentCachedAsyncthatget/set/remove/deleteshort aliases are intentionally absent to avoid worsening method-resolution ambiguity.
- Unify zero-TTL validation across all TTL-capable store builders:
TtlCache,LruTtlCache,TtlSortedCache,ShardedTtlCache,ShardedLruTtlCache,DiskCache,RedisCache, andAsyncRedisCachebuilders now all call the sharedvalidate_ttlhelper and returnBuildError::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). - Make the generated
#[concurrent_cached]in-memoryInfallibleerror shim map into the function's declaredResult<_, E>error type, reject invalid store-selection attributes, and use UFCS for generatedConcurrentCachedcalls so sync functions compile even when both concurrent traits are in scope. - Implement
CacheEvictforShardedTtlCacheBaseandShardedLruTtlCacheBase, make sharded builders returnBuildErrorinstead of panicking on capacity/shard overflows, avoid unnecessary'staticbounds when buildingShardedLruTtlCachewithouton_evict, optimizeShardedTtlCacheBasehits underrefresh_on_hitby bypassing read-locks, and correct the sharded LRU capacity documentation. - Fix timed-store eviction sweeps to use the crate's configured
Instanttype. - Optimize
TtlSortedCache::cache_getandcache_get_mutlive hits to use a single hash-map lookup. - Unify
cache_removesemantics: removing any present entry now fires the store'son_evictcallback (if set) and incrementsevictions. - Tighten
#[concurrent_cached]return-type classification so generic plain return types likeHashMap<K, V>are not mistaken forResultaliases. - Tighten
Result-return detection in all three macros to require the exact identifierResultrather than matching any identifier that ends with"Result". Type aliases such astype MyResult<T> = Result<T, E>are now treated as plain values (theirErrvariant is cached). Only the literalResult<T, E>and its fully-qualified forms (e.g.std::result::Result<T, E>) continue to trigger skip-on-Err/result_fallbacksemantics. This aligns with the existingOption-detection behavior and makes the macro surface consistent. - Pass the stored key (via
remove_entry) rather than the lookup key toon_evictinShardedTtlCache::cache_removeandShardedExpiringCache::cache_get/cache_remove. #[concurrent_cached]now rejectsmap_erroron the default in-memory sharded path with a compile error — the stores areInfallibleand acceptingmap_errorwhile silently ignoring it was misleading. Previouslymap_erroron this path was accepted and the infallible path emitted.expect(…)regardless.- Remove redundant
.clone()on the#[concurrent_cached]cache-hit return path for all three return-type variants. - Fix
#[concurrent_cached(with_cached_flag = true)]on the default in-memory path for plaincached::Return<T>returns. - Extend
build()panic messages on all sharded stores to include the underlyingBuildErrordetail. - Fix
ShardedLruTtlCacheBase::evict()to remove expired inner entries without callingcache_remove, preventing double-counting of evictions and double-firing ofon_evict. - Fix
Cached::cache_delete(now onCachedviacache_remove_entry) correctly returnstruefor entries that were present but already expired; previouslycache_deleteonConcurrentCachedreturnedfalsefor expired entries.
- Add
ExpiringCache(andExpiringCacheBuilder) as a size-unbounded store where each value implements theExpirestrait and determines its own expiration. - Add
expires = trueattribute to the#[cached]procedural macro: automatically selectsExpiringCache(unbounded) orExpiringLruCache(LRU-bounded whensizeis also set), so the return type controls its own expiry viaExpires. Compatible withresult,option,result_fallback,sync_writes,key/convert, andsize. Mutually exclusive withttl,ty,create,with_cached_flag,unsync_reads,refresh, andunbound. - Add support for the
expires = trueattribute in the#[once]procedural macro to allow single-value functions to utilize value-defined expiration (Expirestrait). - Add comprehensive unit tests in
src/stores/expiring_lru.rscovering theExpirestrait andExpiringLruCache'sCachedIter::iterexpired-filtering,Clone,std::fmt::Debug,cache_remove, andcache_clear. - Implement
std::fmt::DebugandCloneforTtlSortedCache(and its internalEntrytype) andExpiringCacheto ensure fullDebug/Clonetrait parity across all 7 core in-memory store types. - Add robust unit tests across all remaining core cache stores (
UnboundCache,LruCache,TtlCache,LruTtlCache,TtlSortedCache) verifyingDebugandClonetrait behaviors;UnboundCacheandLruCachealso verifyPartialEqandEq. - Add comprehensive validation unit tests for each store builder's fallible
try_build()methods (asserting expectedBuildErroroutcomes for invalid capacities, sizes, or missing required attributes likettl). - Add unit tests validating the
std::fmt::Displayrepresentation for allBuildErrorvariants insrc/stores/mod.rs. - Add standardized micro-benchmarks (
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, andRwLocklock-synchronization (with and withoutCachedRead::cache_get_readunsynchronized reads). - Add new
benchtarget to theMakefileto run the benchmark suite. - Add standard, runnable example
examples/expires_per_key.rsdemonstrating how to use theExpirestrait withExpiringLruCacheandExpiringCachefor per-value expiration, including keyed caching via#[cached(expires = true)]and single-value caching via#[once(expires = true)]. - Add detailed library-level documentation and quickstart example for
Expires,ExpiringCache, andExpiringLruCachetosrc/lib.rs(automatically synced toREADME.md).
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).
- Add comprehensive async integration tests in
tests/cached.rsforCachedAsyncmethods onTtlCache,LruTtlCache,TtlSortedCache,ExpiringLruCache, andUnboundCacheto assert correcton_evictinvocation on expired lookups. - Add
make helpandmake check/helptargets for documenting and validating supported Makefile commands. - Add fallible
try_buildmethods toTtlCacheBuilderandExpiringLruCacheBuilder. - Re-export
TtlSortedCacheErrorat the crate root (and viacached::stores) so users can name and match on the error returned byTtlSortedCache::cache_try_set. ExpiringLruCache::store()accessor (mirroringLruTtlCache::store()) for advanced introspection of the innerLruCache.- Add
ConcurrentCached::cache_deleteandConcurrentCachedAsync::cache_deletefor deleting entries without decoding or returning the previous value. CachedPeektrait: non-mutating cache lookups that skip recency updates, TTL refresh, and hit/miss metricsCachedReadtrait: shared-reference reads for stores with no read-side mutation; used byunsync_readsCacheEvicttrait: explicitevict()method to sweep expired entries from all timed/expiring storesunsync_reads = trueoption for#[cached]: uses a read lock on the cache-hit path instead of a write lock; requires the store to implementCachedRead(supported byUnboundCache,TtlSortedCache,HashMap, and custom stores that implementCachedRead)on_evict(|k, v| { ... })eviction callbacks on all in-memory stores (LruCache,TtlCache,LruTtlCache,ExpiringLruCache,TtlSortedCache)::builder()constructor APIs for all in-memory storescache_evictions()metric on all stores that support evictionConcurrentCachedAsyncis now implemented forDiskCache;#[concurrent_cached(disk = true)]on anasync fnruns allsledI/O ontokio's blocking pool viaspawn_blockinginstead of blocking the async runtime. Adds theDiskCacheError::BackgroundTaskFailedvariant returned if that blocking task is cancelled or panics.#[cached],#[once], and#[concurrent_cached]are now re-exported at the crate root (use cached::cached;works), alongside the existingcached::macros::*path.DiskCacheBuildError,DiskCacheBuilder,RedisCacheBuildError,RedisCacheBuilder, andAsyncRedisCacheBuilderare now re-exported at the crate root, matching the in-memory*Builderre-exports — the error type returned byDiskCache/RedisCachebuild()is now nameable via the same path the cache type came from.
- Make LRU-backed
try_buildpaths consistently use fallible allocation helpers instead of panicking constructors. - Optimize
TtlCache,LruTtlCache, andExpiringLruCacheto perform exactly one lookup (O(1)) on hit paths forcache_get,cache_get_mut, andcache_get_with_expiry_statusby inlining expiration status checks. - Breaking:
LruCache::try_with_sizeandLruTtlCache::try_with_size_and_ttlnow returnResult<_, BuildError>directly instead ofstd::io::Resultas a hard breaking change, aligning them with modern Builder pattern construction. TtlSortedCache::set_ttlnow returnsOption<Duration>(previouslyDuration) to matchCacheTtl::set_ttland theset_ttlof every other timed store.LruCache,LruTtlCache, andExpiringLruCachecache_resetimplementations now rebuild their backing stores instead of only clearing entries.DiskCache::cache_getnow returns deserialization errors for corrupted entries instead of treating them as cache misses.DiskCache::remove_expired_entriesnow reports storage and deserialization errors encountered while sweeping instead of ignoring them.- Fix timed
#[once]caches so TTL starts after the function body finishes executing. - Improve macro diagnostics for
result_fallbackwithoutresult = trueand forwith_cached_flagreturn types whose names merely containReturn. - Fix
ExpiringLruCache::cache_capacityto reportSome(capacity)(was falling through to theCacheddefaultNone, sometrics().capacitywas inaccurate for the only size-bounded store that didn't override it). RedisCache,RedisCacheBuilder,AsyncRedisCache, andAsyncRedisCacheBuildernow use a fn-pointerPhantomData<fn() -> (K, V)>so the cache type is unconditionallySend + Syncregardless of whetherK/Vare. Dropped theV: Syncbound fromimpl AsyncRedisCacheandimpl ConcurrentCachedAsync for AsyncRedisCache(values cross the async boundary by value, never by shared reference). A value that isSendbut!Sync(e.g. one containing aCell) — previously rejected because the macro-emittedLazyLock<RedisCache<_, V>>/OnceCell<AsyncRedisCache<_, V>>static required the cache type to beSync(PhantomData<(K, V)>propagatedV: Sync), and the async path additionally hadV: Send + Syncon the trait/inherent impls — is now accepted. Mirrors the asyncDiskCacherelaxation.#[concurrent_cached]now structurally requires the function return to be aResult(last path segment namedResult). PreviouslyOption<T>/Vec<T>/ bareTreturns passed the attribute check and produced a confusing error inside the generated body; they now fail with a clean spanned diagnostic pointing at the return type. Proc-macro token-only limitation: aResulttype alias renamed away fromResultis not recognized (same aswith_cached_flag/Return).- Breaking:
#[concurrent_cached]now rejects every store-builder attribute (ttl,refresh,cache_prefix_block,disk_dir,connection_config,sync_to_disk_on_cache_change) when acreateblock is supplied, with a single unified message naming each offender. Previously onlyttl/refresh(andcache_prefix_blockfor the redis/custom branches) were rejected, sodisk_dir/connection_config/sync_to_disk_on_cache_changepaired withcreatewere silently ignored — a real footgun (the user thought their disk path / durability was applied when it was not). Move the dropped attrs into yourcreateblock, or remove them. - Breaking:
#[cached]likewise rejects its store-builder attributes (ttl,ttl_millis,max_size,unbound,refresh) when acreateblock is supplied, with the same unified message, mirroring#[concurrent_cached]. Previouslyrefreshpaired withcreatewas silently ignored. Move the dropped attrs into yourcreateblock, or remove them. CacheEvict::evictnow returns the number of expired entries removed, matching the existingTtlSortedCachebehavior.- Fix
DiskCache::cache_getrefreshes to return serialization errors instead of panicking when refreshed values cannot be serialized. - Fix
DiskCache::cache_setto return the raw previous value at a key, matching theConcurrentCachedtrait contract and Redis behavior. - Fix
LruTtlCacheexpired lookups so they do not promote expired entries or inflate the innerLruCachehit/miss metrics. - Fix
ExpiringLruCache::cache_getandcache_get_mutto usepeek_by_key+move_to_front_by_keyinstead of routing throughLruCache::cache_get, which was inflating the inner store's hit counter on every successful lookup. - Fix
ExpiringLruCache::cache_get_mutto fireon_evictcallbacks and increment eviction metrics when an expired entry is removed. - Redis TTL handling now rejects only zero durations, rounds sub-second non-zero TTLs up to one second, and avoids overflowing refresh expirations.
- Breaking: Redis cache key format changed from raw concatenation (
{namespace}{prefix}{key}) to colon-delimited joining with empty-segment skipping ({namespace}:{prefix}:{key}). Existing Redis caches built against pre-1.0 versions will see cache misses on upgrade because stored keys will no longer match. The default namespace (cached-redis-store:) is trimmed of its trailing colon and re-joined, so the effective change for default-namespace users is that the prefix and key are now separated by:(e.g.cached-redis-store:my_prefixmy_key→cached-redis-store:my_prefix:my_key). LruTtlCachevalidation errors now useErrorKind::InvalidInputinstead of raw OS error codes.- Improve
#[cached(unsync_reads = true)]diagnostics for generated sized/timed stores and convert several#[concurrent_cached]macro panics into spanned compile errors. - Fix
LruTtlCacheandExpiringLruCache:on_evictcallbacks and eviction counts now correctly fire whencache_get_or_set_withreplaces an expired entry (previously the displaced value was silently discarded) - Fix
ExpiringLruCache::cache_get: expired entries are now removed on access instead of being promoted to most-recent in the LRU, which was causing live entries to be evicted ahead of expired ones - Fix
TtlSortedCache: size-limit validation now returnsErrorKind::InvalidInputinstead offrom_raw_os_error(22) - Fix
HashMapCachedPeek/CachedReadimpls: removed spuriousS: Defaultbound (only theCachedimpl requires it) - Expanded
make testsmatrix with explicitno-default,proc_macro-only,time_stores,async,disk_store, andredisfeature combinations - Breaking:
redis_connection_managerno longer impliesredis_tokio. It now impliesasyncandredis_storeplus theredis/tokio-compandredis/connection-managerredis features — giving you the Tokio async runtime and the connection manager without pulling in TLS. Users who need TLS should addredis_tokio(native-tls) or configure TLS via therediscrate directly.
- Breaking: Completely removed the unused internal
Statusenum fromcached::stores(it was previously returned by an internal helper which has been inlined/eliminated). - Breaking: Removed declarative macros (
cached!,cached_key!,cached_result!,cached_key_result!,cached_control!) and themacrosmodule that contained them. Use the#[cached],#[once], and#[concurrent_cached]procedural macros instead. - Breaking: The procedural macro re-export module has been renamed from
proc_macrotomacros. Updateuse cached::proc_macro::cachedtouse cached::macros::cached(and similarly foronce; theio_cachedmacro was additionally renamed — see below). - Breaking: Renamed the
IOCached/IOCachedAsynctraits toConcurrentCached/ConcurrentCachedAsync, and the#[io_cached]proc macro to#[concurrent_cached](cached::macros::io_cached→cached::macros::concurrent_cached). The contract is unchanged — the names no longer imply "IO", since a self-synchronizing in-memory store is equally valid. Updateimpl IOCached for/use cached::IOCachedand every#[io_cached(...)]attribute accordingly. - Breaking: Removed
InMemoryAdapter<K, V, C>. It only wrapped aCachedstore in a singleparking_lot::Mutex, which is strictly worse than#[cached]for the macro path (double locking) and trivially hand-rolled for the rare generic-bridge case. Use#[cached]/#[once]for in-memory memoization, or implementConcurrentCacheddirectly. - The example files
basic_proc_macroandkitchen_sink_proc_macrohave been renamed tobasicandkitchen_sinkrespectively. - Breaking: Renamed
CanExpiretrait toExpires. Updateuse cached::CanExpiretouse cached::Expiresand allV: CanExpirebounds toV: Expires. - Breaking: IO store builder methods drop the
set_prefix to match in-memory builder style:DiskCacheBuilder:set_ttl→ttl,set_refresh→refresh,set_disk_directory→disk_directory,set_sync_to_disk_on_cache_change→sync_to_disk_on_cache_change,set_connection_config→connection_configRedisCacheBuilder/AsyncRedisCacheBuilder:set_lifespan→ttl,set_refresh→refresh,set_namespace→namespace,set_prefix→prefix,set_connection_string→connection_string,set_connection_pool_max_size→connection_pool_max_size,set_connection_pool_min_idle→connection_pool_min_idle,set_connection_pool_max_lifetime→connection_pool_max_lifetime,set_connection_pool_idle_timeout→connection_pool_idle_timeout,set_client_side_caching→client_side_caching(async only); the internal resolverconnection_string→resolve_connection_string(the setter now owns the bare name).
- Breaking: Removed all
#[deprecated]shim methods:LruCache::with_capacity,TtlSortedCache::ttl_millis,DiskCacheBuilder::set_lifespan. - Breaking: Removed
cache_ttl,cache_set_ttl, andcache_unset_ttlfrom theCachedtrait. UseCacheTtl::ttl,set_ttl, andunset_ttlon timed stores instead. - Breaking: Renamed IO-backed TTL/refresh methods to match
CacheTtl:cache_ttl→ttl,cache_set_ttl→set_ttl,cache_unset_ttl→unset_ttl,cache_set_refresh→set_refresh_on_hit. - Breaking: Renamed inherent timed-store refresh accessors:
TtlCache::refresh→refresh_on_hit,TtlCache::set_refresh→set_refresh_on_hit,LruTtlCache::refresh→refresh_on_hit,LruTtlCache::set_refresh→set_refresh_on_hit. - Breaking:
get_store()→store()onTtlCache,LruTtlCache, andUnboundCache(follows Rust API Guidelines C-GETTER). - Breaking:
TtlSortedCache::get_borrowedremoved;getis now generic (get<Q>(&self, key: &Q) where K: Borrow<Q>) socache.get("key")andcache.get(slice)work directly. - Breaking:
TtlSortedCache's inherentremove(&K)/clear()/len()/is_empty()/get<Q>(&self, ...)methods removed — they shadowed the same-namedCachedshort aliases without adding behavior. BringCachedinto scope and use the trait short aliases (cache.remove(&k)etc.) or the canonicalcache_*forms. The inherentgetwas the only one with a semantic difference: it was&selfand did not evict expired entries on access (the traitCached::getrequires&mut selfand does — it delegates tocache_get, which removes expired entries on access in this store). To preserve the previous&selfnon-evicting read behavior, useCachedRead::cache_get_readorCachedPeek::cache_peek. Both already implemented byTtlSortedCache. - Breaking: Renamed
CachedAsync::get_or_set_with→async_get_or_set_withandCachedAsync::try_get_or_set_with→async_try_get_or_set_with. The old names collided with the same-namedCachedconvenience methods (the in-memory stores implement both traits), so any call with both traits in scope (e.g.use cached::*;) failed to compile withE0034. The#[cached]/#[once]macros are unaffected — they call the canonicalcache_*methods. - Fix rustdoc links so documentation builds cleanly with warnings denied across feature combinations.
- Fix
examples/wasmbuild: addtime_storesfeature to thecacheddependency (required when usingdefault-features = falsewithTimedCache)
- Add
redis_async_cachefeature for Redis client-side caching support via the RESP3 protocol
- Update
redisto 1.0
- Add
parking_lotdependency
- Switch to
parking_lot'sMutexandRwLockin all macros. - Remove
unwrap()calls from lock operations.
- BREAKING All timed/expiring caches now use std::time::Duration values instead of raw seconds/millis.
- Update
redisto 0.32 - Update
hashbrownto 0.15
- Add
sync_writes = "by_key"support to#[cached]
- Update
redisto 0.29.0 - Update
directoriesto 6.0 - Update
thiserrorto 2.0 - With the
sync_writes = "by_key"addition, the argument values changed from a boolean to strings. The equivalent ofsync_writes = trueis nowsync_writes = "default"
- Add
Cached::cache_try_get_or_set_withfor parity with async trait
- Remove unnecessary string clones in redis cache store
- Update cargo default features manifest key
- Replace
instantwithweb_timein proc macro, update cached_proc_macro version
- Replace unmaintained
instantcrate withweb_time
- Propagate function generics to generated inner cache function
- Update
DiskCacheto requireToStringinstead ofDisplay
ExpiringSizedCache: Allow specifying explicit TTL when inserting
- Refactor
ExpiringSizedCacheinternals to not require tombstones ExpiringSizedCachekeys must implOrdExpiringSizedCacheremoveandinsertupdated to return only unexpired values
- Add
get_borrowedmethods toExpiringSizedCacheto support cache retrieval using&str/&[T]when the key types areString/Vec<T>. This is a workaround for issues implementingBorrowfor a generic wrapper type.
- Update documentation and add missing methods to
ExpiringSizedCache(clear, configuration methods)
ExpiringSizedCache: When allocating usingwith_capacity, allocate enough space to account for the default max number of tombstone entries
- Add
ExpiringSizedCacheintended for high read scenarios. Currently incompatible with the cached trait and macros.
- Add
DiskCacheBuilder::set_sync_to_disk_on_cache_changeto specify that the cache changes should be written to disk on every cache change. - Add
sync_to_disk_on_cache_changeto#[io_cached]to allow settingDiskCacheBuilder::set_sync_to_disk_on_cache_changefrom the proc macro. - Add
DiskCacheBuilder::set_connection_configto give more control over the sled connection. - Add
connection_configto#[io_cached]to allow settingDiskCacheBuilder::set_connection_configfrom the proc macro. - Add
DiskCache::connection()andDiskCache::connection_mut()to give access to the underlying sled connection. - Add
cache_unset_lifespanto cached traits for un-setting expiration on types that support it
- [Breaking]
typeattribute is nowty - Upgrade to syn2
- Corrected a typo in DiskCacheError (de)serialization variants
- Signature or
DiskCache::remove_expired_entries: this now returnsResult<(), DiskCacheError>instead of(), returning anErr(sled::Error)on removing and flushing from the connection.
- Fix
DiskCacheexpired value logic
- While handling cache refreshes in
DiskCache::cache_get, treat deserialization failures as non-existent values
- Fix
DiskCache::remove_expired_entriessignature
- Add DiskCache store
- Add
disk=true(and company) flags to#[io_cached]
- Include LICENSE file in
cached_proc_macroandcached_proc_macro_types
- Add
CloneCachedtrait with additional methods when the cache value type implementsClone - Add
result_fallbackoption tocachedproc_macro to support re-using expired cache values when utilizing an expiring cache store and a fallible function.
- Update redis
0.23.0->0.24.0
- Fix #once sync_writes bug causing a deadlock after ttl expiry, #174
- Add
ahashfeature to use the faster ahash algorithm. - Set
ahashas a default feature. - Update hashbrown
0.13.0->0.14.0
- Release
*_no_cachechanges from0.45.0. The change is in the proc macro crate which I forgot to release a new version of.
- Generate
*_no_cachefunction for every cached function to allow calling the original function without caching. This is backwards incompatible if you have a function with the same name.
tokiodependency has been removed fromproc_macrofeature (originally unecessarily included).asyncfeature has been removed from thedefaultfeature. This is a backwards incompatible change. If you want to useasyncfeatures, you need to enableasyncexplicitly.- remove accidental
#[doc(hidden)]on thestoresmodule
- Option to enable redis multiplex-connection manager on
AsyncRedisCache
-
Show proc-macro documentation on docs.rs
-
Document needed feature flags
-
Hide implementation details in documentation
-
Relax
Cachedtrait'scache_get,cache_get_mutandcache_removekey parameter. AllowK: Borrow<Q>likestd::collections::HashMapand friends. Avoids copies particularly onCached<String, _>where now you can docache.cache_get("key")and before you had tocache.cache_get("key".to_string()).Note: This is a minor breaking change for anyone manually implementing the
Cachedtrait. The signatures ofcache_get,cache_get_mut, andcache_removemust be updated to include the additional trait bound on thekeytype:fn cache_get<Q>(&mut self, key: &Q) -> Option<&V> where K: std::borrow::Borrow<Q>, Q: std::hash::Hash + Eq + ?Sized, {
- Dependency to
lazy_staticandasync_onceare removed.
- Update redis
0.22.0->0.23.0 - Update serial_test
0.10.0->2.0.0
- Better code generation for
#[cached]when thesync_writesflag is true.
- Fix "sized" cache types (
SizedCache,TimedSizedCache) to check capacity and evict members after insertion. - Fixes bug where continuously inserting a key present in the cache would incorrectly evict the oldest cache member even though the cache size was not increasing.
- Add optional feature flag
redis_ahashto enableredis's optionalahashfeature
- Update
redisto0.22.0 - Move
tokio'srt-multi-threadfeature from being a default to being optionally enabled byasync_tokio_rt_multi_thread - Fix makefile's doc target to match documentation, changed from
make synctomake docs
- Add flush method to ExpiringValueCache
- Fix proc macro argument documentation
- Disable futures
default-features - Add cache-remove to redis example
- Mark the auto-generated "priming" functions with
#[allow(dead_code)] - Fix documentation typos
- Replace dev/build scripts with a Makefile
- wasm support for non-io macros and stores
- Use
instantcrate for wasm compatible time
- Added
ExpiringValueCachefor caching values that can themselves expire. - Added COPYRIGHT file
- Make sure
AsyncRedisCacheBuilder,RedisCacheBuilder, andRedisCacheBuildErrorpublicly visible
- Replace
async-mutexandasync-rwlockused by proc-macros withtokio::syncversions - Add optional
versionfield toCachedRedisValuestruct - Cleanup feature flags so async redis features include
redis_storeandasyncfeatures automatically
- Allow specifying the namespace added to cache keys generated by redis stores
- Bump hashbrown 0.11.2 -> 0.12: https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md#v0120---2022-01-17
- Bump smartstring 0.2 -> 1: https://github.com/bodil/smartstring/blob/master/CHANGELOG.md#100---2022-02-24
- Fix redis features so
redis/aiois only included when async redis features (redis_tokio/redis_async_std) are enabled
- Fix how doc strings are handled by proc-macros. Capture all documentation on the cached function definitions and add them to the function definitions generated by the proc-macros. Add doc strings to generated static caches. Link to relevant static caches in generated function definitions. Add documentation to the generated cache-priming function.
IOCachedandIOCachedAsynctraitsRedisCacheandAsyncRedisCachestore types- Add
#[io_cached]proc macro for defining cached functions backed by stores that implementIOCached/IOCachedAsync
- Convert from travis-ci to github actions
- Update build status badge to link to github actions
- Add flush method to TimedSize and TimedSized caches
- Fix timed/timed-sized cache-get/insert/remove to remove and not return expired values
- proc-macro: support arguments of the wrapped function being prefixed with
mut
- Add failable TimedSize and SizeCached constructors
- Add
time_refreshoption to#[cached]to refresh TTLs on cache hits - Generate
*_prime_cachefunctions for every#[cached]and#[once]function to allow priming caches.
- Add
sync_writesoption to#[cached]macro to synchronize concurrent function calls of duplicate arguments. For ex, if a long running#[cached(sync_writes = true)]function is called several times concurrently, the actual function is only executed once while all other calls block and return the newly cached value.
- Add
#[once]macro for create aRwLockcache wrapping a single value - For all caches, add a function to get an immutable reference to their contents. This makes it possible to manually dump a cache, so its contents can be saved and restored later.
- Update deps hashbrown and darling, remove async-mutex from cached-proc-macro crate
- Add option to "timed" caches to refresh the ttl of entries on cache hits
- Add docs strings to the items generated by the
#cachedproc macro
cache_reset_metricstrait method to reset hits/misses
- Refactor cache store types to separate modules
- Add support for returning a
cached::Returnwrapper type that indicates whether the result came from the function's cache.
- Support mutual
size&timeargs in the cached proc macro. Added when TimedSizedCache was added, but forgot to release the cached_proc_macro crate update.
- Add a TimedSizedCache combining LRU and timed/ttl logic
- Add new CachedAsync trait. Only present with async feature. Adds two async function in the entry API style of HashMap
- Add type hint
_result!macros - remove unnecessary transmute in cache reset
- remove unnecessary clones in proc macro
- use
async-mutexinstead of fullasync-std
- Store inner values when
result=trueoroption=true. TheErrortype in theResultnow no longer needs to implementClone.
- add
cache_set_lifespanto change the cache lifespace, old value returned.
- fix proc macro when result=true, regression from changing
cache_setto return the previous value
- add
Cachedimplementation for stdHashMap
- trait
Cachedhas a new methodcache_get_or_set_with cache_setnow returns the previous value if any
- add Clone, Debug trait derives on pub types
- fix proc macro documentation
- proc macro version
- async support when using the new proc macro version
- Add
cache_get_muttoCachedtrait, to allow mutable access for values in the cache. - Change the type of
hitsandmissesto beu64.
- Add
value_ordermethod to SizedCache, similar tokey_order
- add
cache_resettrait method for resetting cache collections to their initial state
- Update
once_cellto 1.x
- Replace SizedCache implementation to avoid O(n) lookup on cache-get
- Update to Rust-2018 edition
- cargo fmt everything
- Replace inner cache when "clearing" unbounded cache
- Switch to
once_cell. Library users no longer need to importlazy_static
- Add
cache_clearandcache_resulttoCachedtrait- Allows for defeating cache entries if desired
- Update documentation
- Note the in-memory nature of cache stores
- Note the behavior of memoized functions under concurrent access
- Fixed duplicate key eviction in
SizedCache::cache_set. This would manifest whencachedfunctions called with duplicate keys would race set an uncached key, or ifSizedCachewas used directly.
- Add
cached_resultandcached_key_resultto allow the caching of success for a function that returnsResult. - Add
cached_controlmacro to allow specifying functionality at key points of the macro
- Add
cached_keymacro to allow defining the caching key
- Tweak
cachedmacro syntax - Update readme
- Update trait docs
- Update readme
- Update examples
- Update crate documentation and examples