Repository navigation
TierCache 1.4.0
[1.4.0] - 2026-09-21
Added
- Degradation stale window (
degradationStaleTtl/tiercache.degradation-stale-ttl, per cache, default off): during an L2 outage, physically retained but logically expired L1 entries can be served stale without hitting the source — the failure mode that overloaded origins in load testing. Freshness is tracked by engine-stamped per-entry deadlines (never the L2 write timestamp), stale serving is breaker-gated (OPEN, or HALF_OPEN without a probe permit — probes always flow so the breaker can close), fresh accesses slide the horizon without shortening the store-time retention floor, stale accesses never extend it, and every stale serve is counted (tiercache.requests{result="stale_degraded"}).CacheSettingsgained the component with a compatible old-arity constructor;CacheOverride, both starter property bindings, and the Kotlin DSL are covered. Seedocs/configuration.mdfor the memory trade-off and the residual contract for full-outage writes (not healed by replay; bounded by the stale L2 copy's remaining TTL, not one L1 TTL).
Fixed
- The Spring Boot starter and the Micronaut integration now build the invalidation engine with the application's configured
CacheMetricsListenerinstead of silently selecting the no-op listener, so invalidation traffic (sent, received, replayed, dropped) is observable like every other metric family; without a listener the wiring falls back to NOOP explicitly. - An in-flight load can no longer overwrite a concurrent write or invalidation: the store version is minted at claim time (before the loader runs), so a write or versioned eviction landing during the load wins the conditional store — previously a slow loader's stale snapshot won last-write-wins and every node served stale data until TTL. A load losing to an eviction performs at most one bounded reload instead of returning a false "not found" (at most two loader executions per request). Every L1 state change now goes through one atomic per-key commit over a unified holder (entry + version + invalidation barrier): invalidations are barriered even for absent keys, equal versions are admitted (an UPDATE lifts the barrier and installs its payload in one step), local puts/evicts participate in the same commit, and a per-cache generation guard refuses stale L1 writes when protective state is evicted mid-flight — the L1 fill alone is refused, never converted into an artificial miss.
- A rebuild lock can no longer be orphaned by an ambiguous acquire: when the acquire command's outcome is unknown (client-side timeout while the server may have executed it), the provider schedules a best-effort token-checked compensating release, retried until it deletes or the compensation window (
max(2 × lease, 30 s)) expires — previously the orphan sat for the full lease and blocked rebuilds (a bench read waited 10.57 s). The token check makes the compensation safe against every outcome; retries are bounded per provider (small worker pool, pending cap with drop-on-overflow), and the documented residual — a SET executing only after the window — self-expires within one lease. Lock providers now have defined ownership: library-created providers are closed by the creating factory or as managed beans (destroy callback), wrappers forwardclose(), and provider connections open lazily so wiring never fails on an unreachable server at startup.
Verify
Artifacts are signed (Sigstore keyless) and attested:
gh attestation verify <jar> --repo cramen/tier_cache
SBOM attached (tiercache-sbom.json). Maven coordinates: io.github.cramen:<module>:1.4.0.
https://github.com/cramen/tier_cache/actions/runs/35620622743