v2.0.0
Upgrading from 1.1? See the 2.0 migration guide.
Breaking Changes
Minimum supported Rust version & edition
- 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.)
Trait API changes
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.
Macro attribute changes (#[cached], #[once], #[concurrent_cached])
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.
Store behavior changes
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.
Builder-only construction — build() returns Result, all store constructors removed
- 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.
size → max_size naming (builder setter, macro attribute, runtime setters)
- 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.
Added
New macro attributes
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.
New sharded in-memory cache stores
- 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.
Other additions
- 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.
Fixed
- 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.