Skip to content

v2.0.0

Choose a tag to compare

@jaemk jaemk released this 13 Jul 02:21
· 108 commits to master since this release
ee68f68

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's rust-toolchain.toml pins 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 the Cached trait that removes an entry and returns the stored key and value. Unlike cache_remove, this returns Some even when the deleted entry was already expired, making it possible to distinguish "key absent" from "key present but expired". Always fires the store's on_evict callback (if set).
  • ConcurrentCached::cache_remove_entry(&self, k: &K) -> Result<Option<(K, V)>, Self::Error>: same semantics on the concurrent trait; implemented for all nine concurrent stores (six sharded plus DiskCache / RedisCache / AsyncRedisCache). The seven non-sharded stores (UnboundCache, LruCache, etc.) gain cache_remove_entry via the Cached trait above.
  • Cached::cache_delete<Q>(&mut self, k: &Q) -> bool: new default method on Cached that deletes an entry without returning it; returns true if an entry was physically removed (including expired entries), false if the key was absent. Implemented via cache_remove_entry.
  • DiskCache and RedisCache / AsyncRedisCache now require K: Clone (in addition to existing bounds) for their ConcurrentCached / ConcurrentCachedAsync impls, which is needed to return the stored key from cache_remove_entry.
  • ConcurrentCached / ConcurrentCachedAsync mutators now take &self instead of &mut self: set_refresh_on_hit, set_ttl, and unset_ttl are defined with a shared receiver, matching the internally-synchronized &self contract of the rest of these traits (cache_set, cache_remove, …). This lets you flip the refresh flag or change the TTL on a shared store (e.g. one behind an Arc or a static) without exclusive access. Implementors must update their method signatures (fn set_ttl(&self, …) etc.); the bundled DiskCache / RedisCache / AsyncRedisCache stores do this via interior mutability (parking_lot::Mutex + AtomicBool). The single-owner Cached and CacheTtl traits are unaffected and keep their &mut self mutators.
  • ConcurrentCached::cache_size / ConcurrentCachedAsync::cache_size: new method fn cache_size(&self) -> Result<Option<usize>, Self::Error> reporting the number of entries, with a default of Ok(None). The default makes it non-breaking for existing external implementors and honest for stores that cannot cheaply produce a count: the six sharded stores override it to return Ok(Some(len)), while the external-store impls (DiskCache, RedisCache, AsyncRedisCache) keep the Ok(None) default because their backends (redb, Redis) expose no O(1) size. Sharded stores also retain their inherent len() / is_empty() for a non-Result count.

Macro attribute changes (#[cached], #[once], #[concurrent_cached])

  • result = true removed from #[cached] and #[once]: All Result<T, E> return types now automatically skip caching Err values. Remove result = true from all #[cached] and #[once] annotations — the behavior is now the default. To force-cache Err values, use the new cache_err = true opt-in.
  • option = true removed from #[cached] and #[once]: All Option<T> return types now automatically skip caching None values. Remove option = true from all #[cached] and #[once] annotations — the behavior is now the default. To force-cache None values, use the new cache_none = true opt-in.
  • #[concurrent_cached] now supports Option<T> returns: previously only Result<T, E> was accepted; Option<T> and plain T: Clone returns are now natively supported on the default in-memory sharded path. Note: option = true was never a recognized attribute on #[concurrent_cached] (it was silently ignored in 1.x); the new cache_none = true is the explicit opt-in to cache None values.
  • #[cached] / #[once] on fn() -> Option<T> without attributes: previously cached None as-is; now skips caching None. Add cache_none = true to preserve the old behavior.
  • #[cached] / #[once] on fn() -> Result<T,E> without attributes: previously cached the full Result; now skips caching Err. Add cache_err = true to preserve the old behavior.
  • result_fallback = true no longer requires result = true: the explicit result = true companion is dropped; result_fallback now auto-detects Result<T,E> return types.
  • Custom-ty users storing Option<T> or Result<T,E> directly: if your cache store type holds Option<T> or Result<T,E> as the value, you must now add cache_none = true or cache_err = true respectively so the macro uses the full wrapper type rather than extracting the inner T.
  • map_error on the default in-memory sharded path is now a compile error: previously map_error = "…" was silently accepted and ignored when the store was the infallible default. If you had map_error on a #[concurrent_cached] that uses no redis/disk/ty/create, remove it. If you still need map_error (because you are switching to a redis or disk backend), add the corresponding backend attribute.
  • result_fallback = true and with_cached_flag = true are mutually exclusive on #[concurrent_cached]: using both together is now a compile error. The combination was never valid — result_fallback stores the inner Ok(T) value while with_cached_flag wraps it in Return<T> — but the error was previously inscrutable. Remove one of the two attributes.
  • cache_none = true and with_cached_flag = true are mutually exclusive on #[cached], #[once], and #[concurrent_cached]: using both together is now a compile error. The combination was never valid — cache_none = true stores Option<T> as the cached value type while with_cached_flag = true stores the inner T — but the error was previously a confusing downstream type mismatch. Remove one of the two attributes.

Store behavior changes

  • cache_remove on expiring stores now returns None for expired-but-present entries. Previously ExpiringCache, ExpiringLruCache, and expiry-aware sharded stores returned Some(value) for an already-expired entry; now returns None. The entry is still removed and on_evict still fires.
  • ConcurrentCached::cache_delete (and its ConcurrentCachedAsync equivalent) now returns true for expired-but-physically-present entries. In 1.x the method returned false for such entries. Use cache_remove if you need to distinguish a live removal from an expired one.
  • LruCache::retain now fires on_evict and increments cache_evictions() for each removed entry, matching the semantics of cache_remove. Previously retain was side-effect-free. Internal TTL and expiring wrapper stores (LruTtlCache, ExpiringLruCache) use a new crate-internal retain_silent for their eviction sweeps, so those stores continue to count evictions exactly once.
  • DiskCacheBuildError gains a new InvalidTtl(BuildError) variant: any exhaustive match on DiskCacheBuildError must add an arm for InvalidTtl. This variant is returned when a DiskCacheBuilder is given a zero-duration TTL.
  • RedisCacheBuildError gains a new InvalidTtl(BuildError) variant: same as above for RedisCacheBuildError. Returned when a RedisCacheBuilder is given a zero-duration TTL.

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, every try_with_*, and the sharded new / with_shards / with_max_size[_and_shards] / with_ttl[_and_shards] / with_max_size_and_ttl[_and_shards] variants — across UnboundCache, LruCache, TtlCache, LruTtlCache, TtlSortedCache, ExpiringCache, ExpiringLruCache, and all six sharded stores. (DiskCache / RedisCache / AsyncRedisCache are unchanged: their new(...) / builder(...) already return a builder.) This removes the second, panic-prone construction path that duplicated the builder.
  • Builder::build now returns Result<Store, BuildError> for every in-memory and sharded store. It previously returned the store directly and panicked on invalid configuration. Add ? or .unwrap(). (Disk/Redis build() already returned Result; unchanged.)
  • try_build() is removed from all builders. Now that build() is the single fallible constructor the alias is redundant — replace every .try_build() with .build().
  • TtlSortedCacheBuilder gains .capacity(n) — the preallocation hint formerly supplied via TtlSortedCache::with_ttl_and_capacity. It is distinct from .max_size(n), which is the eviction bound.
  • Zero TTL is now always rejected. Because every store is built through its (validating) builder, a zero Duration yields BuildError::InvalidTtl. The previously-permissive direct constructors (e.g. TtlCache::with_ttl(Duration::ZERO)) that accepted a zero TTL no longer exist.

size → max_size naming (builder setter, macro attribute, runtime setters)

  • Builder setter .size(n) → .max_size(n) (LRU-family stores and TtlSortedCache). The sharded builders' per-shard cap setter is per_shard_max_size.
  • The #[cached] / #[concurrent_cached] macro attribute size = N → max_size = N. The old size = N spelling keeps working as a deprecated alias that emits a deprecation warning (anchored at the size token). Setting both on one annotation is a compile error. See "New macro attributes" under Added below.
  • TtlSortedCache runtime max-size setters: size_limit(n) → set_max_size(n) and try_size_limit(n) → try_set_max_size(n) (matching the set_ttl runtime-mutator convention). The error type also changed: try_set_max_size now returns Result<Option<usize>, cached::SetMaxSizeError> instead of std::io::Result<Option<usize>>; if you propagate the error with ? into an io::Error context, update the enclosing function's error type or convert explicitly.

Added

New macro attributes

  • max_size = N attribute for #[cached] and #[concurrent_cached]: the preferred spelling of the LRU-bound attribute, mirroring the renamed max_size builder setter. The original size = N attribute continues to work as a deprecated alias — using it emits a deprecation warning (anchored at the size token) steering you to max_size. Specifying both size and max_size on the same annotation is a compile error.
  • cache_err = true attribute for #[cached], #[once], and #[concurrent_cached]: opt-in to also cache Err values from Result<T, E> returns (requires a Result<T, E> return type; mutually exclusive with result_fallback).
  • cache_none = true attribute for #[cached], #[once], and #[concurrent_cached]: opt-in to also cache None values from Option<T> returns (requires an Option<T> return type).
  • result_fallback = true support for #[concurrent_cached]: on an Err return, the last cached Ok value for the same key is returned instead. The stale value is kept in the primary cache slot (via ConcurrentCloneCached::cache_get_with_expiry_status) and re-cached with a fresh TTL window on Err; no separate fallback store is created. Requires a TTL (ttl/ttl_secs/ttl_millis) (a compile error is emitted otherwise). Restricted to the default in-memory sharded path (not redis/disk). Mutually exclusive with cache_err and with_cached_flag.

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, requires time_stores), ShardedLruTtlCache<K,V> (LRU + TTL, requires time_stores), ShardedExpiringCache<K,V> (per-value expiry, unbounded), and ShardedExpiringLruCache<K,V> (per-value expiry, LRU-bounded). All six wrap an Arc (cheap clone, Send + Sync), use power-of-two per-shard parking_lot::RwLocks with cache-line-padded shard structs to eliminate false sharing, and support builder APIs with on_evict callbacks, copy_from for live resharding, and metrics() / shard_sizes() for observability. Shard routing uses the ShardHasher<K> trait (default: DefaultShardHasher backed by ahash) as a zero-overhead type parameter, allowing custom partition logic without runtime overhead.
  • #[concurrent_cached] now defaults to an in-memory sharded store when redis = true and disk = true are both absent and no custom ty/create is provided. Macro attributes max_size = N, ttl = T, shards = S, and expires = true select the matching variant. map_error must not be specified on this path — the stores are Infallible and have no errors to map (supply redis = true, disk = true, or a custom ty/create to use a fallible store).
  • #[concurrent_cached] on the default in-memory sharded stores now accepts plain return types — any T: Clone, Option<T>, or Result<T, E>. redis, disk, and custom ty/create stores still require Result<T, E>.
  • Add expires = true attribute support to #[concurrent_cached] macro to automatically select ShardedExpiringCache (unbounded) or ShardedExpiringLruCache (LRU-bounded when max_size is also set).
  • ShardedExpiringCache and ShardedExpiringLruCache require cached values to implement the Expires trait; copy_from skips entries already reporting is_expired() == true. Both expose deep_clone for snapshot copies.

Other additions

  • Add cache_clear_with_on_evict() to all six sharded stores (ShardedCache, ShardedLruCache, ShardedTtlCache, ShardedLruTtlCache, ShardedExpiringCache, ShardedExpiringLruCache): fires the on_evict callback for every removed entry when a callback is configured, and (where applicable) increments the evictions counter (ShardedCache is unbounded and has no evictions counter). The plain clear() inherent method remains fast and side-effect-free; cache_clear_with_on_evict() is the opt-in alternative.
  • Add cache_clear_with_on_evict() to all seven non-sharded stores (UnboundCache, LruCache, TtlCache, LruTtlCache, ExpiringCache, ExpiringLruCache, TtlSortedCache): fires the on_evict callback for every removed entry and (where applicable) increments the evictions counter. The plain cache_clear() method remains fast and side-effect-free; cache_clear_with_on_evict() is the opt-in alternative.
  • Add StripedCounter — a 16-slot cache-line-padded atomic counter — for hit/miss metrics on UnboundCache and TtlSortedCache to reduce false sharing under concurrent cache_get_read. All other stores continue to use plain AtomicU64.
  • Add ConcurrentCloneCached<K, V> trait: concurrent analogue of CloneCached for the four expiry-capable sharded stores (ShardedTtlCache, ShardedLruTtlCache, ShardedExpiringCache, ShardedExpiringLruCache). Provides cache_get_with_expiry_status(&self, key: &K) -> (Option<V>, bool) — returns the value without removing expired entries, enabling result_fallback to fall back to stale values in-place. Takes &self (not &mut self) since sharded stores are internally synchronized.
  • Add API consistency aliases: Cached::{get,set,remove,remove_entry,delete} and ConcurrentCached::{get,set,remove,remove_entry,delete} delegate to the existing cache_* methods (the sync Cached trait gains remove_entry / delete to match ConcurrentCached); both the sharded and non-sharded TTL builders expose .refresh_on_hit(...) as the primary setter with .refresh(...) retained as an alias; DiskCache, RedisCache, and AsyncRedisCache expose ::builder(...) aliases (alongside their existing ::new(...) builder entry points). Note: DiskCache::new(...) / RedisCache::new(...) / AsyncRedisCache::new(...) are builder entry points -- they return a builder, not a ready-to-use store -- and are intentionally retained; only the in-memory and sharded store constructors that returned stores directly were removed.
  • Add an inherent capacity() getter to LruCache, LruTtlCache, and ExpiringLruCache — and to their sharded counterparts ShardedLruCache, ShardedLruTtlCache, and ShardedExpiringLruCache — that returns the configured max-entry bound (distinct from cache_size(), which returns the current live entry count).
  • Add BuildError::InvalidTtl { ttl } variant for a single consistently-worded zero-TTL rejection path across all builders.
  • Document on ConcurrentCachedAsync that get/set/remove/delete short 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, and AsyncRedisCache builders now all call the shared validate_ttl helper and return BuildError::InvalidTtl { ttl }. With construction now builder-only, a zero TTL is uniformly rejected at build time (there is no longer a permissive direct-constructor path).
  • Make the generated #[concurrent_cached] in-memory Infallible error shim map into the function's declared Result<_, E> error type, reject invalid store-selection attributes, and use UFCS for generated ConcurrentCached calls so sync functions compile even when both concurrent traits are in scope.
  • Implement CacheEvict for ShardedTtlCacheBase and ShardedLruTtlCacheBase, make sharded builders return BuildError instead of panicking on capacity/shard overflows, avoid unnecessary 'static bounds when building ShardedLruTtlCache without on_evict, optimize ShardedTtlCacheBase hits under refresh_on_hit by bypassing read-locks, and correct the sharded LRU capacity documentation.
  • Fix timed-store eviction sweeps to use the crate's configured Instant type.
  • Optimize TtlSortedCache::cache_get and cache_get_mut live hits to use a single hash-map lookup.
  • Unify cache_remove semantics: removing any present entry now fires the store's on_evict callback (if set) and increments evictions.
  • Tighten #[concurrent_cached] return-type classification so generic plain return types like HashMap<K, V> are not mistaken for Result aliases.
  • Tighten Result-return detection in all three macros to require the exact identifier Result rather than matching any identifier that ends with "Result". Type aliases such as type MyResult<T> = Result<T, E> are now treated as plain values (their Err variant is cached). Only the literal Result<T, E> and its fully-qualified forms (e.g. std::result::Result<T, E>) continue to trigger skip-on-Err / result_fallback semantics. This aligns with the existing Option-detection behavior and makes the macro surface consistent.
  • Pass the stored key (via remove_entry) rather than the lookup key to on_evict in ShardedTtlCache::cache_remove and ShardedExpiringCache::cache_get / cache_remove.
  • #[concurrent_cached] now rejects map_error on the default in-memory sharded path with a compile error — the stores are Infallible and accepting map_error while silently ignoring it was misleading. Previously map_error on this path was accepted and the infallible path emitted .expect(…) regardless.
  • 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 plain cached::Return<T> returns.
  • Extend build() panic messages on all sharded stores to include the underlying BuildError detail.
  • Fix ShardedLruTtlCacheBase::evict() to remove expired inner entries without calling cache_remove, preventing double-counting of evictions and double-firing of on_evict.
  • Fix Cached::cache_delete (now on Cached via cache_remove_entry) correctly returns true for entries that were present but already expired; previously cache_delete on ConcurrentCached returned false for expired entries.