Skip to content

feat: macros, stores/traits, redis TLS split, runtime decoupling, constructor and ttl consistency#272

Merged
jaemk merged 44 commits into
masterfrom
260609.next-major-batch
Jun 21, 2026
Merged

feat: macros, stores/traits, redis TLS split, runtime decoupling, constructor and ttl consistency#272
jaemk merged 44 commits into
masterfrom
260609.next-major-batch

Conversation

@jaemk

@jaemk jaemk commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Breaking and additive changes across the macros, stores, and traits for the next major release: constructor and ttl-attribute consistency, a sync_writes = "by_key" default, async-runtime decoupling, trait/store ergonomics, redis serialization and connection-string changes, custom hashers on the in-memory stores, and the supporting docs and tests, on top of the redb DiskCache rewrite.

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for full migration details.

Breaking changes

  • Macro ttl attribute now takes a Duration expression (ttl = "core::time::Duration::from_secs(60)"). The old whole-seconds integer form (ttl = 60) is removed and emits a migration error pointing at ttl_secs / ttl_millis.
  • RedbCache / RedisCache / AsyncRedisCache drop new() (it returned a builder, not a cache); use builder(...).
  • get_or_set_with / try_get_or_set_with (and async variants) return &V instead of &mut V; new *_mut variants preserve the old behavior (Unnecessary &mut V with get_or_set_with and try_get_or_set_with (CachedAsync) #179).
  • Redis TLS features split so rustls is selectable; redis_tokio / redis_smol no longer imply native-tls (add *_native_tls or *_rustls) (Rustls support for Redis #231).
  • RedisCacheBuilder / AsyncRedisCacheBuilder build() reject an empty namespace+prefix scope (EmptyScope); RedbCacheBuilder::build() rejects an invalid cache_name (InvalidCacheName).
  • #[cached] defaults sync_writes = "by_key": concurrent first-calls for the same key dedup through bucketed per-key locks, instead of the previous no-synchronization double-compute (which mirrored Python lru_cache). Opt out with sync_writes = false. #[once] / #[concurrent_cached] defaults unchanged.
  • Cached gains an associated type Error; cache_try_set / try_set return Result<_, Self::Error>. Built-ins use Infallible (non-fallible stores) and CacheSetError (TtlCache / LruTtlCache / TtlSortedCache); custom impls must declare it.
  • The four CachedAsync shorthand methods get_async / set_async / remove_async / clear_async are renamed to async_cache_get / async_cache_set / async_cache_remove / async_cache_clear, so every CachedAsync method uses the async_cache_* namespace (ConcurrentCachedAsync already did).
  • Short method aliases (get / set / remove / clear / len / is_empty / the short get_or_set_with family, etc.) move off the core Cached / ConcurrentCached traits onto blanket extension traits CachedExt / ConcurrentCachedExt (re-exported from the crate root and the prelude). The core traits keep only the cache_-prefixed methods, so a custom store implements a smaller surface. Callers using cached::prelude::* need no change; others add use cached::CachedExt; / use cached::ConcurrentCachedExt;, or use the cache_-prefixed form. Custom impl Cached / impl ConcurrentCached blocks must drop any short-alias methods.
  • ShardedCache renamed to ShardedUnboundCache (with ShardedCacheBase / ShardedCacheBuilder renamed to match); the old name only described the unbounded variant. No deprecated alias; rename at the call site.
  • TtlSortedCacheError is removed; TtlSortedCache shares the unified CacheSetError with TtlCache / LruTtlCache.
  • TTL stores store per-entry expiry: set_ttl now applies to future inserts only (it no longer re-dates existing entries), and set_ttl(0) / unset_ttl() disable expiry for future inserts only. TtlSortedCache now treats a zero ttl as never-expires for future inserts as well (per-entry expiry is Option<Instant>, ordered so never-expiring entries evict last); it previously meant immediate expiry.
  • async feature no longer pulls tokio; smol / async-std async users no longer compile it. The async_tokio_rt_multi_thread feature and the RedbCacheError::BackgroundTaskFailed variant are removed. Async RedbCache runs blocking redb work on the blocking crate (runtime-agnostic), and cached::async_sync re-exports async-lock instead of tokio::sync.
  • The disk_store cargo feature is renamed to redb_store, and redis_ahash is removed.
  • cache_reset (and the concurrent / async variants) no longer preserves the preallocated backing capacity: it now does clear() + shrink_to(initial_capacity), which the allocator may satisfy with a smaller allocation. Recreate the cache instead of resetting it to retain the allocation.
  • TimedEntry is now pub(crate) (it was an unused public type after the store() accessors were removed).

Redis store

  • Redis values are serialized with MessagePack (rmp-serde) instead of JSON; the redis_store feature pulls rmp-serde (and keeps serde_json only as a read-only fallback). Pre-3.0 JSON entries are read transparently: the store tries MessagePack first, then falls back to serde_json for entries carrying a version key, serving the value without recompute. New writes use MessagePack; old entries are rewritten as MessagePack on their next write. RedisCacheError's serialize/deserialize variants carry rmp_serde error types instead of serde_json::Error.
  • Redis TTL uses the millisecond commands PSETEX / PEXPIRE; sub-second TTLs are honored to the millisecond instead of rounded up to the next whole second. Whole-second TTLs are unchanged. Requires Redis 2.6+.
  • RedisCache::connection_string() / AsyncRedisCache::connection_string() return a ConnectionString newtype whose Display and Debug both redact credentials; call .reveal() for the raw URL. connection_string_unredacted() is removed.
  • Store error enums are now struct variants (named fields); RedbCacheBuildError::Connection is renamed Storage, the serialize/deserialize variants carry MessagePack error types, and CacheDeserialization gains a cached_value: Vec<u8> field. Tuple patterns like CacheSerialization(e) become CacheSerialization { source }.
  • disk / redis / map_error on #[cached] / #[once] now emit a guiding error pointing at #[concurrent_cached] (they were never supported on the single-owner macros).

Additive

  • new() returning a ready-to-use cache on every in-memory and sharded store, all #[must_use] (LruCache::new(max_size), TtlCache::new(ttl), LruTtlCache::new(max_size, ttl), the Sharded* variants, etc.). new() now consistently returns a usable cache everywhere it exists.
  • Custom hash builder on the non-sharded in-memory stores: a S = DefaultHashBuilder type parameter and a .hasher(...) builder method on UnboundCache / LruCache / TtlCache / LruTtlCache / TtlSortedCache / ExpiringCache / ExpiringLruCache. The default keeps existing call sites source-compatible; DefaultHashBuilder is re-exported.
  • Concurrent metric accessors (cache_hits / cache_misses / cache_capacity / cache_evictions, plus a default metrics()) on ConcurrentCacheBase, so the sharded stores' aggregated metrics are reachable through a trait bound.
  • ttl_secs (whole seconds) and ttl_millis (sub-second) macro attributes; ttl / ttl_secs / ttl_millis are three-way mutually exclusive, and each is mutually exclusive with expires (Feature Request: Floating-Point ttl #149).
  • ttl_secs / ttl_millis convenience setters on every TTL builder (non-sharded, sharded, Redis, Redb); last-writer-wins with ttl(Duration).
  • force_refresh (per-call cache bypass) on all three macros, with result_fallback interaction and no read side effects on the bypassed entry (Feature Request: bool function argument that forces a cache refresh. #146).
  • in_impl to cache methods inside impl blocks; self-receiver methods require it and get a {fn}_no_cache sibling (macro: Not working inside impl blocks #16, Feature request: Skip self field to allow caching methods #140).
  • Reference arguments (&T, Option<&T>) form the default key without convert (proc_macro: support args which are &T and Option<&T> #202, Support &T and Option<&T> in input #203).
  • SerializeCached / SerializeCachedAsync (cache_set_ref) for serialize-backed stores; #[concurrent_cached] routes its set through them to avoid a value clone (Borrowed keys and values for IOCached::set_cache #196, Borrowed keys and values for IOCached::set_cache #195).
  • RedisCache / AsyncRedisCache gain cache_clear / async_cache_clear (Add cache_clear operation #200); LruCache::set_max_size / try_set_max_size with matching LruTtlCache / ExpiringLruCache methods (Feature: Ability to configure (or reconfigure) a SizedCache size based on runtime data #180); ConcurrentCloneCached::cache_peek_with_expiry_status.
  • Generated code resolves the crate path via proc-macro-crate (renamed dependency works, Allow reexport #157); macro bindings are hygienic (key argument name collision #230, Cannot use key as a function argument #114); clear error for generic functions without key + convert (How to aproach generics ? #80).
  • Unquoted code-valued macro attributes: convert / create / force_refresh / map_error / cache_prefix_block accept bare Rust (convert = { format!("{a}") }, map_error = |e| ...); the quoted-string forms still work. ty / key stay quoted (they hold types).
  • map_error optional on disk / redis #[concurrent_cached] when the error type implements From<store error> (the macro generates .map_err(Into::into)?).
  • Infallible inherent get / set / remove / remove_entry / delete / reset on the six sharded stores, returning unwrapped values (Option<V>, bool, ()), removing the .expect("...infallible") ceremony. The cache_* trait methods still return Result for generic code.
  • companions_vis macro attribute sets the visibility of the generated {fn}_no_cache / {fn}_prime_cache companions independently of the cached fn.
  • Expires::expires_at(&self) -> Option<Instant> default method (advisory observability; is_expired() stays the authoritative liveness check).

Documentation

  • Docs and examples prefer the short alias method API (get / set / remove / clear / len / ...) over the cache_*-prefixed forms, with a note on when to reach for the cache_* names (collision with another in-scope trait's method). The aliases now live on CachedExt / ConcurrentCachedExt (see Breaking changes); the prelude brings them in.
  • The README "Upgrading from 2.x?" banner points to the migration guide rather than enumerating breaking changes inline.
  • New specs/ directory documenting the current state and proposed work for the 3.0 effort, each item marked implemented, not implemented, or needs research.
  • Float convert guidance (Document how this should work on floats? #78), cache-invalidation and struct-method examples (Examples: cache invalidation #21, Add example of passing dyn trait instance #236), the redis backward-read / connection-string / millisecond-TTL behavior, and a broad accuracy pass across the macro/store docs and the migration guide.
  • LRU sharded stores take a write lock on read hits: documented as a known limitation with a planned separate read-optimized store type. Examples modernized to the unquoted attributes, the infallible sharded API, and the optional map_error path.

Tests

  • Positive macro coverage for all three ttl spellings across #[cached] / #[once] / #[concurrent_cached], and compile-fail UI goldens for the three-way exclusivity, the ttl = <int> migration error, and the disk / redis / map_error rejection on the single-owner macros.
  • Inline store coverage for every new new() constructor and the ttl_secs / ttl_millis builder setters (including override semantics).
  • Redis backward-read of pre-3.0 JSON entries, a structured value round-tripped through the MessagePack path, and sub-second TTL precision via PTTL (server-gated); a custom BuildHasher threaded through the non-sharded stores; aggregated cache_hits / cache_misses / cache_capacity / cache_evictions through ConcurrentCacheBase.
  • Clone-elision counts, force_refresh + result_fallback / in_impl, _mut trait coverage, redis/redb cache_set_ref round-trips, additional attribute/store combinations, and sharded peek-does-not-renew-TTL.
  • sync_writes = "by_key" default dedup, per-entry expiry (set_ttl future-only), the Cached::Error associated type, the infallible sharded inherent methods, unquoted-attribute parsing (positive and UI goldens), the async_cache_* rename and unified CacheSetError, TtlSortedCache never-expires-on-zero-ttl, Expires::expires_at, and async RedbCache under a non-tokio executor (futures::executor::block_on).

Verification

cargo build / clippy --all-features --all-targets clean; full make ci green (check + tests + examples), including redis-backed tests and examples against a local server and the trybuild UI goldens; doctests pass; cargo fmt --check clean; README regenerated from src/lib.rs and in sync. cargo tree confirms redis_smol and the async feature no longer pull tokio (it remains a dev-dependency only).

@jaemk jaemk changed the title feat!: macro attrs, trait refinements, redis TLS split, store APIs (next major) feat: macro attrs, trait refinements, redis TLS split, store APIs (next major) Jun 11, 2026
@jaemk jaemk force-pushed the 260609.next-major-batch branch 4 times, most recently from e7d16f6 to eee9213 Compare June 13, 2026 11:32
@jaemk jaemk changed the title feat: macro attrs, trait refinements, redis TLS split, store APIs (next major) feat!: next-major batch - macros, stores/traits, redis TLS, constructor + ttl consistency Jun 13, 2026
@jaemk jaemk force-pushed the 260609.next-major-batch branch from 9ea2c64 to 9d1ea5d Compare June 14, 2026 01:29
jaemk added 5 commits June 13, 2026 23:38
…sistency

Make constructors and the ttl attribute consistent across the public surface, on
top of the redb DiskCache rewrite. Breaking and additive changes with doc and test
follow-ups:

Macros:
- `ttl` attribute now takes a `Duration` expression
  (`ttl = "core::time::Duration::from_secs(60)"`); the old whole-seconds integer
  form (`ttl = 60`) is removed and emits a migration error pointing at
  `ttl_secs`/`ttl_millis`
- `ttl_secs` attribute for whole-second TTLs and `ttl_millis` for sub-second TTLs;
  `ttl` / `ttl_secs` / `ttl_millis` are three-way mutually exclusive, and each is
  mutually exclusive with `expires` (#149)
- `force_refresh` attribute to bypass the cache per call, on all three macros
  (`#[cached]` / `#[concurrent_cached]` / `#[once]`); on `#[once]` it overwrites
  the single shared value (no per-call key, so no key-exclusion caveat). Combined
  with `result_fallback`, a force-refreshed `Err` still serves the previously
  cached `Ok`, and capturing that fallback leaves no read side effects on the
  bypassed entry (no TTL renewal, recency update, or hit-counter change) on both
  `#[cached]` and `#[concurrent_cached]` (#146)
- `in_impl` attribute to cache methods inside `impl` blocks; `self`-receiver
  methods require `in_impl` (a `convert` block alone cannot rescue them, since the
  cache static cannot live at `impl` scope) and emit a `{fn}_no_cache`
  cache-bypass sibling, hidden from rustdoc via `#[doc(hidden)]` (#16, #140)
- reference arguments (`&T`, `Option<&T>`) form the default key without `convert` (#202, #203)
- generated code resolves the crate path via `proc-macro-crate`, so a renamed or
  re-exported `cached` dependency works (#157)
- macro-introduced bindings are hygienically named, so arguments named `key` /
  `cache` / `result` no longer collide with generated code (#230, #114)
- clear compile error for generic functions used without `key` + `convert` (#80)
- `refresh` is rejected alongside the other store-builder attributes when a
  `create` block is supplied, mirroring `#[concurrent_cached]`
- the shared `force_refresh` guard builder is factored into one helper

Traits and stores:
- every in-memory and sharded store gains a `new()` that returns a ready-to-use
  cache (zero-config stores take no args; required-field stores take them
  positionally - `LruCache::new(max_size)`, `TtlCache::new(ttl)`,
  `LruTtlCache::new(max_size, ttl)`, the `Sharded*` variants, etc.), all
  `#[must_use]`. `RedbCache` / `RedisCache` / `AsyncRedisCache` drop `new()` (it
  returned a builder, not a cache); use `builder(...)`. `new()` now consistently
  returns a usable cache everywhere it exists
- `new()` / `builder()` on each sharded `*Base` type are now defined only on the
  default-hasher specialization (`*Base<K, V, DefaultShardHasher>`, the named
  alias). They previously sat on the generic `*Base<K, V, H>` impl but always
  returned a `DefaultShardHasher` builder, so a `*Base::<_, _, CustomHasher>::
  builder()` / `::new()` turbofish compiled yet silently dropped the custom
  hasher; that turbofish now fails to compile (E0599). A custom hasher is
  specified via `ShardedX::builder().hasher(h)`, which switches the builder's
  hasher type; alias-based construction and the `.hasher()` path are unchanged
- every TTL builder (non-sharded, sharded, Redis, Redb) gains `ttl_secs` /
  `ttl_millis` convenience setters alongside `ttl(Duration)`; last-writer-wins
- `get_or_set_with` / `try_get_or_set_with` (and the async variants) now return
  `&V` instead of `&mut V`, with new `*_mut` variants preserving the old behavior (#179)
- new additive `SerializeCached` / `SerializeCachedAsync` traits (`cache_set_ref`)
  for serialize-backed stores; `#[concurrent_cached]` routes its set through an
  autoref shim that uses the borrowed setter for any store implementing the trait
  (built-in redis/disk or a custom `ty`/`create` store), avoiding a value clone (#196, #195)
- `RedisCache` / `AsyncRedisCache` gain `cache_clear` / `async_cache_clear`, with
  `cache_reset` delegating to `cache_clear`; `RedisCacheBuilder` /
  `AsyncRedisCacheBuilder` `build()` reject an empty namespace+prefix scope
  (`EmptyScope`) so `cache_clear` cannot `SCAN MATCH *` the whole database (#200)
- `RedbCacheBuilder::build()` validates `cache_name` as a filename component,
  returning `InvalidCacheName` for a path separator (`/` or `\`) or a
  path-traversal component (`.` / `..`)
- `LruCache::set_max_size` / `try_set_max_size` for resizing a live cache, with
  matching methods on `LruTtlCache` and `ExpiringLruCache` (#180)
- `ConcurrentCloneCached` gains a non-renewing `cache_peek_with_expiry_status`
  (used to capture a `result_fallback` stale value without read side effects)

Redis:
- TLS features split so rustls is selectable; `redis_tokio` / `redis_smol` no
  longer imply native-tls (add `*_native_tls` or `*_rustls`) (#231)

Docs and process:
- docs and examples now prefer the short alias method API
  (`get`/`set`/`remove`/`clear`/`len`/...) over the `cache_*`-prefixed forms, with
  a note (in the crate docs and on the `Cached` trait) on when to use the `cache_*`
  names - a collision with another in-scope trait's method. Both forms remain; no
  method was removed
- document floats as the canonical `convert` case (#78); add cache-invalidation
  and struct-method examples (#21, #236), the latter demonstrating `in_impl` and
  noting the in_impl cache is not externally invalidatable and its shared-id
  staleness footgun
- release workflow tags and creates a GitHub release for each published workspace
  crate on publish, via `bin/tag-release.sh` (root `cached` -> `vX.Y.Z`, subcrates
  -> `<crate-name>-vX.Y.Z`) (#245); it retries a missing release when the tag
  already exists on the remote; the CI tag-release git identity is scoped to `--local`
- fix a doctest under `--no-default-features` (#260)
- exclude internal dev tooling (.agents, .claude, .github, bin, docs/dev, AGENTS.md,
  CLAUDE.md) from the published crate via Cargo `exclude`
- README is generated from src/lib.rs via cargo-readme (regenerated here); dev
  tooling: pr-review shards reviewers across model-sized randomized chunks; pr-cycle
  fans fix application across disjoint sub-agents
- documentation accuracy pass: `ttl_millis` / `ttl_secs` are valid with
  `result_fallback` and (on the in-memory path) need `time_stores`; on `#[cached]`
  the "honored exactly" claim is scoped to the default in-memory store; Redis rounds
  `ttl_millis` up to the next whole second (500ms -> 1s, 1500ms -> 2s); `in_impl`
  emits a `#[doc(hidden)]` `{fn}_no_cache` sibling (all three macros);
  `#[concurrent_cached]` `force_refresh` documents the `result_fallback` interaction;
  the `#[concurrent_cached]` attribute table reaches parity with `#[cached]`; wasm is
  incompatible with `redis_connection_manager` / `redis_async_cache`; the async
  `V: Sync` clone-elision asymmetry and how a `Send + !Sync + !Clone` value surfaces
  at the generated set site; `CachedAsync` shared-ref default Send bounds;
  `Cached::cache_get_or_set_with` / `cache_try_get_or_set_with` flagged as provided
  defaults; migration-guide `&mut V` -> `&V` coercion caveat; redis `build()` lists
  `InvalidTtl` before `EmptyScope` to match validation order
- macro-output polish: ASCII-only macro diagnostics; no dead `#[doc]` on the
  function-local `in_impl` cache static; documented rationale for `#[allow(dead_code)]`
  on the generated prime companion and for the `crate_path()` `::cached` fallback
- elevate all compiler warnings to errors workspace-wide via the Cargo `[lints]`
  table (`[workspace.lints.rust] warnings = "deny"`, opted into by the `cached`
  and `cached_proc_macro` packages), so `cargo build`/`test`/`clippy` deny
  warnings rather than only the Makefile clippy target; gate `validate_ttl` to
  `any(time_stores, disk_store, redis_store)` and an unused test `AtomicUsize`
  import to `proc_macro` to keep `--no-default-features` warning-clean

Tests:
- positive macro coverage for all three ttl spellings (`ttl` Duration expr /
  `ttl_secs` / `ttl_millis`) across `#[cached]` / `#[once]` / `#[concurrent_cached]`,
  and compile-fail UI goldens for the three-way exclusivity and the `ttl = <int>`
  migration error
- inline store coverage for every new `new()` constructor and the `ttl_secs` /
  `ttl_millis` builder setters (including override semantics)
- compile-fail UI golden asserting `*Base::<_, _, CustomHasher>::{new,builder}()`
  no longer compiles (the custom-hasher `.hasher()` path stays covered by the
  existing sharded `custom_hasher` unit test)
- async clone-elision clone-count assertions; `#[concurrent_cached]`
  `result_fallback` + `force_refresh` (including the no-read-side-effects bypass);
  `#[once]` `force_refresh`; `force_refresh` + `in_impl`; generic-rejection goldens;
  `_mut` trait coverage on `ExpiringCache` / `TtlSortedCache`; redis `cache_clear` /
  `cache_set_ref` and redb `cache_set_ref` round-trips; deferred attribute/store
  combinations (max_size+ttl_millis, in_impl+ttl_millis, ttl_millis+force_refresh,
  result_fallback+ttl_millis, async once in_impl) and store(0) counter resets;
  sharded peek does not renew TTL under refresh_on_hit

See docs/migrations/2.0-to-unreleased.md and the CHANGELOG for migration details.
… sharded must_use, redis tests

Docs:
- CHANGELOG: stop claiming RedbCache/RedisCache/AsyncRedisCache ::new are
  retained (the type-level ::new were removed; only *Builder::new remain);
  mark ttl_millis as new this release, not pre-existing; result_fallback
  requires ttl/ttl_secs/ttl_millis.
- migration guide: RedbCache::new -> builder in the cache-name example;
  ttl_millis marked new.
- proc-macro docs: ttl now takes a Duration-expression string, document
  ttl_secs, fix 3-way exclusivity wording; once.rs comments name the
  generated <fn>_no_cache sibling (was __cached_inner).
- README (via src/lib.rs): time_stores also required for #[once] TTL forms;
  result_fallback ttl wording.
- AGENTS.md: CacheMetrics field entry_count (was size); _prime_cache not
  emitted for in_impl; document force_refresh/in_impl; store table notes
  ttl_millis / ttl=expr select the TTL stores.

Code/tests:
- redb: reject NUL byte in cache_name (InvalidCacheName) + test.
- LruTtlCache::set_max_size: own the documented zero-size panic.
- sharded ExpiringLru/LruTtl/Ttl builder(): add #[must_use].
- redis: fix AsyncRedisCacheBuilder::new doc; test namespace-only clear pattern.
- tests: UnboundCache cache_try_get_or_set_with_mut Err path; redis
  set_ref_round_trip clears first for idempotency.
…rename next_major_* tests to v3_*

Macro codegen:
- cached/concurrent_cached: reject const-generic functions without `convert`,
  extending the existing type-generic guard (const params have the same
  monomorphic-static naming problem). Lifetime-only generics still compile.
- cached: hoist the force_refresh predicate into a single binding on the
  `unsync_reads` + `SyncWriteMode::Default` path. It was previously expanded in
  both the optimistic read-lock block and the write-lock re-check, so a
  side-effecting predicate evaluated twice per call.

Stores:
- sharded ttl/lru_ttl: deflake the expiry timing tests (shorter TTLs, wider
  sleep-to-ttl ratio).

Release infra:
- tag-release.sh: fixed-string tab-anchored tag matching, deduped remote
  checks, corrected backfill-semantics comments.
- release.yml/build.yml: corrected comments, bump actions/checkout to v6.

Tests:
- new UI goldens: const-generic-without-convert and unparseable-ttl-duration
  for the relevant macros.
- new coverage: lifetime-only generics compile and cache; unsync_reads with
  and without force_refresh; force_refresh single-eval; in_impl `_no_cache`
  sibling bypass (given its own struct/counter to avoid a parallel-run race).
- rename tests/next_major_* to tests/v3_* (the next major is 3.0).
…uble-eval; default-key Option<&mut T>

- `#[cached(result_fallback, force_refresh)]`: an expired entry is now still served as the
  stale fallback when a bypassed recompute returns `Err`. The bypass path captured the
  fallback with `cache_peek`, which drops expired entries; it now uses a new non-renewing
  `CloneCached::cache_peek_with_expiry_status` that surfaces expired entries with no read
  side effects. Defaulted trait method (non-breaking), overridden in the single-owner TTL
  stores (`ttl`, `ttl_sorted`, `expiring`, `expiring_lru`, `lru_ttl`).
- `#[once(sync_writes, force_refresh)]`: hoist the predicate into a single binding so it is
  evaluated once per call instead of twice on the write path, matching `#[cached]`.
- default-key `Option<&mut T>`: use `as_deref()` so the non-`Copy` option is not moved before
  the generated `_no_cache` call.
- docs: revert a stray `::new`-removal note out of the frozen 2.0.0 changelog section, name
  the `DiskCache` alias in the Unreleased breaking entry, document the unused force_refresh
  flag in the README, and list the NUL byte in the `InvalidCacheName` doc.
Docs and comments only, no behavior
change:

- result_fallback rustdoc enumerated only `ttl`/`ttl_millis` but the gate is
  `has_ttl`, which `ttl_secs` also satisfies; normalize the four sites in
  cached_proc_macro to `ttl`/`ttl_secs`/`ttl_millis`.
- force_refresh note was self-contradictory (claimed the body never sees
  `refresh`, then told the user to silence its unused-variable warning); the
  generated body does receive `refresh`. Reworded in src/lib.rs.
- Method-naming docs wrongly attributed `async_cache_*` to CachedAsync; only
  ConcurrentCachedAsync uses that spelling. CachedAsync uses the
  async_get_or_set_with family.
- Example run-command headers named `redis_tokio`/`redis_smol` but the
  examples' required-features demand the `_native_tls` variants; copy-paste
  now works.
- CHANGELOG: drop stale "(seconds)" on ttl, add ttl_secs to the ttl_millis
  exclusion set, fix broken `buildd` migration anchor.
- AGENTS.md: scope `max_size` to #[cached]/#[concurrent_cached] (not #[once]).
- Store doc nits: document LruTtlCache::iter_order, point lru.rs build() links
  at the BuildError variants, correct the redb cache_name traversal comment.
- Fix a misleading peek-expiry test comment; ASCII-ify em-dashes in changed
  examples and one ui fixture.

Regenerated README.md from src/lib.rs.
@jaemk jaemk force-pushed the 260609.next-major-batch branch from 4ce955b to 1363073 Compare June 16, 2026 15:09
jaemk added 17 commits June 17, 2026 00:39
Breaking: `redis_async_cache` no longer implies native-tls. It now pulls
`redis_tokio` (TLS-agnostic) + `redis/cache-aio`; add `redis_tokio_native_tls`
or `redis_tokio_rustls` alongside if TLS is required. This folds the feature
into the same uniform rule as `redis_tokio`/`redis_smol` (issue #231) instead
of leaving it as the lone TLS-bundling exception. Updated the example's
required-features and run-command, the crate feature-list rustdoc (and the
generated README), the CHANGELOG TLS-split entry, and migration section 8.
The Makefile CI target `redis-async-cache-smol` (an invalid Tokio-runtime +
smol-TLS combo once the feature went agnostic) becomes `redis-async-cache-rustls`,
checking `redis_tokio_rustls,redis_async_cache` and adding the rustls CI coverage
that was previously missing.

Macro codegen nits:
- Gate the unsync_reads (cached) and once force_refresh binding so no constant
  `let __cached_force_refreshing = if true { false } else { true }` is emitted
  when force_refresh is absent; the force_refresh-present path is unchanged.
- Reorder the `expires`-vs-`ttl` mutual-exclusion checks ahead of ttl parsing
  in all three macros, so `expires = true` with a malformed `ttl` surfaces the
  exclusion error rather than a parse error.

Test coverage:
- Add the missing async owned-store cache-hit clone assertion in
  serialize_set_dispatch, mirroring the sync sibling.
- Add a `concurrent_cached` refresh-in-create conflict trybuild fixture (parity
  with the `cached` one).
- Add expires+malformed-ttl trybuild fixtures for all three macros to lock in
  the reorder (existing exclusive fixtures used a valid ttl and passed either
  way).
…:Error` to `TtlSortedCacheError`

Breaking changes for the v3 release:

- Rename the unbounded sharded store `ShardedCache` (and its `ShardedCacheBase`/
  `ShardedCacheBuilder`) to `ShardedUnboundCache`. The old name read as the umbrella
  for the whole sharded family while it was only the unbounded variant; the new name
  is parallel with `ShardedLruCache`, `ShardedTtlCache`, etc. Clean break, no alias.
- Rename `ttl_sorted::Error` to `TtlSortedCacheError` and drop the old public alias.
  Also remove the dead `From<ttl_sorted::Error> for std::io::Error` impl (unused; the
  store never surfaces errors through `io::Error`).

Updates the `#[concurrent_cached]` codegen, re-exports, doc tables, examples, tests,
benches, and trybuild fixtures.
…dant re-export

Breaking and additive trait-surface changes for the v3 release:

- Make `cache_peek_with_expiry_status` a required method on `CloneCached` and
  `ConcurrentCloneCached`. The old provided defaults returned a wrong/no-op result that
  silently broke `force_refresh` + `result_fallback` for external stores; every built-in
  store already overrides it, so only out-of-tree impls are affected.
- Make `cache_clear` and `cache_reset` required on `ConcurrentCached` and
  `ConcurrentCachedAsync`. Their no-op `Ok(())` defaults silently did nothing; all
  built-in stores already override both. `cache_reset_metrics` keeps its default (a
  legitimate no-op for the metric-less redis/redb stores).
- Add `CacheTtl::try_set_ttl`, a validated variant of `set_ttl` that returns the new
  `SetTtlError::ZeroTtl` instead of storing a zero ttl (which silently makes every
  inserted entry expire on insertion). Additive: provided default, no impl changes.
  Also correct the `set_ttl` docs, which wrongly claimed it panics on a zero ttl.
- Add ergonomic `len`/`is_empty` aliases on `ConcurrentCached`/`ConcurrentCachedAsync`,
  mirroring the sync `Cached` trait.
- Remove the redundant `#[doc(hidden)] pub use web_time;` re-export (the `pub use
  web_time as time;` alias is unaffected) and a redundant intra-doc link target.
…sh` a plain bool, improve macro parse errors

Proc-macro surface changes for the v3 release:

- Remove the `unbound` attribute from `#[cached]`. It was redundant: the default
  store (no `max_size`, `ttl`, or `expires`) is already an `UnboundCache`, so
  `#[cached(unbound)]` built an identical store to a bare `#[cached]`. The attribute
  is intercepted with a migration error pointing at the bare-`#[cached]` replacement
  rather than a generic "unknown field". Breaking: drop `unbound` from your attributes.
- Collapse `#[concurrent_cached]`'s `refresh` from `Option<bool>` to `bool`, matching
  `#[cached]`. `refresh = false` is now the default and no longer conflicts with
  `expires` or a `create` block (only `refresh = true` does); the redis/disk codegen
  always emits `refresh_on_hit(refresh)`, which is a no-op for the false default.
- Add context to the `key` and `convert` parse errors: a malformed `key`/`convert`
  now explains what the attribute expects with an example instead of surfacing a bare
  `syn` "unexpected token".
…d Debug/PartialEq, simplify redis features

Store and Cargo changes for the v3 release.

Breaking:
- Remove the inherent `refresh_on_hit`/`set_refresh_on_hit` methods on `TtlCache` and
  `LruTtlCache`. They shadowed the `CacheTtl` trait methods, and the inherent setter
  returned `()` instead of the previous value. Bring `CacheTtl` into scope to call them;
  `set_refresh_on_hit` now returns the previous `bool`. The builder `refresh_on_hit` is
  unchanged.
- Remove the `wasm` cargo feature. It gated nothing - `web-time` provides wasm-compatible
  time types transparently with no opt-in feature, so the flag was a no-op. Drop it from
  your feature list; nothing else is needed for wasm targets.

Additive:
- Implement `Debug` for `RedisCache`, `AsyncRedisCache`, and `RedbCache` (redacted: only
  namespace/prefix/path/ttl/refresh, never connection strings or credentials).
- Implement `PartialEq`/`Eq` for `ExpiringCache` and `ExpiringLruCache` (equal when their
  stored entries are equal).
- Add `#[must_use]` parity and spread the `with_hasher` doc alias across the sharded builders.

Other:
- Simplify `redis_connection_manager` to build on `redis_tokio` instead of re-listing its
  redis sub-features (resolved feature set unchanged).
- Bump `cached_proc_macro_types` to edition 2024 / rust 1.89, matching the workspace.
- Fix the `ConcurrentCached` trait doc example to implement the now-required `cache_clear`
  and `cache_reset`.
…lease

Document the breaking and additive changes and reconcile the existing
Unreleased entries they invalidated:

- Add changelog entries for the `ShardedUnboundCache` / `TtlSortedCacheError` renames,
  the now-required `cache_clear`/`cache_reset`/`cache_peek_with_expiry_status`, the
  removed `unbound` attribute and inherent ttl refresh setters, the `refresh` bool, the
  removed `wasm` feature, and the additive `try_set_ttl`/`SetTtlError`, concurrent
  `len`/`is_empty`, `Debug`, and `PartialEq`/`Eq` impls.
- Reconcile the changelog/migration notes that described `cache_clear`/`cache_reset` and
  `cache_peek_with_expiry_status` as no-op/non-meeting defaults: they are now required.
- Add migration sections 15-21 with detection/action and update the VERIFY checklist.
- Rename `ShardedCache` references in the migration guide and AGENTS.md store table.
- Regenerate README.md from the updated lib.rs docs.
- Remove the stale `unbound` attribute from the `#[cached]` macro rustdoc; the
  attribute was removed this release, so the docs no longer advertise it.
- Correct the `ExpiringLruCache` `PartialEq` doc: equality is membership-based and
  does not compare LRU recency order (the "same LRU order" claim was wrong).
- Document that the sharded stores' inherent `len`/`is_empty` shadow the new
  `ConcurrentCached`/`ConcurrentCachedAsync` trait aliases, and how to call the trait
  methods via fully-qualified syntax; note the `is_empty` `Ok(Some(false))` case.
- Warn on the concurrent `set_ttl` that a zero ttl is stored unchecked and that the
  validated `try_set_ttl` lives on the single-owner `CacheTtl` trait.
- Note in the migration guide that the TTL stores intentionally omit `PartialEq`/`Eq`.
…or sources, add Clone

Self-contained additive changes:

- Validate the `name` attribute on `#[cached]`/`#[once]`/`#[concurrent_cached]` via
  `syn::parse_str::<Ident>` and emit a spanned "`name` must be a valid Rust identifier"
  error instead of panicking in `Ident::new` on a bad name.
- Reject the `#[cached]`-only sync attributes on the other macros with a clear
  "`<attr>` is not supported on `#[<macro>]`" message: `sync_lock`/`unsync_reads` on
  `#[once]` and `#[concurrent_cached]`, and `sync_writes_buckets` on `#[concurrent_cached]`.
  `sync_writes`/`sync_writes_buckets` remain valid on `#[once]` (they drive its codegen).
- Wire `#[source]` on `RedisCacheBuildError::MissingConnectionString` and
  `RedisCacheError::CacheDeserialization`/`CacheSerialization`, and switch their Display
  from `{error:?}` to `{error}` so error chains are reachable and render cleanly.
- Implement `Clone` for `RedisCache` and `AsyncRedisCache` (Arc-backed pool / cloneable
  connection); `RedbCache` stays non-Clone. Make `Clone` a supertrait of `ShardHasher`,
  matching the de-facto bound on `deep_clone`/`copy_from`.
… `DiskCache`/`store()`, must_use

Coupled main-crate trait cluster:

- Replace `Box<dyn Error>` on `Cached::cache_try_set`/`try_set` with a concrete
  `#[non_exhaustive] CacheSetError` (variant `TimeBounds`), re-exported from the crate
  root. `TtlSortedCache` maps its expiry-overflow error onto it. Callers can now match.
- Add defaulted `cache_get_or_set_with`/`get_or_set_with` to `ConcurrentCached` and
  `async_cache_get_or_set_with` to `ConcurrentCachedAsync` (get-then-set, documented
  non-atomic), closing the central-primitive gap versus the single-owner traits.
- Default `set_refresh_on_hit` to `{ false }` on both concurrent traits (was required
  boilerplate for every custom impl) and add a defaulted `refresh_on_hit` getter.
- Remove the `DiskCache`/`DiskCacheBuilder`/`DiskCacheError`/`DiskCacheBuildError`
  aliases; use the `Redb*` names.
- Remove the `store()` accessors from `UnboundCache`/`TtlCache`/`LruTtlCache`/
  `ExpiringLruCache`; they leaked the internal `TimedEntry` wrapper and pinned the
  representation.
- Add `#[must_use]` to the pure-query trait methods (`cache_size`/`len`/`is_empty`/
  `metrics`/`hits`/`misses`/`ttl`/...), to `cache_remove`/`cache_remove_entry`, and to
  `ConcurrentCacheEvict::evict`. The short `remove`/`remove_entry` aliases stay
  un-annotated for ergonomic for-effect removal.
Docs, toolchain, and feature-gate updates:

- Raise `cached_proc_macro`'s rust-version to 1.89, matching the workspace. Collapse a
  now-lintable nested `if let` in the macro into a let-chain (the MSRV bump enables
  clippy's let-chain suggestion).
- Document all batch items in the CHANGELOG: breaking (typed `cache_try_set`, `DiskCache`
  alias removal, `store()` removal, `ShardHasher: Clone`, must_use) and additive
  (concurrent `get_or_set_with`, `refresh_on_hit` getter / defaulted setter, redis
  `Clone`, `name` validation, attr rejection, error sources, `evict` must_use).
- Add migration-guide sections 22-25 and the matching VERIFY entries.
- Drop the stale `DiskCache` alias note from the disk example comment.
- Regenerate README.md from the updated lib.rs docs.
…d errors, macro guards

Breaking (v3):
- builders: every store uses a no-arg `::builder()`; required fields move to setters validated in `build()` (`RedbCache::builder().name(..)`, `RedisCache`/`AsyncRedisCache` `.prefix(..).ttl(..)`)
- rename the four `CachedAsync` methods to `async_cache_*` (`ConcurrentCachedAsync` unchanged)
- remove `BuildError::InvalidTtl`; a zero ttl is now `InvalidValue { field: "ttl", .. }`; rename `RedisCacheBuildError`/`RedbCacheBuildError` `InvalidTtl` to `Build(BuildError)`
- sharded `set_ttl(0)` no longer panics
- reject `#[cached(refresh = true)]` without a ttl

Non-breaking:
- `#[once]` rejects the `#[cached]`-only attrs (`result_fallback`, `refresh`, `max_size`, `ty`, `create`, `key`, `convert`)
- add `#[must_use]` to `CacheEvict::evict` and the single-owner inherent `evict`
- emit a helpful error for a non-string `force_refresh`
- fix the `Duration` import under no default features
- update changelog, migration guide, AGENTS.md, README
Breaking (v3): a zero `Duration` passed to `set_ttl` now disables expiry,
identical to `unset_ttl()`, uniformly across `ShardedTtlCache`,
`ShardedLruTtlCache`, `TtlCache`, `LruTtlCache`, and `RedisCache`/`AsyncRedisCache`.
Previously the sharded stores panicked and the others errored on the next write.

- sharded stores: collapse the `ttl_set` bool and `ttl_nanos` into a single
  `AtomicU64` (0 = disabled), removing the prior torn-read window
- single-owner stores: route liveness through an `entry_live` helper (zero ttl =
  always live)
- redis: a disabled ttl writes keys without expiry (plain `SET`, no `EX`);
  refresh-on-hit leaves a pre-existing key TTL intact rather than persisting it
- `build()` and `try_set_ttl(0)` still reject zero; disable via `unset_ttl`/`set_ttl(0)`
- `TtlSortedCache` semantics unchanged (out of scope)
- update set_ttl docs, changelog, migration guide, README; add single-owner,
  redis, and sharded zero-ttl coverage
… links

- redb: `set_ttl(0)` now disables expiry (== `unset_ttl`) like the other stores,
  instead of expiring entries immediately - the zero-ttl=disabled work had missed
  the disk store. Sync and async impls, with a regression test.
- document that `TtlSortedCache` cannot disable expiry (a zero ttl expires entries
  immediately there) and soften the `CacheTtl::set_ttl` contract doc accordingly
- fix the crate-doc `AsyncRedisCache::builder` example to the no-arg builder form
  (it still used the removed positional arguments)
- derive `Clone, PartialEq, Eq` on `CacheSetError` and `TtlSortedCacheError` for
  parity with `SetMaxSizeError`/`SetTtlError`
- document the `MissingRequired` build error at the redis/redb `builder()` entry
  points, add a match snippet to the `*BuildError` types, and note the redis
  namespace vs prefix layout in the crate docs
- repair broken (`Self::unset_ttl`) and redundant intra-doc links so `cargo doc` is clean
…iguity

Breaking (v3): `ConcurrentCached` and `ConcurrentCachedAsync` each declared the same
eight synchronous helpers (`cache_size`/`len`/`is_empty` and `ttl`/`set_ttl`/`unset_ttl`/
`refresh_on_hit`/`set_refresh_on_hit`), so any type implementing both (`RedbCache`, every
sharded store) hit `error[E0034]: multiple applicable items in scope` when both traits were
in scope - which the prelude does. The single-owner side avoids this because `Cached` is a
shared core that async stores also implement; the concurrent side had no such base.

- add `ConcurrentCacheBase` (associated `Error` + `cache_size`/`len`/`is_empty`) as a
  supertrait of both `ConcurrentCached` and `ConcurrentCachedAsync`; the helpers are now
  declared once
- add `ConcurrentCacheTtl` (`ttl`/`set_ttl`/`unset_ttl`, a default `try_set_ttl` that rejects
  a zero `Duration` with `SetTtlError::ZeroTtl`, and the refresh getters), implemented only by
  TTL-capable concurrent stores; non-TTL stores (`ShardedUnbound`/`ShardedLru`/`ShardedExpiring*`)
  no longer expose `set_ttl`
- mirrors the single-owner `Cached`/`CacheTtl`/`CachedAsync` split
- export both new traits at the crate root and in `cached::prelude`
- adds a validated concurrent `try_set_ttl` that the concurrent surface previously lacked
…etter truthful

Breaking (v3): `refresh_on_hit` and `set_refresh_on_hit` are now required (no default
bodies) on `CacheTtl` and `ConcurrentCacheTtl`. Custom impls of either trait must provide
both.

This fixes a latent bug the defaults hid: the concurrent stores (sharded TTL stores,
`RedisCache`, `AsyncRedisCache`, `RedbCache`) overrode `set_refresh_on_hit` (writing the real
flag) but not the `refresh_on_hit` getter, so through trait dispatch the getter returned a
stale `false` even after `set_refresh_on_hit(true)`. Requiring both forces a truthful getter
by construction; each store now reads its real refresh flag. `TtlSortedCache` declares its
no-refresh stance explicitly (both return `false`).

- flip the tests that pinned the buggy default-false getter to assert it reflects the setter
- changelog + migration note for the required methods and the getter fix
…onal map_error, add companions_vis

bare `#[cached]` defaults to `sync_writes = "by_key"` instead of Disabled.
The cache static becomes `(lock, Vec<Arc<Mutex<...>>>)` in ByKey mode; existing
code that accesses the lock directly via `.read()`/`.write()` now requires `.0.read()`.
`result_fallback = true` without an explicit `sync_writes` silently selects Disabled
(since ByKey and result_fallback are incompatible). Explicit `sync_writes` + result_fallback
still errors. Updated all tests, examples, and UI golden files to reflect the new type.

`convert`, `create`, `force_refresh`, `map_error`, `cache_prefix_block` changed from
`Option<String>` to `Option<syn::Expr>`. darling 0.20.11 handles both unquoted (`convert =
{ ... }`) and legacy quoted (`convert = "{ ... }"`) forms transparently. Removed
`validate_force_refresh_is_string` and all call sites. Added `expr_to_block` helper in
`helpers.rs`. Updated `make_cache_key_type` and `parse_force_refresh_block` accordingly.

`map_error` is now optional on fallible `#[concurrent_cached]` paths (redis/disk/custom).
When absent, the macro generates `?` on the cache operation result, which requires the
function's error type to implement `From<StoreError>` via the standard `?` operator. When
present, the closure form `.map_err(closure)?` is still used.

added `companions_vis: Option<String>` to all three macros. `None` inherits the cached
function's visibility (current behavior). A non-empty string is parsed as `syn::Visibility`
and applied to all companion functions (`_no_cache`, `_prime_cache`, `__cached_inner`).

Tests added in `tests/v3_macros.rs`:
- `test_cached_by_key_default_deduplicates`: four concurrent threads for key=1 produce
  exactly one body execution under the new ByKey default.
- `test_cached_sync_writes_false_double_compute`: `sync_writes = false` gives a bare-lock
  static accessible via `.read()` directly.
- `test_cached_result_fallback_no_explicit_sync_writes_compiles`: result_fallback + bare
  #[cached] compiles and caches correctly.
- `test_cached_unquoted_convert_compiles_and_caches`: unquoted `convert = { ... }` works.
- `test_cached_legacy_quoted_convert_compiles_and_caches`: legacy `convert = "{ ... }"` works.
- `test_cached_unquoted_force_refresh_compiles_and_works`: unquoted `force_refresh = { ... }`.
- `disk_no_map_error_tests::test_disk_concurrent_without_map_error_compiles_and_caches`:
  disk path without `map_error` compiles when the error type implements `From<RedbCacheError>`.
- `companions_vis_tests::test_companions_vis_pub_crate_produces_pub_crate_companions`: pub fn
  with `companions_vis = "pub(crate)"` has `pub(crate)` companion.
- `companions_vis_tests::test_companions_vis_default_inherits_fn_visibility`: default inherits.

UI tests added in `tests/ui/`:
- `cached_result_fallback_sync_writes_by_key`: explicit `sync_writes = "by_key"` + result_fallback errors.
- `cached_convert_malformed_unquoted`: malformed `convert = { let x = }` rejected by darling.
- `concurrent_cached_map_error_non_closure`: `map_error = 5` (non-closure) rejected with clear message.
jaemk added 7 commits June 19, 2026 15:41
…only

Replace the insertion-time `instant: Instant` field in `TimedEntry` with
`expires_at: Option<Instant>` computed at insert time. `None` means the
entry never expires (TTL was disabled at insert). `Some(t)` means the
entry is live while `Instant::now() < t`.

This changes the semantics of `set_ttl`: it now only affects future
inserts. Existing entries keep their original `expires_at` and are not
retroactively re-evaluated. `refresh_on_hit` recomputes `expires_at =
now + current_ttl` on each live hit; when the current TTL is zero it
preserves the existing `expires_at` rather than setting None.

Affected stores: `TtlCache`, `LruTtlCache`, `ShardedTtlCache`,
`ShardedLruTtlCache`. Helper functions `entry_live(expires_at)` and
`compute_expires_at(ttl, now)` are shared per-module. `cache_try_set`
returns `CacheSetError::TimeBounds` on overflow.

`TimedEntry` is now `pub(crate)` (previously `pub`). No re-export existed
in `src/lib.rs`; this is purely a visibility tightening.

Updated `tests/v3_single_owner_zero_ttl.rs`, `tests/v3_sharded_zero_ttl.rs`,
and `tests/v3_traits.rs` to assert the new future-only semantics. Added
`tests/v3_per_entry_expiry.rs` with 10 new tests gated
`#[cfg(feature = "time_stores")]` covering `set_ttl` future-only contract
and `refresh_on_hit` deadline extension on `TtlCache`, `LruTtlCache`, and
`ShardedTtlCache`.

Verified: `cargo build --all-features`, `cargo clippy --all-features
--all-targets -- -D warnings`, `make tests/no-default tests/default
tests/time-stores tests/proc-macro` all pass.
…arded stores

Each of the six sharded in-memory stores (ShardedUnboundCache, ShardedLruCache,
ShardedTtlCache, ShardedLruTtlCache, ShardedExpiringCache, ShardedExpiringLruCache)
now exposes inherent methods that return unwrapped values directly:

  store.get(&k) -> Option<V>
  store.set(k, v) -> Option<V>
  store.remove(&k) -> Option<V>
  store.remove_entry(&k) -> Option<(K, V)>
  store.delete(&k) -> bool
  store.reset()

These shadow the trait's short-alias methods (get/set/remove/remove_entry/delete)
which returned Result<_, Infallible>, eliminating the .expect("infallible") noise
at every call site. The cache_* prefixed trait methods and fully-qualified trait
path (ConcurrentCached::cache_get) remain the Result-returning form for generic code.

Call-site sweep:
- Updated examples/sharded.rs and examples/sharded_expiring.rs (6 .expect removed).
- Updated the deep_clone doctest in sharded/unbound.rs (3 .expect removed).
- Updated doctests in src/lib.rs ConcurrentCached::cache_remove_entry and
  ConcurrentCloneCached (2 .expect removed).
- Updated tests/cached.rs concurrent_cached_trait_short_aliases_work to use
  inherent methods (unwrapped) and trait path (Result) side by side.

Tests added per store (each asserting round-trip set/get, remove returns prior
value, delete bool, and clear/reset empties + resets metrics):
- sharded/unbound.rs: inherent_get_returns_option_not_result,
  inherent_set_returns_previous_value, inherent_remove_returns_prior_value,
  inherent_remove_entry_returns_key_and_value, inherent_delete_returns_bool,
  inherent_reset_clears_and_resets_metrics,
  inherent_and_trait_methods_coexist_via_fully_qualified_path
- sharded/lru.rs: same 7 tests
- sharded/ttl.rs: same 7 tests (gated by time_stores module feature)
- sharded/lru_ttl.rs: same 7 tests (gated by time_stores module feature)
- sharded/expiring.rs: inherent_get/set/remove/remove_entry/delete tests plus
  inherent_get_returns_none_for_expired and trait coexistence
- sharded/expiring_lru.rs: same as expiring.rs
…c_sync uses async-lock

- async feature drops tokio dep; uses dep:async-lock + dep:blocking instead
- redb async ops use blocking::unblock instead of tokio::task::spawn_blocking
- remove RedbCacheError::BackgroundTaskFailed (blocking::unblock propagates panics directly)
- async_sync module re-exports async_lock::{Mutex, RwLock, OnceCell} instead of tokio::sync
- proc macro concurrent_cached: OnceCell::const_new() -> OnceCell::new() (async-lock API)
- remove async_tokio_rt_multi_thread feature; tokio moved to dev-dep with rt-multi-thread
- redis_smol* no longer pulls tokio through cached's async feature
- update Makefile tests/async, tests/disk-store, tests/redis-tokio to drop removed feature
- update all example required-features and doc comments referencing removed feature
- add futures::executor::block_on test to prove async RedbCache is runtime-agnostic
…untime decoupling, sync_writes default, unquoted attrs, companions_vis, per-entry TTL, Cached::Error, sharded inherent methods, TimedEntry pub(crate), LRU write-lock note)
…thout redundant braces

The unquoted-attr work parsed `create`/`cache_prefix_block` through `expr_to_block`
and emitted a block in value position (`Lock::new({ expr })` / `.prefix({ expr })`),
so a single-expression block tripped `unused_braces` under `-D warnings` and a bare
expression panicked the macro. Add `expr_value_tokens`, which unwraps a single-expression
block to its inner expression and emits bare expressions directly, keeping multi-statement
blocks intact.
…API, optional map_error

- `struct_method`/`expires_per_key`: unquoted `convert` blocks; `struct_method` adds `companions_vis`
- `kitchen_sink`: unquoted `create` expressions
- `disk`: unquoted `map_error` closure plus a `From<RedbCacheError>` path with no `map_error`
- `sharded`: correct the mislabeled `.expect` on the `load_record` Result
@jaemk jaemk force-pushed the 260609.next-major-batch branch from 1363073 to b99113c Compare June 20, 2026 00:01
@jaemk jaemk changed the title feat!: next-major batch - macros, stores/traits, redis TLS, constructor + ttl consistency feat: next-major batch - macros, stores/traits, redis TLS, runtime decoupling, constructor + ttl consistency Jun 20, 2026
jaemk added 5 commits June 19, 2026 20:34
Extends the Expires trait with a default-impl expires_at -> Option<Instant>
so types that track a concrete deadline can expose it for observability without
breaking existing impls. is_expired remains the authoritative liveness check.
… unify TtlSortedCacheError into CacheSetError; make set_ttl(0) mean never-expires in TtlSortedCache

rename four default methods on the CachedAsync trait from get_async/set_async/
remove_async/clear_async to async_cache_get/async_cache_set/async_cache_remove/
async_cache_clear. The docs already advertised the new spelling; this makes the
declarations match. No call sites existed in the codebase.

delete TtlSortedCacheError (a duplicate of CacheSetError) and replace every
use with super::CacheSetError. TtlSortedCache now shares the canonical error type with
TtlCache and LruTtlCache. Removes TtlSortedCacheError from the public re-exports.

change TtlSortedCache per-entry expiry from Instant to Option<Instant> where
None = never expires. Stamped and Entry both get Option<Instant> expiry fields. The
Stamped ordering is custom: None sorts GREATEST so never-expiring entries are the last
candidates for size-limit eviction and are not swept by evict(). set_ttl(Duration::ZERO)
and unset_ttl() now both disable expiry for future inserts, consistent with TtlCache /
LruTtlCache. Entries inserted with a non-zero TTL are unaffected. Builder still requires
a non-zero initial TTL.

Tests added: async_cache_get_set_remove_clear_behavioral (lib.rs),
ttl_sorted_cache_set_error_is_clone_eq, ttl_sorted_cache_try_set_returns_cache_set_error_on_overflow,
set_ttl_zero_entries_never_expire, set_ttl_zero_only_affects_future_inserts,
set_ttl_zero_never_expire_entries_evicted_last_under_size_pressure,
unset_ttl_makes_future_inserts_never_expire, evict_does_not_remove_never_expiring_entries
(ttl_sorted.rs). Existing v3_traits.rs tests updated to CacheSetError.
Breaking changes for the major release:
- `disk_store` feature renamed to `redb_store` across Cargo.toml, src/lib.rs,
  src/stores/mod.rs, examples/disk.rs, examples/disk_async.rs, and all test files.
- `redis_ahash` feature deleted entirely (no Rust cfg gates existed; only the
  Cargo.toml declaration and the doc bullet in src/lib.rs are removed).
…ache never-expires, and Expires::expires_at

Independent coverage for the async_cache_* rename, the unified
CacheSetError, TtlSortedCache zero-ttl never-expires semantics, and
Expires::expires_at:

- async_cache_get/set/remove/clear exercised on a real store (TtlSortedCache).
- compile_fail doctests guard against reintroducing TtlSortedCacheError.
- explicit Duration::ZERO insert stores expiry = None; retain_latest and
  max-size eviction keep never-expiring entries last.
- get_or_set_with / try_get_or_set_with under zero ttl persist.
- Expires::expires_at default vs override, and is_expired stays authoritative.
- correct the stale zero-ttl assertions in tests/v3_single_owner_zero_ttl.rs.
…trait rename and feature changes

Update the migration guide, CHANGELOG, README, and AGENTS for the
async_cache_* rename, the CacheSetError unification, the TtlSortedCache
zero-ttl change, and the redb_store/redis_ahash feature changes:

- extend the CachedAsync naming docs to cover the renamed
  get_async/set_async/remove_async/clear_async shorthand methods.
- fix the migration entries and VERIFY list, which previously described
  TtlSortedCache as still using TtlSortedCacheError and zero-ttl as immediate
  expiry (both now reversed).
- document the TtlSortedCacheError removal, the disk_store ->
  redb_store rename, the redis_ahash removal, and Expires::expires_at.
- rename disk_store -> redb_store in the Makefile test targets.
- regenerate README from lib.rs.
@jaemk jaemk changed the title feat: next-major batch - macros, stores/traits, redis TLS, runtime decoupling, constructor + ttl consistency feat: macros, stores/traits, redis TLS split, runtime decoupling, constructor and ttl consistency Jun 20, 2026
@jaemk jaemk force-pushed the 260609.next-major-batch branch from b99113c to a1ed7eb Compare June 20, 2026 09:30
jaemk added 10 commits June 20, 2026 14:55
Brings `BuildError` to parity with the sibling error enums
`SetMaxSizeError`, `SetTtlError`, and `CacheSetError`, which already
derive these. Its variants carry only `&'static str`, so the derive is
sound. Callers can now compare and clone `build()` errors in tests.

The `RedisCacheBuildError` and `RedbCacheBuildError` wrappers keep
`Debug` only; their other variants wrap `redis::RedisError`,
`r2d2::Error`, `redb::Error`, and `io::Error`, none of which implement
these traits.
… redact connection string

- serialize redis values with `rmp-serde` (MessagePack) instead of `serde_json`; `redis_store` now pulls `rmp-serde`. Existing redis entries are recomputed on miss.
- set redis expiry with `PSETEX`/`PEXPIRE` so sub-second ttl is honored to the millisecond instead of rounded up to a whole second.
- `RedisCache`/`AsyncRedisCache` `connection_string()` returns the redacted form; add `connection_string_unredacted()` for the raw url.
- rename `RedbCacheBuildError::Connection` to `Storage`; convert the redb error enums to struct variants matching redis. serialize/deserialize variants on both backends carry `rmp_serde` errors, and `CacheDeserialization` carries the raw `cached_value` bytes.
…nce]`

- emit a guiding error for `disk`, `redis`, and `map_error` on `#[cached]`/`#[once]` pointing to `#[concurrent_cached]`, instead of darling's generic unknown-field message.
- generated `#[cached]` code calls the core `cache_set` method directly.
…ses to extension traits, expose sharded metrics via trait

- add a hasher type parameter (`S = DefaultHashBuilder`) and a `.hasher()` builder method to `UnboundCache`, `LruCache`, `TtlCache`, `LruTtlCache`, `TtlSortedCache`, `ExpiringCache`, and `ExpiringLruCache`; export `DefaultHashBuilder`.
- move the short method aliases (`get`/`set`/`remove`/`len`/...) off `Cached`/`ConcurrentCached` onto blanket `CachedExt`/`ConcurrentCachedExt` traits, re-exported from the prelude; the core traits keep only the `cache_`-prefixed methods.
- add `cache_hits`/`cache_misses`/`cache_capacity`/`cache_evictions` and a default `metrics()` to `ConcurrentCacheBase` so sharded metrics are reachable through a trait bound.
- document the `len`/`iter`/`evict` contract on lazy-eviction stores.
…t changes; add specs

- reconcile the migration guide and changelog with the redis msgpack/millisecond-ttl/redaction changes, the struct-variant store errors, the `CachedExt`/`ConcurrentCachedExt` split, the non-sharded custom hasher, and the concurrent metric accessors.
- regenerate the readme.
- add a `specs/` directory documenting current and proposed 3.0 work, each item marked implemented, not implemented, or needs research.
…honor millisecond redis ttl

- read legacy redis entries transparently: deserialize MessagePack first, then fall back to the pre-3.0 serde_json format identified by its `version` key. `serde_json` is re-added as a read-only fallback enabled by `redis_store`.
- `RedisCache`/`AsyncRedisCache` `connection_string()` returns a `ConnectionString` whose `Display` and `Debug` redact; call `.reveal()` for the raw url. `connection_string_unredacted()` is removed. `ConnectionString` is re-exported from the crate root and `cached::stores`.
- clamp a non-zero sub-millisecond ttl to 1ms so `PSETEX`/`PEXPIRE` never receive 0, and correct the docs that claimed redis rounds ttl up to whole seconds.
- point the readme upgrade banner at the migration guide instead of listing breaking changes inline.
- fix doc references to the extension traits: short aliases live on `CachedExt`/`ConcurrentCachedExt`, concurrent `len`/`is_empty` on `ConcurrentCacheBase`, and repair the `CachedExt::get_or_set_with` link.
- drop the migration entry that pointed at the removed `TtlSortedCacheError`; the unified type is `CacheSetError`.
- document the redis backward-read fallback (pre-3.0 entries are read transparently, not recomputed) and the `ConnectionString`/`.reveal()` api.
- note that `cache_reset` no longer restores preallocated capacity.
- correct comments: the single generic `cache_reset` applies to all hashers, the sharded ttl expiry predicate, the `compute_expires_at` overflow-to-never-expires case, and the single-key refresh doctest.
…rrent metrics

- add server-gated redis tests for reading pre-3.0 json entries, round-tripping a struct value through msgpack, and millisecond ttl precision via `PTTL`.
- add tests threading a custom `BuildHasher` through `UnboundCache`/`LruCache`.
- add tests asserting aggregated `cache_evictions`/`cache_hits`/`cache_misses`/`cache_capacity` through `ConcurrentCacheBase`.
- update `connection_string()` call sites to `.reveal()`, fix the stale peek-status docstring, de-flake the per-entry-expiry timing tests, and relax the hit-path clone-count assertions.
@jaemk jaemk merged commit 3576bf0 into master Jun 21, 2026
1 check passed
@jaemk jaemk mentioned this pull request Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant