Skip to content

Releases: cramen/tier_cache

TierCache 2.2.0

Choose a tag to compare

@cramen cramen released this 20 Oct 09:53

TierCache 2.2.0 improves rebuild-lock resilience and circuit-breaker failure accounting. No configuration or Redis key/value-format migration is required from 2.1.0.

Fixes

  • Retry transient watchdog renewal failures with the original token; stop renewal on confirmed lease loss without replacing the loader outcome.
  • Count prewrapped transport failures once per circuit breaker while keeping open-breaker rejections neutral.
  • Warn when custom L2 wiring enables invalidation without an invalidation handler factory.
  • Isolate test expiry configuration and add real Redis lease-loss/timeout and shutdown-race regressions.

Validation

Full build, Redis/Valkey and Spring compatibility, Sentinel scenarios, Java 21/25 core and virtual-thread checks, native smoke and reproducible core JAR checks passed during prerelease validation. PIT scores were 84.49% / 76.57%, using incremental history and including timeout detections.

A six-hour churn run completed 3.28 billion operations without worker failures. RSS growth strongly decelerated. The strict memory-growth gates remain FAIL and are explicitly WAIVED by the maintainer for this release; a small post-GC heap trend is confounded by the measurement harness retaining history. See maintenance/release-2.2.0-validation.md. This is not a claim that every workload is leak-free.

See CHANGELOG.md and UPGRADING.md for details. Signed dependency evidence is attached separately; application BOM overrides require scanning the application's own dependency graph.

TierCache 2.1.0

Choose a tag to compare

@cramen cramen released this 26 Sep 23:20

Changed

  • Reduced shared-monitor contention for read-only L1 freshness checks, atomic access-expiry refresh and CLOSED circuit-breaker admission while preserving entry identity, retention floors and breaker epoch accounting.
  • Async singleflight followers no longer occupy API workers while waiting for the leader. Cancellation, completion and shutdown release follower registrations.
  • Pub/Sub invalidations now use bounded, ordered per-cache queues over a shared worker pool. Message, encoded-byte and recovery attachment budgets prevent unbounded retention; overflow triggers explicit gap recovery. Spring and Micronaut expose the dispatch limits and Micrometer reports receiver pressure.
  • Journal replay applies events in bounded slices, allowing other recovery work to progress between slices.
  • User documentation is separated from maintainer procedures; the documentation index links configuration, recovery, observability and integration guides.

Fixed

  • Successfully committed remote writes preserve local coherence even when invalidation barriers race with local publication. Rejected writes cannot overwrite the winning value in L1.
  • Cache registration waits for invalidation readiness, and dispatch overflow remains visibly pending until verified replay or conservative clear restores a safe baseline.

Validation

  • Virtual-thread checks separate cold-start diagnostics from the strict steady-state gate on Java 21 and 25.
  • Lock-provider shutdown tests observe the provider's owned executor termination instead of unrelated JVM thread counts.
  • Six-instance tests exercised Sentinel failover, Redis outages, instance isolation and restart, Pub/Sub reconnect and source failures. Follow-up checks converged on all instances; transient stale reads during Redis failures remain possible under the documented eventual-consistency contract.

See upgrade notes and the documentation index.

TierCache 2.0.0

Choose a tag to compare

@cramen cramen released this 23 Sep 22:02
d918cb7

TierCache 2.0.0 isolates Redis cache namespaces and hardens invalidation recovery, concurrent writes, resource ownership and Spring asynchronous cache retrieval.

Breaking change: coordinated migration from 1.x

Redis keyspace v2 separates data, tags, journals, channels, consumer groups and rebuild locks. Complete cache names are encoded so clearing one cache cannot delete another cache's data or control state.

Do not perform an ordinary rolling upgrade from 1.x. Drain and stop old readers/writers, start 2.0.0 with empty v2 namespaces, and allow for cold-cache load on the source. Legacy data is neither read nor migrated automatically. Rollback also requires a drained cutover and clean isolated cache storage.

Read the migration and rollback guide and all upgrade notes before deployment. Custom tagged-write transports and custom L1 providers may require the SPI capabilities described there. Enabled invalidation journals now require capacity of at least 65; the default remains 10000.

Correctness and recovery

  • Foreground reads joining skipped refreshes no longer receive a false null.
  • Losing tagged writes no longer warm L1, publish a losing value or replace tag indexes. Data and reverse indexes share an absolute expiration instant.
  • Journal recovery performs bounded asynchronous I/O outside state monitors. Streams drains pending batches and requires a safe reset before acknowledging unreadable history.
  • L1 freshness follows the retained value's lifetime. Atomic access refresh cannot resurrect an evicted entry.
  • Owned lock/cache resources close safely across initialization and shutdown races.
  • Managed Spring caches implement both asynchronous retrieval overloads; publication outcomes, JMX ownership and locale-independent metric labels are corrected.

Compatibility and dependencies

Java 17 remains the baseline. CI covers Java 17/21/25, Redis 6.2/7.4/8.x, Valkey, Spring Boot 3.5/4.1 consumers and Redis Sentinel. Redis Cluster remains unsupported by the built-in transport.

Micrometer is updated to 1.15.12 and the transport uses Netty BOM 4.2.17.Final. Signed release evidence includes resolved dependency inventories, SBOMs, vulnerability scan results and checksums for publication files. Scan the resolved dependency graph of your own application when overriding BOMs.

Operational limits

  • The cache is eventually consistent. Sentinel's asynchronous replication can lose invalidations during failover and temporarily restore an older cached value; Pub/Sub and Streams cannot guarantee read-your-writes through such failures.
  • In the tested Spring configuration, cold startup requires a reachable Redis/Sentinel journal connection.
  • During Redis outages, misses can overload the source. Optional bounded stale retention improves availability at the cost of freshness; size TTLs and source concurrency to application requirements.

A six-instance Docker campaign on the same runtime code completed a 60-minute mixed-load run at 6000 RPS: 21.6 million requests, p99 187 ms and 47 errors (0.000218%). All 720 writes were acknowledged; no stale answers after the one-second allowance were found among 9.64 million freshness-checked responses. Failure scenarios covered Sentinel promotion/primary loss, partitions, journal truncation, Redis/source outages and API restart. These are measurements of one stand, not universal throughput or consistency guarantees. RSS grew by 22–30 MiB per JVM over that hour; the run does not establish a full memory plateau or prove the absence of leaks.

Maven Central

implementation("io.github.cramen:tiercache-spring-boot-starter:2.0.0")

All nine library/TCK modules use version 2.0.0. See the full changelog for details.

Release verification

  • CI for the release commit: passed.
  • Nightly gates on the identical candidate tree: passed, including PIT (core 83%, invalidation 78%; threshold 75%), native smoke, Sentinel, reproducible build and dependency checks.
  • Final signed evidence: passed; Sigstore signature, GitHub attestation and all manifest hashes independently verified.
  • All 49 publication files across nine modules were downloaded from public Maven Central and matched the final evidence SHA-256 values. The attached Central bundle contains these exact files with PGP signatures and checksums.

TierCache 1.4.0

Choose a tag to compare

@cramen cramen released this 21 Sep 15:45

[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"}). CacheSettings gained the component with a compatible old-arity constructor; CacheOverride, both starter property bindings, and the Kotlin DSL are covered. See docs/configuration.md for 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 CacheMetricsListener instead 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 forward close(), 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

TierCache 1.3.0

Choose a tag to compare

@cramen cramen released this 20 Sep 17:55

[1.3.0] - 2026-09-20

Fixed

  • Closing the factory now fails outstanding async operations instead of abandoning them: every stage handed out through AsyncTierCache views is tracked and completed with CancellationException on TierCacheFactory.close(), so callers waiting on queued (never-started) operations unblock with a visible failure instead of hanging forever.
  • The async close protocol is now race-free: submission (state check + registry + executor hand-off) and the close drain share a per-view lifecycle lock, and view creation (asyncCache) and the factory's CLOSED transition share a factory-level lock — a close landing anywhere in the submit path either rejects the submission or drains the registered stage, and no view can be published after the drain snapshot. Two visible consequences: a submission after close now fails its stage with CancellationException (previously RejectedExecutionException), and asyncCache(name) on a closed factory throws IllegalStateException (previously returned a view nothing would ever drain).
  • Tag indexes are now bounded under every workload: the per-key reverse index keeps the data entry's exact TTL, each per-tag member set's TTL is extend-only and assigned atomically with the member add (one Lua call) — a shorter-lived entry can no longer shrink the shared index (mixed TTLs), and sets of never-touched-again tags expire within the longest member TTL. Dead members are reclaimed atomically (janitor sampling on writes plus read-time pruning, both check-and-remove in the same Lua call): tag lookups never return phantom entries, a concurrent rewrite can never lose its membership, and steady-state set size stays within the stated statistical bound under uninterrupted writes. See UPGRADING.md for the transition notes (pre-existing shrunken set TTLs do not self-repair; mixed-rollout caveats).
  • Replay cursor correctness: the replay position now marks the end of the contiguous applied prefix of the journal — live deliveries are tracked in a bounded (128-version) applied window, and the cursor advances on a bounded cadence over exactly the rows already applied, never to the stream end and never past an unconsumed row (previously, a writer that journaled a row but delayed its publish could have that row skipped after a reconnect; a row written mid-replay could likewise be skipped). Every cursor read is one atomic checked read (integrity proof and range from the same response): if the cursor's own row was trimmed, the receiver takes the flush path with the flush signal instead of mistaking surviving rows for a contiguous prefix; a window overflow behind a delayed row falls back to journal-driven catch-up. Trim detection for a beginning cursor (0-0) is now exact — counted atomically on every journal-appending path — including the boundary where the stream length equals the capacity exactly (previously misread as "no loss"). A trim-counter read failure takes the flush path with a warning, never "no loss". Operational note: the journal capacity must exceed the cursor cadence (64 events); below that, integrity is unconfirmable on every tick and the flush path fires by design (see docs/sizing-and-ttl.md).
  • The L1 flush path can no longer swallow an invalidation: the journal baseline is now captured BEFORE the local cache is cleared, so a write journaled between the clear and the baseline establishment stays ahead of the cursor and is replayed instead of being silently accounted (previously the stale re-warmed entry survived). The order is baseline → clear → settle state → notify observers, so a listener/metrics failure cannot cancel the mandatory clear; if the baseline read itself fails, the previous confirmed cursor is kept — the cursor never jumps past unread rows.
  • Applied-version tracking is now hard-bounded (at most 768 versions per cache: 512 window hard cap + 256 confirmed, plus one bounded read batch): versions the cursor already passed are recorded in a bounded confirmed-set and late duplicates are applied to L1 without being re-tracked (previously 1,000 re-delivered events were retained forever at a nominal cap of 128); every catch-up pass ends in an explicit CAUGHT_UP / MORE_WORK / FAILED outcome — only a pass that reached the journal end clears the window; and on read failures the service enters a resync-required state (tracking memory freed, retries throttled to one per second, cursor never advanced without confirmed rows). Recovery is deliberately lazy: without new deliveries or a reconnect it stays incomplete and L1 may serve stale data until the next trigger, which then applies the missed rows.
  • evictAll is now journaled and covered by reconnect replay: the RemoteCache SPI gained a versioned clear(Version) (defaulting to clear(), so custom transports keep their current behavior), the engine routes evictAll through it, and the Redis transport appends the EVICT_ALL journal row atomically with the namespace clear. Previously a receiver that missed the live EVICT_ALL notification kept serving stale L1 entries until TTL.
  • UPDATE-mode journal replay now deserializes the payload with the value serializer before applying it to L1, matching the live Pub/Sub path; previously a replayed UPDATE stored the raw serialized bytes as the value (typed reads could hit ClassCastException).
  • Direct journal appends now serialize the UPDATE payload with the value serializer (previously the key serializer), staying symmetric with replay deserialization when key and value serializers differ. The standard write path (payload framed by the L2 scripts) is byte-identical before and after. Rows misencoded before this fix are not repaired retroactively — they age out within the journal window.

Changed

  • Versions moved from millisecond to microsecond resolution (max(epochMicros, previous + 1)), shrinking the cross-instance simultaneity window a thousandfold: two instances writing within one millisecond now order by real time. Same-microsecond writes remain ordered by the instance-ID tiebreak — the documented last-write-wins trade-off; a strict "later always wins" guarantee would require per-write coordination and stays out of scope by design. Wire format and Redis scripts are unchanged; on platforms with millisecond-granular clocks the scheme degrades to the 1.2.0 behavior.

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.3.0.

https://github.com/cramen/tier_cache/actions/runs/35527255199

TierCache 1.2.0

Choose a tag to compare

@cramen cramen released this 19 Sep 22:21

[1.2.0] - 2026-09-20

Added

  • tiercache.async-executor-threads / TierCacheFactory.Builder.asyncExecutorThreads(int) (also on the Kotlin DSL, KTierCacheFactory.Builder, and ReactorCacheFactory.Builder): thread cap for the bounded async executor, default max(4, availableProcessors).
  • docs/sizing-and-ttl.md: journal-capacity sizing guidance.

Changed

  • The async executor is now bounded with a bounded queue (10,000): previously AsyncTierCache operations ran on an unbounded cached thread pool shared with background revalidation. Under saturation, submissions fail their CompletionStage with RejectedExecutionException instead of growing threads without bound. Background SWR/XFetch revalidation runs on its own small bounded pool. See UPGRADING.md for the saturation behavior change.
  • Docs accuracy: the recovery description no longer claims L1 is "never" flushed (replay within the journal window; flush on window overflow, signalled via log/metric/listener). README now states that @Cacheable load coalescing requires sync = true and that null caching is opt-in.

Fixed

  • Cross-instance write ordering: versions were per-instance counters starting at 1, so a freshly started instance's writes (and evicts) could be rejected as stale against a long-running instance's earlier writes on the same key. Versions are now time-ordered (wall-clock hybrid sequence); the wire format and Redis scripts are unchanged. See UPGRADING.md for the rolling-upgrade note and the clock-sync expectation.
  • Reconnect replay for Spring/Micronaut-wired caches: the invalidation journal was written under the namespaced cache name while the recovery path replayed the logical name, so missed invalidations were never replayed and stale L1 entries survived until TTL. The journal now follows the logical cache name in all wirings.

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.2.0.

TierCache 1.1.0

Choose a tag to compare

@cramen cramen released this 18 Sep 10:31

[1.1.0] - 2026-09-18

Added

  • tiercache-micronaut: Micronaut integration — a CacheManager replacing Micronaut's DefaultCacheManager, SyncCache/AsyncCache adapters over the core engine, @ConfigurationProperties("tiercache") with the same keys and defaults as the Spring Boot starter, default wiring parity (shared Redis client, per-cache L2 namespacing, Pub/Sub invalidation profile, conditional Micrometer metrics). Micronaut @Cacheable/@CachePut/@CacheInvalidate code works unchanged.
  • docs/configuration.md: "Micronaut binding notes" section.
  • docs/migration-from-redisson.md: migration guide from Redisson RLocalCachedMap.
  • docs/migration-from-jetcache.md: migration guide from JetCache.
  • docs/sizing-and-ttl.md: L1 sizing, TTL ratios, jitter, per-cache overrides.
  • README section "When Tiercache is not the right tool".

Changed

  • CI workflows moved to current action majors: actions/checkout v7, actions/upload-artifact v7, actions/setup-java v6, gradle/actions/setup-gradle v6.3.0.

Removed

  • The soak gate is no longer part of CI (hosted runners kill long jobs). Run it locally: ./gradlew :tiercache-tck:soakTest.

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.1.0.

TierCache 1.0.0

Choose a tag to compare

@cramen cramen released this 14 Sep 21:18

[1.0.0] - 2026-09-14

GA release. No runtime behavior changes versus 0.4.0.

Added

  • Complete Javadoc with @since across all published modules, plus package-level documentation.
  • UPGRADING.md with per-release upgrade notes.
  • "Compatibility and versioning" policy in the README: semantic versioning commitment, the definition of the supported public API, and the deprecation/removal rules.

Changed

  • CI workflows moved to actions/setup-java v5.

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.0.0.

TierCache 0.4.0

Choose a tag to compare

@cramen cramen released this 13 Sep 18:51

[0.4.0] - 2026-09-13

Changed

  • The absolute two-level throughput budget (1M ops/s per instance) is dropped from the requirements: absolute throughput depends on the hardware and network environment the code runs on, so it is not a fair library contract. The throughput benchmarks stay as reproducible trend/regression measurements; budgets, if any, are defined per reference environment.

Added

  • tiercache-reactor: Reactor bridge over the async view — ReactorTierCache Mono facade (all operations; sync and Mono loader forms of getOrCompute, coalescing stays in the engine's singleflight) and ReactorCacheFactory exposing inbound invalidation events as a cold Flux with bounded per-subscriber buffering and a drop signal.

Verify

Artifacts are signed (Sigstore keyless) and attested:

gh attestation verify <jar> --repo cramen/tier_cache

SBOM attached under sbom/. Maven coordinates: io.github.cramen:<module>:0.4.0.

TierCache 0.3.0

Choose a tag to compare

@cramen cramen released this 13 Sep 17:17

[0.3.0] - 2026-09-13

Added

  • AsyncTierCache: a non-blocking CompletionStage view of the cache (all operations, plus sync and async loader forms for getOrCompute), obtained via TierCacheFactory.asyncCache(name); coalescing stays in the engine's singleflight.
  • The Spring Boot starter now registers the factory-level gauges (tiercache.degraded, tiercache.breaker.state, tiercache.journal.size, tiercache.last.load.age) eagerly — they were previously never registered in the Spring path.

Changed

  • KTierCache suspend operations now await the core's async view instead of offloading blocking calls to Dispatchers.IO; loader failures reach coalesced callers directly (unwrapped) instead of CompletionException-wrapped. Public Kotlin API is unchanged.
  • The shaded core's Gradle .module metadata now matches its POM (shaded jar as the default variant, no Caffeine leak; previously Gradle-metadata consumers got the unshaded jar).

Verify

Artifacts are signed (Sigstore keyless) and attested:

gh attestation verify <jar> --repo cramen/tier_cache

SBOM attached under sbom/. Maven coordinates: io.github.cramen:<module>:0.3.0.