Skip to content
Merged
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [ ] `./mvnw checkstyle:check -B` passes when Java source changed.
- [ ] New behavior has tests; bug fixes have a regression test.
- [ ] Redis integration tests use `AbstractRedisIntegrationTest`; Cluster tests use `AbstractRedisClusterIntegrationTest` (Testcontainers — Docker must be running).
- [ ] `bash scripts/ci/check-workflows.sh` passes when workflows or CI scripts changed.
- [ ] `bash scripts/ci/check-test-names.sh` passes; integration classes do not use `*IT.java`.
- [ ] `bash scripts/ci/check-docs-contracts.sh` passes when docs, source references, or public contracts changed.
- [ ] Documentation changes update the canonical owner in [`docs/README.md`](../docs/README.md); no task/date/session document was added as current-state policy.
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ documentation and source.
| Distributed lock | optional Redisson 3.50.0 | `pom.xml` |
| Local support | Caffeine 3.1.8 | `pom.xml` |
| Build | Maven 3.x / `./mvnw` | root POM and wrapper |
| Tests | JUnit 5, Testcontainers, AssertJ, Awaitility | `pom.xml` and `src/test/` |
| Tests | JUnit Jupiter (Boot-managed), Testcontainers, AssertJ, Awaitility | `pom.xml` and `src/test/` |

Do not copy dependency versions into a second contract. The current source
tree and module ownership are in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
Expand All @@ -105,6 +105,7 @@ runtime module.
- Separate style gate: `./mvnw checkstyle:check -B`.
- Packaged public-consumer gate: `bash scripts/ci/check-external-consumer.sh`.
- Documentation/source guard: `bash scripts/ci/check-docs-contracts.sh`.
- Workflow/script guard: `bash scripts/ci/check-workflows.sh`.

Full command semantics, test layers, CI, and contribution checks live in
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) and
Expand Down
19 changes: 16 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,20 @@ contract.
> Java 21 artifact exists yet, so a blocking compatibility gate waits for a
> same-line published artifact with matching tag provenance.

The source-cleanup section labelled `0.0.2` below is repository history, not a
release note for the historical Central artifact with the same version. Current
source changes remain unreleased; publication is tracked in
[`COMPATIBILITY.md`](COMPATIBILITY.md).

## [Unreleased] — current development

### Documentation

- Reconcile adoption, configuration fallback, explicit serializer allowlists,
synchronization timeout, batch migration/rollback, test tooling and public
surface descriptions against the current source and CI. Preserve historical
benchmark scores while removing unsupported performance-SLO labels.

### CI/CD

- Validate the packaged-consumer JDK before building, resolve PATH Java shims,
Expand Down Expand Up @@ -392,8 +404,9 @@ Current milestones:
engine-side rejection of a malformed `HandlerResult` is retained because
`STABILITY.md` §4 documents it.
- **TTL precedence in one module (c8)** — `TtlPolicy` owns the ordered
resolution and both defaults; every path keeps its previous effective TTL and
no default changed. See [`COMPATIBILITY.md`](./COMPATIBILITY.md) and
resolution. The ownership extraction preserved behavior; the separate
annotation-default change above removed the implicit 60-second fallback. See
[`COMPATIBILITY.md`](./COMPATIBILITY.md) and
[`docs/REFERENCE.md`](docs/REFERENCE.md).
- **Synchronization lifecycle owned by its state (c9)** — the single-flight
lifecycle (enter → complete → exit → cleanup, exactly once) now lives with the
Expand All @@ -419,7 +432,7 @@ Current milestones:
bumped to `'21'` to match `pom.xml <java.version>21</java.version>`.
- Whitelist rejection message now mentions the remediation property key.

## [0.0.2] — current
## [0.0.2] — historical source cleanup

### Removed — over-engineering cleanup (~2,989 lines)

Expand Down
60 changes: 24 additions & 36 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ ResiCache ships on a **single build line**:
Redisson 3.50**.

CI is configured to run `clean verify -B` on Java 21. Local verification also
requires JDK 21 and Docker for Testcontainers; this session is not a release
baseline.
requires JDK 21 and Docker for Testcontainers. A configured CI matrix is not
a claim that every patch of a component has been tested.

> **Historical context**: Previously the repository carried a `boot3` line
> (Boot 3.4.13 / Java 17 / Redisson 3.27). The migration to Boot 4 merged
> into the main line; the dual-branch strategy is **abandoned**. Boot 3.x
> compatibility is not maintained. See `CHANGELOG.md` for migration context.
>
> Verified 2026-09-05: every Maven Central version (0.0.1–0.0.5, 0.0.7,
> 3.2.4) is the earlier Boot 3.2.4 / Java 17 line; no Boot 4 artifact is
> published yet. A bounded public adopter search on the same date found no
> external consumers of any line (private adopters remain unprovable).
> Publication checked 2026-10-07 against [Central metadata](https://repo.maven.apache.org/maven2/io/github/davidhlp/ResiCache/maven-metadata.xml)
> and each published POM: versions 0.0.1–0.0.5, 0.0.7 and 3.2.4 all belong
> to the earlier Boot 3.2.4 / Java 17 / Redisson 3.17.6 line. There is no
> matching Boot 4 artifact. The checkout's `0.0.2` build version must not be
> confused with the historical [Central 0.0.2 POM](https://repo.maven.apache.org/maven2/io/github/davidhlp/ResiCache/0.0.2/ResiCache-0.0.2.pom).
> A bounded public adopter search on 2026-09-05 found no external consumers;
> that historical search does not establish current adoption or private usage.

## Supported versions

Expand All @@ -26,7 +29,7 @@ baseline.
| Component | Version | Tested |
|-----------|---------|--------|
| Java | 21 | CI |
| Spring Boot | 4.0.0 | 4.0.x (CI) |
| Spring Boot | 4.0.0 | 4.0.0 (CI); no multi-patch matrix |
| Spring Framework | 7.x | (via Boot) |
| Spring Cache | 7.x | (via Boot) |
| Spring Data Redis | 4.0.x | (via Boot) |
Expand All @@ -52,29 +55,19 @@ baseline.

| Dependency | Required? | Notes |
|---|---|---|
| **Redisson** | Optional | Needed for distributed-lock (`sync=true`). Without it, a
sync operation fails fast unless `resi-cache.sync-lock.local-only=true` is
explicitly configured. |
| **Micrometer Core** | Required | Runtime handler and metrics seams reference its
types even when metrics publishing is disabled. Its version is managed by the
Spring Boot dependency management in `pom.xml`; it does not create a registry. |
| **Actuator / metrics registry provider** | Optional | Cache metrics require
`resi-cache.metrics.enabled=true` (default OFF) and a `MeterRegistry`;
otherwise the resolved metrics seam is a no-op adapter.
`RedisCacheHealthIndicator` requires Actuator and the `HealthIndicator`
class; it is not gated on the metrics property, so an application with
Actuator and Redis assembles it and each `/actuator/health` probe issues a
synchronous Redis `connection.ping()` round trip (see the probe-cost note in
[`docs/OPERATIONS.md`](docs/OPERATIONS.md)). |
| **Caffeine** | Bundled | Used internally for the local hash cache and
bloom-filter bitset; not exposed as a multi-level cache. |
| **Redisson** | Optional | Add the core dependency explicitly for the built-in distributed lock, or supply a `LockManager`. Without a backend, `sync=true` fails closed unless `resi-cache.sync-lock.local-only=true`. |
| **Micrometer Core** | Required | Runtime SPI and handler types reference it even when publishing is disabled. The Boot-managed dependency does not create an application registry. |
| **Actuator / registry provider** | Optional | Publishing requires `resi-cache.metrics.enabled=true` and a `MeterRegistry`. The Actuator health indicator is independent of that switch and performs a synchronous Redis PING per invocation; see [`docs/OPERATIONS.md`](docs/OPERATIONS.md#observability-and-diagnosis). |
| **Caffeine** | Bundled | Internal hash-position cache and local Bloom support; no multi-level cache API. |

## Serialization compatibility

⚠️ ResiCache serializes values in an internal `{version, payload}` envelope via
`SecureJackson` for safe deserialization. This is **not** wire-compatible with
Spring's `GenericJackson2JsonRedisSerializer` or `JdkSerializer`. Existing caches must be **migrated** when adopting ResiCache, otherwise the
entire cache misses on cutover. Adopt a bounded **shadow-read → dual-write →
Spring's `GenericJackson2JsonRedisSerializer` or `JdkSerializer`. Existing
caches must be **migrated** when adopting ResiCache; otherwise values may miss
or fail decoding, depending on the configured failure policy. Adopt a bounded
**shadow-read → dual-write →
cutover** migration workflow: run ResiCache alongside the existing cache,
shadow-read through the new serializer while dual-writing, then cut over once
hit rates stabilize. This preserves TTL, supports resumable rollback, and does
Expand All @@ -83,8 +76,8 @@ not require a cache flush.
## Known limitations

- **Reactive types**: `Mono<T>` / `Flux<T>` return types are **not supported**.
ResiCache's interceptor is blocking; such methods log an explicit "caching
will not take effect" warning and bypass ResiCache.
The blocking interceptor logs a warning and bypasses its cache advice for
these declared return types. Other host advisors retain their own behavior.
- **Async methods**: `@Async` cached methods are not supported for sync-lock and
Bloom-filter enhancements.

Expand Down Expand Up @@ -114,18 +107,13 @@ not require a cache flush.
`RedisProCacheWriter` for GET/GET-hit/GET-miss/PUT/DELETE, including exact
`clear` deletion counts and PUT_IF_ABSENT insertion. `withStatisticsCollector`
fully rebinds statistics; lock-wait duration remains unreported (zero).
- **Class-level cache annotations**: Spring operation resolution sees class-level ResiCache annotations, but the annotation chain does not apply their policy fields to methods without method-level annotations; this behavior is unchanged from `main`.
- **Class-level cache annotations**: Spring operation resolution sees class-level ResiCache annotations, but the annotation chain does not apply their policy fields to methods without method-level annotations; use method-level declarations when a protection policy is required.
- **`value` and `cacheNames` resolution**: the three annotations are not
`@AliasFor`-linked, so one declaration may set both attributes. There is one
resolution for both faces (`RedisCacheAttributesProjector.resolveCacheNames`)
and **`value` wins**; `cacheNames` is the fallback and only applies when
`value` is empty. A declaration that sets both targets the `value` cache.
This is the operation face's `main` behaviour, so the cache in use is
unchanged. The policy face changes for that same both-set declaration: on
`main` the operation targeted `value` while the policy snapshot was
registered under `cacheNames`, so the declared policy silently did not apply
to the cache that was used. Both faces now resolve to `value`, and the policy
applies to the cache in use.
Both the Spring operation and policy snapshot resolve to that same cache.
- **TTL default precedence**: one module owns the resolution (`TtlPolicy`; the
ordered rule is specified in [`docs/REFERENCE.md`](docs/REFERENCE.md)). A
method-level `ttl` greater than zero is the only declaration that overrides
Expand All @@ -144,8 +132,8 @@ not require a cache flush.
seconds, so an annotated method without an explicit `ttl` expired its
entries after `60s` even when `resi-cache.default-ttl` was configured. The
annotation-side implicit `60` is removed: such a method now uses the
configured default (`30m` unless overridden), and `60` is no longer
reachable from the resolution path. Methods that set `ttl` explicitly are
configured default (`30m` unless overridden), with no implicit `60s` fallback
in the resolution path. Methods that set `ttl` explicitly are
unaffected. Deployments relying on the old `60s` expiry for methods that
omit `ttl` must either set `ttl` explicitly or set `resi-cache.default-ttl`.
A cache configured with no expiry (a caller-supplied
Expand Down
11 changes: 7 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,20 @@ outputs ignored; do not force-add them to bypass this boundary.

Use the repository PR template. Summarize the behavior and evidence, identify
compatibility impact, and state what was not run. Documentation-only changes
still need link/reference review and the docs contract check.
still need link/reference review and the docs contract check. The shared CI
classifies only its explicit documentation path allowlist as docs-only; changes
to Java comments, templates, workflows, or scripts still run full verification.

Be respectful and constructive. This is a best-effort project; assume good
intent and keep discussions focused on the code and its evidence.

## Maintainers and bus factor

ResiCache is currently a **single-maintainer project** — all merges, releases,
and architectural decisions flow through `DavidHLP` (the only committer with
`CODEOWNERS` write access on `main`; `master` is retained only where legacy
workflow references still exist).
and architectural decisions flow through `DavidHLP`, the owner named in
[`.github/CODEOWNERS`](.github/CODEOWNERS).
`main` is the sole maintained branch. CODEOWNERS records review ownership;
repository permissions and branch protection are managed separately on GitHub.

**Bus factor: 1** is the current state, not an aspirational promise. Before a
`1.0.0` tag, this section must document either a named successor or a
Expand Down
69 changes: 49 additions & 20 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ResiCache Performance Benchmarks

This document captures the baseline JMH throughput numbers for core protection strategies and chain execution in ResiCache.
This document preserves historical JMH throughput measurements and explains
how to run the current benchmarks. The numbers are not current-build results
or enforced performance SLOs.
All measurements were produced by the `resicache-bench` module using JMH 1.37 on local development hardware.

---
Expand Down Expand Up @@ -40,30 +42,56 @@ These are not release SLO gates.

```bash
# 1. Install ResiCache core into your local Maven cache
mvn install -DskipTests -Djacoco.skip=true
./mvnw install -DskipTests -Djacoco.skip=true -B

# 2. Build the fat-jar
mvn -f resicache-bench/pom.xml clean package -DskipTests
./mvnw -f resicache-bench/pom.xml clean package -DskipTests -B

# 3. Run all benchmarks (~1-2 min)
java -jar resicache-bench/target/resicache-bench.jar -f 1 -wi 1 -i 2 -w 1s -r 1s -rf json -rff results.json
# 3. Run all benchmarks (duration depends on methods and parameter combinations)
java -jar resicache-bench/target/resicache-bench.jar -f 1 -wi 1 -i 2 -w 1s -r 1s -rf json -rff resicache-bench/target/results.json

# 4. Run a specific benchmark suite
java -jar resicache-bench/target/resicache-bench.jar BloomFilterBenchmark
```

---

## Benchmark Results
## Current benchmark names and CI smoke

Use JDK 21 for both build and execution. List runnable methods with
`java -jar resicache-bench/target/resicache-bench.jar -l`.
Historical identifiers below retain their original scores; renamed methods are
not newly measured results:

| Historical identifier | Current identifier / scope |
|---|---|
| `springNativeCacheLookup` | `concurrentMapLookup`: direct map access |
| `ttlJitter_concurrent_uniformity` | `ttlJitter_concurrentThroughput`: concurrent computation |
| `cost_1_handler_ttl` through `cost_5_handlers_full` | `cost_1_passthrough` through `cost_5_passthrough`: handler dispatch |

The current serialization suite additionally measures `storageRoundTrip` at
multiple payload sizes, without Redis network traffic. CI verifies the saved
core candidate, builds this module, and runs only a bounded protocol smoke:

```bash
java -jar resicache-bench/target/resicache-bench.jar \
'CacheSerializationBenchmark.storageRoundTrip' \
-p payloadBytes=64 -wi 0 -i 1 -r 100ms -f 1 -foe true \
-jvmArgs '-Xms128m -Xmx256m'
```

That smoke proves executable protocol behavior, not throughput thresholds.

## Historical benchmark results

### Benchmark 1 — Bloom Filter (`BloomFilterBenchmark`)
Measures JVM-level `LocalBloomIFilter` cache-penetration gate throughput.

| Benchmark | Params (bitSize, hashFunc) | Score (ops/s) | Interpretation | Status |
|---|---|---|---|---|
| `bloomMightContain_hit` | 8388608, 3 | **5,850,803** | True-positive fast path (bit-array read only) | **OK** (SLO ≥ 5.0 M ops/s) |
| `bloomMightContain_miss` | 8388608, 3 | **5,670,175** | Definitive negative, DB load bypassed | **OK** |
| `bloomPut` | 8388608, 3 | **4,296,647** | Insertion throughput on cache write | **OK** (SLO ≥ 1.0 M ops/s) |
| `bloomMightContain_hit` | 8388608, 3 | **5,850,803** | True-positive fast path (bit-array read only) | Historical |
| `bloomMightContain_miss` | 8388608, 3 | **5,670,175** | Local filter negative; no DB load measured | Historical |
| `bloomPut` | 8388608, 3 | **4,296,647** | Insertion throughput on cache write | Historical |

---

Expand All @@ -73,25 +101,25 @@ Measures single-flight leader-follower synchronization under cache breakdown / t
| Benchmark | Threads | Score (ops/s) | Interpretation | Status |
|---|---|---|---|---|
| `noSync` | 1 | **1,787,267** | Baseline: direct CPU-bound loader substitute | Reference |
| `syncLocalOnly_8threads` | 8 | **81,128,097** | Leader-follower coordination with 8 concurrent threads | **OK** |
| `syncContended_32threads` | 32 | **455,369,392** | Worst-case stampede (32 threads hammered on 1 key) | **OK** (gate remains stable) |
| `syncLocalOnly_8threads` | 8 | **81,128,097** | Leader-follower coordination with 8 concurrent threads | Historical |
| `syncContended_32threads` | 32 | **455,369,392** | Worst-case stampede (32 threads hammered on 1 key) | Historical |

---

### Benchmark 3 — TTL Jitter (`TtlJitterBenchmark`)
Measures `TtlHandler` Gaussian random variance calculation to prevent cache avalanche.
Measures `TtlPolicy` Gaussian random variance calculation to prevent cache avalanche.

| Benchmark | jitterRatio | Score (ops/s) | Interpretation | Status |
|---|---|---|---|---|
| `ttlBaseline` | 0.1 | **478,945,558** | Direct unjittered return baseline | Reference |
| `ttlJitter_compute` | 0.1 | **54,806,459** | Gaussian jitter per cache put (~18 ns overhead) | **OK** (SLO ≥ 10.0 M ops/s) |
| `ttlJitter_compute` | 0.2 | **52,697,903** | Configurable ratio variance sweep | **OK** |
| `ttlJitter_concurrent_uniformity` | 0.1 (8 threads) | **5,551,344** | Concurrent jitter computation throughput | **OK** |
| `ttlJitter_compute` | 0.1 | **54,806,459** | Gaussian jitter per cache put (~18 ns overhead) | Historical |
| `ttlJitter_compute` | 0.2 | **52,697,903** | Configurable ratio variance sweep | Historical |
| `ttlJitter_concurrent_uniformity` | 0.1 (8 threads) | **5,551,344** | Concurrent jitter computation throughput | Historical |

---

### Benchmark 4 — Chain Pass-Through (`ChainPassThroughBenchmark`)
Measures ResiCache `ChainEngine` pass-through execution against direct method calls and Spring-native map lookup.
Measures ResiCache `ChainEngine` pass-through execution against direct method calls and a direct concurrent-map lookup.

| Benchmark | Score (ops/s) | Avg Latency | Interpretation |
|---|---|---|---|
Expand All @@ -107,9 +135,10 @@ Measures the additive overhead per installed handler in the execution chain.
| Benchmark | Installed Handlers | Score (ops/s) | Marginal Delay |
|---|---|---|---|
| `cost_1_handler_ttl` | 1 (dispatch) | **31,466,289** | ~31.8 ns baseline |
| `cost_2_handlers` | 2 (TTL + Bloom) | **28,108,769** | +3.8 ns |
| `cost_3_handlers` | 3 (TTL + Bloom + Null) | **25,716,085** | +3.3 ns |
| `cost_4_handlers` | 4 (TTL + Bloom + Null + Sync) | **24,240,836** | +2.4 ns |
| `cost_2_handlers` | 2 (dispatch) | **28,108,769** | +3.8 ns |
| `cost_3_handlers` | 3 (dispatch) | **25,716,085** | +3.3 ns |
| `cost_4_handlers` | 4 (dispatch) | **24,240,836** | +2.4 ns |
| `cost_5_handlers_full` | 5 (dispatch) | **22,488,609** | +3.2 ns |

*Conclusion: Each additional protection handler adds ~2.5–3.8 ns of chain advancement overhead in memory.*
*In this historical run, additional pass-through dispatch nodes added roughly
2.4–3.8 ns each. These figures do not include real protection work or Redis I/O.*
Loading
Loading