From 03d80a5083a334b7bc01378146d1eb0f786fa537 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Fri, 18 Sep 2026 22:36:26 +0800 Subject: [PATCH] docs: consolidate documentation and remove stale ADRs --- .github/PULL_REQUEST_TEMPLATE.md | 30 +- .gitignore | 4 +- AGENTS.md | 13 +- CHANGELOG.md | 27 +- CLAUDE.md | 234 +++---- COMPATIBILITY.md | 9 +- CONTRIBUTING.md | 180 +++-- README.md | 631 +++--------------- README.zh-CN.md | 583 +++------------- SECURITY.md | 2 +- STABILITY.md | 6 +- docs/ARCHITECTURE.md | 144 ++++ docs/DEVELOPMENT.md | 85 +++ docs/OPERATIONS.md | 103 +++ docs/PRODUCT.md | 125 ++++ docs/README.md | 92 +++ docs/REFERENCE.md | 106 +++ docs/adr/0001-interface-contract-closure.md | 553 --------------- docs/adr/README.md | 5 - scripts/ci/check-docs-contracts.sh | 81 ++- .../cache/redis/cache/CacheErrorHandler.java | 10 +- .../redis/cache/CacheFailureReporter.java | 4 +- .../redis/cache/CacheOperationException.java | 6 +- .../spring/cache/redis/cache/ChainEngine.java | 4 +- .../redis/cache/DistributedLockManager.java | 6 +- .../cache/redis/cache/EarlyRefresh.java | 9 +- .../cache/redis/cache/FailureDiagnostics.java | 2 +- .../cache/redis/cache/LoaderOrchestrator.java | 2 +- .../cache/redis/cache/RedisProCache.java | 2 +- .../cache/RedisProCacheConfiguration.java | 2 +- .../redis/cache/RedisProCacheWriter.java | 2 +- .../cache/redis/cache/RefreshRetryPolicy.java | 2 +- .../cache/SerializationMigrationEngine.java | 6 +- .../spring/cache/redis/cache/SyncRole.java | 6 +- .../spring/cache/redis/cache/SyncSupport.java | 2 +- .../ThreadPoolEarlyExpirationExecutor.java | 2 +- .../spring/cache/redis/chain/CacheResult.java | 2 +- ...alCacheOperationExceptionContractTest.java | 4 +- .../redis/PublicSurfaceContractTest.java | 2 +- .../cache/BloomFailureLogKeyPrivacyTest.java | 6 +- .../redis/cache/BloomFilterHandlerTest.java | 4 +- .../cache/BloomFilterIntegrationTest.java | 6 +- .../redis/cache/CacheErrorHandlerTest.java | 4 +- .../redis/cache/CacheFailureReporterTest.java | 2 +- .../redis/cache/FailureLogKeyPrivacyTest.java | 10 +- .../redis/cache/LoaderOrchestratorTest.java | 4 +- .../PathCAopContractIntegrationTest.java | 4 +- .../RedisCacheSemanticsIntegrationTest.java | 2 +- ...edisDownFaultInjectionIntegrationTest.java | 2 +- .../cache/RedisProCacheLoadPathTest.java | 6 +- .../cache/redis/cache/TestCacheService.java | 2 +- .../cache/redis/chain/CacheResultTest.java | 2 +- 52 files changed, 1178 insertions(+), 1964 deletions(-) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/OPERATIONS.md create mode 100644 docs/PRODUCT.md create mode 100644 docs/README.md create mode 100644 docs/REFERENCE.md delete mode 100644 docs/adr/0001-interface-contract-closure.md delete mode 100644 docs/adr/README.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 171af842..c6285836 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,6 @@ ## Summary @@ -10,21 +9,28 @@ ## Checklist -- [ ] `./mvnw clean verify -B` passes locally (coverage gate). -- [ ] `./mvnw checkstyle:check -B` passes locally. +- [ ] `./mvnw clean verify -B` passes locally when source/build behavior changed (coverage gate). +- [ ] `./mvnw checkstyle:check -B` passes when Java source changed. - [ ] New behavior has tests; bug fixes have a regression test. -- [ ] Integration tests touching Redis extend `AbstractRedisIntegrationTest` (Testcontainers — Docker must be running). +- [ ] Redis integration tests use `AbstractRedisIntegrationTest`; Cluster tests use `AbstractRedisClusterIntegrationTest` (Testcontainers — Docker must be running). - [ ] `bash scripts/ci/check-test-names.sh` passes; integration classes do not use `*IT.java`. -- [ ] No over-engineering: features that belong in [Resilience4j](https://resilience4j.readthedocs.io/) (circuit breaking / rate limiting) or [Caffeine](https://github.com/ben-manes/caffeine) (multi-level caching) are **out of scope** — see README "Not in Scope". -- [ ] Javadoc on public API; Chinese rationale comments are welcome for design decisions (matching the existing codebase style). +- [ ] `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. +- [ ] Public API/configuration/wire changes are checked against [`STABILITY.md`](../STABILITY.md) and [`COMPATIBILITY.md`](../COMPATIBILITY.md). +- [ ] No over-engineering: features that belong in [Resilience4j](https://resilience4j.readthedocs.io/) (circuit breaking / rate limiting) or [Caffeine](https://github.com/ben-manes/caffeine) (multi-level caching) are out of scope unless the product boundary changes explicitly. +- [ ] Javadoc on public API; Chinese rationale comments are welcome for design decisions matching the existing codebase style. - [ ] Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `ci:`, `chore:`. ## Backward compatibility + +- [ ] Compatibility impact is described, or this change is documentation/internal-only. +- [ ] Any pre-1.0 internal/default/metric-namespace change is recorded in [`CHANGELOG.md`](../CHANGELOG.md), with a `⚠️` marker when user-visible. diff --git a/.gitignore b/.gitignore index 1f3f718d..f2e8977a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,8 +39,8 @@ !SECURITY.md !PERFORMANCE.md !docs/ -!docs/adr/ - +docs/adr/ +docs/plans/ # --- 项目配置 --- !.editorconfig !.cbmignore diff --git a/AGENTS.md b/AGENTS.md index 4017a676..23c1673d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,17 @@ # Agent Project Instructions Project conventions live in [CLAUDE.md](./CLAUDE.md), including the -task-scoped agent workflow. Read the relevant sections for the current task; -linked documents are references, not a mandatory reading checklist. +project-scoped workflow and verification commands. Read the relevant sections; +linked documents are pointers, not a requirement to load the whole repository. -Keep shared project instructions in `CLAUDE.md` and this file as the entry point. +For documentation or architecture work, start with +[`docs/README.md`](docs/README.md), then read only the task-relevant core page. +Keep one canonical owner per topic. Verify current behavior against source, +configuration, tests, and scripts before changing prose. + +Keep shared agent instructions in `CLAUDE.md` and this file as the entry point. +Do not turn task logs, dated summaries, generated reports, or ignored local +status files into permanent current-state documentation. For documentation maintenance, verify commands and versions against `pom.xml` and the relevant CI scripts. Preserve existing local edits and update the diff --git a/CHANGELOG.md b/CHANGELOG.md index fa80dc7d..372a8b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,13 @@ All notable changes to ResiCache are documented here. Format follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with the pre-1.0 caveat below. -## Pre-1.0 caveat +While the version is `0.x`, behavior and APIs outside the surfaces explicitly +covered by [`STABILITY.md`](./STABILITY.md) may change in any release, +including patch releases. Breaking changes are marked with ⚠️ in this +changelog. The stable 0.x surfaces and their exceptions are defined by +`STABILITY.md`; do not use this general pre-1.0 caveat to override that +contract. -While the version is `0.x`, **APIs may change in any release — including -patch releases.** Breaking changes are marked with ⚠️ in this changelog. -API stability is only guaranteed from `1.0.0` onward; see -[`STABILITY.md`](./STABILITY.md) for the contract that defines which -surfaces are stable in 0.x and which change. > No local `v1.0` release tag is treated as an API-stability baseline here. > Maven Central publishes `io.github.davidhlp:ResiCache` 0.0.1–0.0.5, 0.0.7, > and 3.2.4, but every version (verified 2026-09-05 from the published @@ -66,9 +66,10 @@ Current milestones: `resi-cache.sync-lock.local-only=true` for explicit single-instance degradation. - ⚠️ **Bloom CLEAN semantics** — ordinary cache eviction no longer clears the - Bloom filter or relies on a rebuilding marker/TTL window. Bloom bits describe - possible data-source membership, so retained bits can only create - false-positives; valid loader calls are not blocked by false-negatives. + Bloom filter or relies on a rebuilding marker/TTL window. Bloom bits describe + possible data-source membership: false-positives are safe, but a missing + membership bit can short-circuit Redis lookup and loader execution. Seed or + maintain membership before enabling it for an existing data set. - ⚠️ **Read-through is availability-first** — `get(key, loader)` (default and sync paths) always returns a successful loader value; a cache write-back failure after a successful load is logged (redacted, no raw key) and never @@ -102,7 +103,7 @@ Current milestones: same put metrics as the sync path. Failing write-backs are still logged redacted and never override the loaded value. - **Key privacy in failure diagnostics** — WARN/ERROR logs and typed exception - messages omit the raw key (ADR-0001 §15); where no `cacheName` is available + messages omit the raw key; where no `cacheName` is available they carry `FailureDiagnostics.keyFingerprint` instead. Covers distributed lock acquire/release, `SyncRole` leader/follower failures, the async early-expiration retry path, chain post-processing, and the migration @@ -110,13 +111,13 @@ Current milestones: - **Redacted diagnostics at every failure site** — the chain’s observer dispatch and all six Bloom failure paths now render only the exception type chain at WARN/ERROR (`FailureDiagnostics.sanitizedFailure`) and keep the full stack at DEBUG, closing the - remaining ADR-0001 §15 gaps. Fail-open behavior and the Bloom failure counters are + remaining key-privacy gaps. Fail-open behavior and the Bloom failure counters are unchanged. - **Sync early-expiration answers with a miss** — a synchronous early-expiration skip (`EarlyExpirationMode.SYNC`) now returns `CacheResult.miss()` through the chain instead of a null-byte `success`: `EarlyExpirationHandler` advances to `ActualCacheHandler`, which consumes the documented `PrefetchDecision` and - answers the miss its Javadoc and ADR-0001 §22 describe. Writer and Spring + answers the miss its Javadoc and current handler contract describe. Writer and Spring behavior is unchanged (null bytes, miss statistics, loader refresh); chain observers and SPI callers now receive `Outcome.MISS`. - ⚠️ **`@RedisCachePut` metadata now resolves** — the chain used to read only @@ -150,7 +151,7 @@ Current milestones: one evaluation, publishes the typed `PrefetchDecision` (so the value the decision read is reused instead of fetched twice) and maps the outcome to `SKIP_ALL`/`CONTINUE`. The executor, retry policy and cancellation seam are - unchanged (ADR-0001 §11). + unchanged (see [`STABILITY.md`](./STABILITY.md)). - **Nested chain advancement is explicit (SPI addition, compatible)** — `CacheHandler` gains `default HandlerResult handle(CacheContext, ChainContinuation next)`; the default ignores `next` and delegates to diff --git a/CLAUDE.md b/CLAUDE.md index 62d98d93..48a95b28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,156 +2,86 @@ ## Agent workflow -- Start with the requested outcome and inspect the affected files. For a small, - clear task, proceed directly; use a plan or broader investigation when scope, - dependencies, or risk warrant it. -- Choose tools that answer the current question. Prefer the code graph for - structural discovery when available; use direct reads or search for known - paths, literals, configuration, and documentation. If graph results are - unavailable, stale, or incomplete, verify the relevant source and continue. - Check coverage when relying on graph evidence; an empty result alone does not - establish that code is absent. -- At session start or after compaction, confirm the graph project, root, and - index generation with `list_projects` or `index_status`. Use Tier 2 verification - by default: `search_graph` for symbols, `trace_path` for relevant callers or - callees, and `get_code_snippet` for material source evidence. Check pagination - and call `check_index_coverage` for all evidence paths (plus bounded scopes - for absence claims). Read current source for stale, excluded, partial, or - unknown coverage; a clean coverage result is not proof of completeness. -- Treat memory and documentation as context. Resolve conflicts against current - source, configuration, and observed behavior. Distinguish verified facts, - assumptions, and unknowns; never invent tool results or completed checks. -- Reuse existing patterns and make the smallest change that satisfies the task. - Preserve unrelated work. Ask for clarification when ambiguity materially - changes the outcome or an action exceeds the user's authorization; otherwise - use a reasonable default. +- Start from the requested outcome and inspect the affected files. Use a plan + when scope, dependencies, or risk warrant it; otherwise make the smallest + change that satisfies the task. +- At session start or after compaction, confirm the ResiCache code-graph project + and index status. Use graph queries for structural code discovery when the + result is available, then verify relevant source directly. A clean or partial + index is not proof that a symbol or file is absent. +- Treat documentation and memory as context, not executable truth. Resolve + conflicts against current source, build configuration, contracts, tests, and + observed behavior. Distinguish verified facts, assumptions, and unknowns. +- Preserve unrelated work. Do not change product code, public APIs, + dependencies, runtime configuration, or external systems unless the task + explicitly includes them. - Run checks appropriate to the change. Documentation-only edits normally need - a diff and link review, not a Java build. For behavior changes, run focused - tests and applicable project gates; report what ran and any remaining limits. -- Stop investigating when there is enough evidence to implement and validate - the requested change. Repeat a check only for new changes, failures, or an - unresolved question. If a tool repeatedly fails, change approach or report - the blocker instead of retrying unchanged. - -## Tech Stack - -| Layer | Technology | Version | -|-------|-----------|---------| -| Language | Java | 21 | -| Framework | Spring Boot | 4.0.0 | -| Cache | Spring Cache + Spring Data Redis | - | -| Distributed Lock | Redisson | 3.50.0 | -| Local Cache | Caffeine | 3.1.8 | -| Build | Maven | 3.x | -| Testing | JUnit 5 + Testcontainers + AssertJ + Awaitility | - | - -> Tech Stack 表为单构建口径(Boot 4.0 / Java 21 / Redisson 3.50.0 单构建线)。重构后已无 `wrapper/`/`spi/`/`event/`/`evaluator/`/`CacheMetricsRecorder`,目录树见下方 Project Structure + 已移除 callout。 - -## Code Style - -- **Naming**: Java standard PascalCase for classes, camelCase for methods/fields -- **Checkstyle**: Enforced by `./mvnw checkstyle:check -B` -- **Lombok**: Used throughout - `@Data`, `@Getter`, `@Setter`, `@Builder` -- **Javadoc**: Chinese comments explaining design rationale in key classes - -## Documentation authorities - -Keep one canonical owner per topic; linked documents provide navigation, not a -second copy of the contract: - -- Public overview and quick start: `README.md`; `README.zh-CN.md` is the - translated companion and does not override the English contract. -- Stable public surface: `STABILITY.md`; supported build line and runtime - limitations: `COMPATIBILITY.md`. -- Accepted design rationale: `docs/adr/README.md`; change history: - `CHANGELOG.md`; contributor workflow: `CONTRIBUTING.md`. -- Current local task/deferred status: `.agent/tasks/resicache-maturity.yaml`. - Closed plans, checkpoints, and review notes are not recreated after their - durable facts have been absorbed into the sources above. - -## Testing - -- **Run tests**: `./mvnw test` -- **No-Docker unit tests**: `./mvnw -Punit test -B`; this profile excludes - `**/*IntegrationTest*.java`, including nested integration test classes. - Passing it does not establish real Redis or Cluster behavior. -- **Run with coverage**: `./mvnw verify` (JaCoCo enforced at 70% line / 40% branch coverage) -- **Integration tests**: Use Testcontainers for Redis, extend `AbstractRedisIntegrationTest` -- **Pattern**: Test classes mirror source structure under `src/test/java/` -- **Container-test naming check**: `bash scripts/ci/check-test-names.sh`. - Container-backed tests must use `*IntegrationTest.java`, subject to the - script's explicit fixture/helper exceptions. - -Use JDK 21. Full verification needs a working Docker environment and the -Testcontainers images; report an environment blocker separately from a test -failure. Checkstyle is a separate gate, not part of Maven `verify`. - -## Build & Run - -- **Dev build**: `./mvnw clean compile` -- **Full verify**: `./mvnw clean verify -B` -- **Checkstyle only**: `./mvnw checkstyle:check -B` -- **Package**: `./mvnw clean package -DskipTests` - -## Project Structure - -``` -ResiCache/ -├── resicache-bench/ # Standalone JMH micro-benchmark module (JMH 1.37, shade fat-jar, 5 benchmark suites) -└── src/main/java/io/github/davidhlp/spring/cache/redis/ - ├── annotation/ # @RedisCacheable/Put/Evict/Caching stable annotations - ├── cache/ # package-private runtime module: AOP, chain, operations, protections, serialization, assembly - ├── chain/ # stable CacheHandler/Operation/Result contracts and typed decision views - ├── config/ # RedisCacheAutoConfiguration + RedisProCacheProperties + metrics/enablement entries - ├── protection/ # stable BloomIFilter, LockManager, EarlyExpirationMode contracts - └── serialization/ # public SerializationException + operator migration contracts and wire envelope -``` - -> 已移除(不在源码中):`wrapper/`(熔断/限流)、`spi/`(ServiceLoader)、`event/`、独立 `evaluator/`、`CacheMetricsRecorder`、`BloomRebuilder`,以及已收拢至 package-private `cache/` 的旧实现子包。文档始终以实际源码为准。 - -### Test Structure - -``` -src/test/java/io/github/davidhlp/spring/cache/redis/ -├── cache/ # implementation unit/integration tests + Testcontainers fixtures -├── chain/ + config/ # stable contract and configuration tests -├── PublicSurfaceContractTest.java # top-level and nested public-surface manifest -└── com/example/ # external-consumer and serializer domain fixtures -``` -> Redis integration tests now live beside the internal cache module; all use the -> `*IntegrationTest.java` suffix. The naming guard checks container markers and -> rejects container-backed tests with other names, including `*IT.java`. -## Key Architecture: Chain of Responsibility - -Cache operations that use ResiCache go through a chain of handlers (in order): - -1. **BloomFilterHandler** (100) - Checks if key exists in bloom filter, blocks cache penetration -2. **SyncLockHandler** (200) - Acquires distributed lock, prevents cache breakdown -3. **EarlyExpirationHandler** (250) - Triggers async early refresh for hot keys -4. **TtlHandler** (300) - Applies TTL variation to prevent cache avalanche -5. **NullValueHandler** (400) - Caches null values to prevent cache penetration -6. **ActualCacheHandler** (500) - Executes actual Redis PUT - -Each handler implements `CacheHandler` interface with `handle()` method. - -## Conventions - -- **Handler ordering**: Defined by `@HandlerPriority(HandlerOrder)` enum in `chain/HandlerOrder.java` (gap=100, single source of truth) -- **Configuration properties**: Use `@ConfigurationProperties(prefix = "resi-cache")` with nested properties classes -- **Checkstyle**: Runs through the explicit `checkstyle:check` command, not as part of `verify`. -- **Strategy replacement**: only documented stable seams (`BloomIFilter` and `LockManager`) - are replaceable. Other policies, handlers, metadata, and serializers are package-private - implementation details assembled by the internal `cache` module. The public auto-configuration - scans that internal package with a test-class exclusion; no root-package scan is used. - -## Where to Look - -| I want to... | Look at... | -|--------------|-----------| -| Understand the chain / a mechanism | stable contracts in `chain/`, runtime handlers in `cache/`, and replaceable seams in `protection/` | -| Understand a module | the package itself under `src/main/java/.../`; module layout is in Project Structure above | -| Add a new cache protection handler | internal `cache/` runtime + implement `CacheHandler`, annotate `@HandlerPriority(HandlerOrder.X)` | -| Modify annotation processing | internal `cache/` annotation pipeline | -| Change Redis connection config | internal `cache/RedisConnectionConfiguration.java` | -| Configure behavior | `config/RedisProCacheProperties.java` (`resi-cache.*` prefix) | -| Add integration tests | internal `cache/` test fixtures + Testcontainers | + a diff and link/reference review plus the docs contract script, not a Java + build. Report environment blockers separately from test failures. + +## Documentation authorities and reading route + +The progressive-disclosure entry point is [`docs/README.md`](docs/README.md): + +1. read this instruction surface and `AGENTS.md`; +2. read the documentation map and current local task ledger when present; +3. read only the relevant current-state document; +4. verify claims against source, build files, tests, and scripts; +5. read changelog entries or generated reports only for needed history. + +Canonical ownership is: + +- public overview and quick start: `README.md`; `README.zh-CN.md` is the + translated companion and does not override the English contract; +- current product, architecture, development, operations, and reference: + `docs/PRODUCT.md`, `docs/ARCHITECTURE.md`, `docs/DEVELOPMENT.md`, + `docs/OPERATIONS.md`, and `docs/REFERENCE.md`; +- stable public surface: `STABILITY.md`; supported build line and limits: + `COMPATIBILITY.md`; +- current architecture and design rationale: `docs/ARCHITECTURE.md`; + versioned history: `CHANGELOG.md`; historical performance evidence: + `PERFORMANCE.md`; +- contributor and security policy: `CONTRIBUTING.md` and `SECURITY.md`; +- current local task/deferred status: `.agent/tasks/resicache-maturity.yaml`. + +The task ledger is ignored and may be absent in a fresh clone. It is status, +not a design or contract source. Closed plans, checkpoints, and review notes +are not recreated after their facts have been absorbed into the current +documentation and source. + +## Tech stack and exact sources + +| Layer | Technology | Exact source | +|---|---|---| +| Language | Java 21 | `pom.xml` and Maven Enforcer | +| Framework | Spring Boot 4.0.0 / Spring 7 | `pom.xml` | +| Cache | Spring Cache + Spring Data Redis 4.0.x | `pom.xml` | +| 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/` | + +Do not copy dependency versions into a second contract. The current source tree +and module ownership are in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). +Only the documented stable seams (`BloomIFilter`, `LockManager`, and the +handler/observer/value contracts) are replaceable; `cache/` is an internal +runtime module. + +## Coding and verification conventions + +- Use Java naming and existing Lombok patterns. +- Keep public API Javadoc and explain non-obvious design rationale near the + source; keep current behavior in the canonical docs. +- Handler ordering comes from `@HandlerPriority(HandlerOrder)` in + `chain/HandlerOrder.java`; do not duplicate numeric priorities. +- Integration tests use Testcontainers fixtures and the `*IntegrationTest.java` + suffix. The naming guard is `bash scripts/ci/check-test-names.sh`. +- Unit path: `./mvnw -Punit test -B`. +- Full Redis/coverage path: `./mvnw clean verify -B` (70% line / 40% branch). +- 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`. + +Full command semantics, test layers, CI, and contribution checks live in +[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) and +[`CONTRIBUTING.md`](CONTRIBUTING.md). diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 6ce9e9d3..e0a77053 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -33,6 +33,7 @@ baseline. | Redis Server | 7.x | 7.x | | Redisson | 3.50.0 | 3.50.0 | | Caffeine | 3.1.8 | 3.1.8 | +| Testcontainers | 1.20.6 (test scope) | CI/integration tests | ## Spring Boot version policy @@ -56,7 +57,8 @@ baseline. explicitly configured. | | **Micrometer / Actuator** | Optional | Cache metrics require `resi-cache.metrics.enabled=true` (default OFF) and a `MeterRegistry`. - `RedisCacheHealthIndicator` requires Actuator. | + `RedisCacheHealthIndicator` additionally requires Actuator, the + `HealthIndicator` class, and the same metrics property set to `true`. | | **Caffeine** | Bundled | Used internally for the local hash cache and bloom-filter bitset; not exposed as a multi-level cache. | @@ -117,8 +119,9 @@ not require a cache flush. has no unrelated absolute 60-second bypass. - **Bloom CLEAN semantics**: Bloom tracks possible data-source membership, not current cache entries. CLEAN preserves existing bits and never uses a - rebuilding marker or TTL window; false-positives are safe, while loader - execution must not be blocked by a Bloom false-negative. + rebuilding marker or TTL window; false-positives are safe, but a missing bit + may short-circuit Redis lookup and loader execution. Seed or maintain Bloom + membership before enabling it for an existing data set. - **User `CacheManager` opt-out**: defining your own `CacheManager` bean backs off the library's `RedisProCacheManager` and, with it, the ResiCache annotation proxy (`redisCacheAdvisor`/`redisCacheInterceptor`); your Spring diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26962c6b..b485d45c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,138 +1,112 @@ # Contributing to ResiCache -Thanks for your interest in improving ResiCache! This is a small, -single-maintainer, **Non-SLA best-effort** project — PRs of all sizes are -welcome, and the bar below keeps the project healthy. +Thanks for improving ResiCache. This is a small, single-maintainer, +**non-SLA best-effort** project. The repository prefers a small change that +fits the existing contracts over speculative framework surface. ## Before you start -- ResiCache is **pre-1.0**: APIs may change. If your change alters a public API, - please open an issue to discuss it first. -- Read [README.md](README.md) (Known Limitations + Not in Scope) and - [CLAUDE.md](CLAUDE.md) (Project Structure + Key Architecture) so your change - fits the architecture. +- ResiCache is pre-1.0. If a change alters a documented public API, property + key, wire format, or SPI behavior, open an issue before implementation. +- Read [`README.md`](README.md) for the runnable entry point and + [`docs/PRODUCT.md`](docs/PRODUCT.md) for scope and non-goals. +- Read [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for ownership and + [`STABILITY.md`](STABILITY.md) before depending on a public type. +- Check the existing issues, architecture map, and change history before + adding a new extension point or repeating a closed design. -## Development setup +## Development setup and checks -Requirements: **JDK 21** (matches `pom.xml` `21`), -**Maven 3.x** (the wrapper `./mvnw` is bundled), -**Docker** (for Testcontainers-based integration tests). +Requirements are **JDK 21**, **Maven 3.x** (the bundled `./mvnw` is preferred), +and **Docker** for Testcontainers-backed Redis and Cluster tests. The command +matrix and evidence boundaries are in +[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md). + +At minimum, run the checks relevant to the change: ```bash -./mvnw clean verify -B ./mvnw -Punit test -B +./mvnw clean verify -B ./mvnw checkstyle:check -B -./mvnw clean package -DskipTests -B bash scripts/ci/check-test-names.sh +bash scripts/ci/check-docs-contracts.sh ``` -`./mvnw -Punit test -B` is the no-Docker daily path. It does not verify real -Redis/Cluster behavior and is not a release gate; `./mvnw clean verify -B` -remains the full Redis proof. - -The `verify` goal enforces a JaCoCo coverage gate: - -- **70% line coverage** -- **40% branch coverage** - -A PR that drops below these thresholds will fail CI. **If you add code, add -tests.** - -- [ ] `./mvnw clean verify -B` passes locally (including the coverage gate). -- [ ] `./mvnw checkstyle:check -B` passes locally. -- [ ] New behavior has tests; bug fixes have a regression test. -- [ ] Integration tests touching Redis extend `AbstractRedisIntegrationTest` - (Testcontainers — Docker must be running). -- [ ] No over-engineering: features that belong in - [Resilience4j](https://resilience4j.readthedocs.io/) (circuit breaking / - rate limiting) or [Caffeine](https://github.com/ben-manes/caffeine) - (multi-level caching) are **out of scope** — see README "Not in Scope". -- [ ] Javadoc on public API; Chinese rationale comments are welcome for design - decisions (matching the existing codebase style). -- [ ] Commit messages follow - [Conventional Commits](https://www.conventionalcommits.org/): - `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `ci:`, `chore:`. +The unit profile is a no-Docker path and excludes `**/*IntegrationTest*.java`; +it does not prove real Redis behavior. `clean verify` is the full Redis and +coverage path and enforces 70% line / 40% branch coverage. If code changes, +add or update tests; if a bug is fixed, add a regression test when the failure +is reproducible. ## Architecture pointers -| You're touching... | Start here | +| Change | Start here | |---|---| -| A protection mechanism | internal `cache/` runtime + `chain/CacheHandlerChainFactory` | +| Product behavior or a new capability | [`docs/PRODUCT.md`](docs/PRODUCT.md) | +| Module boundary or handler order | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and `chain/HandlerOrder.java` | +| Protection mechanism | internal `cache/` runtime and `CacheHandlerChainFactory` | | Annotation handling | internal `cache/` annotation pipeline | -| Auto-configuration | `config/RedisCacheAutoConfiguration` + `RedisProCacheProperties` | -| Serialization | `serialization/SecureJackson*` | +| Auto-configuration or properties | `config/RedisCacheAutoConfiguration` and `RedisProCacheProperties` | +| Serialization | `serialization/` and the serializer tests | | Cache core | `cache/RedisProCache`, `RedisProCacheManager`, `RedisProCacheWriter` | - -See [CLAUDE.md](CLAUDE.md) (Key Architecture + Where to Look) for the design -rationale and source pointers. +| Runtime/migration behavior | [`docs/OPERATIONS.md`](docs/OPERATIONS.md) and [`docs/REFERENCE.md`](docs/REFERENCE.md) | ## Adding a protection handler -1. Create a class in the internal `cache/` runtime implementing `CacheHandler` - (extend `AbstractCacheHandler`). -2. Annotate it `@HandlerPriority(HandlerOrder.YOUR_ORDER)` — `HandlerOrder` is - the single source of truth for ordering (gap = 100, extend the enum to insert). -3. Annotate it `@Component` — the internal `cache/` runtime package is the - only package `RedisCacheAutoConfiguration` scans (test classes excluded); - no root-package scan is used. Internal `@Configuration` classes inside - `cache/` are picked up by the same internal-only scan. -4. Add tests; document the mechanism's design rationale in Javadoc on the - handler class (matching the existing codebase style). - -The full extension protocol (non-null `HandlerResult`, `FlowControl` -semantics, post-process isolation, observer hook order, scope tokens, -thread safety, nested public type classification) is normative in -[`STABILITY.md`](./STABILITY.md) §4; the nested public type list is pinned -by `src/test/resources/allowlist/public-surface-nested.txt` and -`PublicSurfaceContractTest`. - -See [CLAUDE.md](CLAUDE.md) § Key Architecture: Chain of Responsibility for the -handler-ordering model, and the `protection/` packages for worked examples of -existing handlers. - -## Code of conduct +1. Add the implementation to the internal `cache/` runtime and implement + `CacheHandler` (extend `AbstractCacheHandler` when its behavior fits). +2. Use `@HandlerPriority(HandlerOrder.YOUR_ORDER)`; `HandlerOrder` is the single + ordering source and leaves gaps for intentional insertion. +3. Register the handler as an internal `@Component`. The library scan covers + only the internal runtime package and excludes test classes; host handlers + must be discovered by the host application or supplied as a bean. +4. Add focused tests and document non-obvious design rationale in public + Javadoc or the owning current-state document. + +The handler/observer protocol, non-null results, flow control, post-processing, +thread-safety, scope tokens, and nested advancement rules are normative in +[`STABILITY.md`](STABILITY.md). The public nested-type list is pinned by the +allowlist and `PublicSurfaceContractTest`. + +## Documentation changes + +Update the existing canonical page named in [`docs/README.md`](docs/README.md). +Keep README focused on adoption and quick start; do not create a task-, date-, +phase-, or session-specific permanent guide. If a new current-state document is +truly necessary, document its reader, distinct responsibility, and lifecycle in +the documentation map. Preserve changelog history, performance evidence, and +unresolved task entries. + +## Pull requests + +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. Be respectful and constructive. This is a best-effort project; assume good -intent and keep discussions focused on the code. +intent and keep discussions focused on the code and its evidence. -## Maintainers & bus factor +## 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). -**Bus factor: 1** (current). This is honest, not aspirational. - -We publicly track this because it matters for downstream evaluation. See -[`STABILITY.md`](STABILITY.md) §4 1.0 graduation criterion #6 — graduation -requires either a **named successor** (someone who can carry the project -forward if the maintainer disappears) **or** a **documented succession plan** -(e.g. an org transfer, a publisher hard-takeover clause, or a fork governance -agreement). - -What this means in practice today: - -- **Pre-1.0**: bus factor 1 is acceptable. The project is explicitly - best-effort, no SLA, no production adopters are pinned to it. -- **At 1.0 graduation**: this section must be rewritten to document either a - successor or a plan before the `1.0.0` tag is cut. The graduation criteria - are an explicit pre-flight checklist for this kind of risk. - -If a serious downstream evaluation finds bus factor 1 unacceptable, file an -issue — the maintainer is open to succession conversations and to a publisher -hand-off, not to abandoning the project. +**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 +succession plan. Pre-1.0, the project remains explicitly best-effort with no +SLA and no pinned production-adopter guarantee. -## Releases & CI infrastructure +## Releases and CI infrastructure -CI runs on every push to `main` or `master` and every PR via +CI runs on pushes to `main` or `master` and on pull requests through [`.github/workflows/ci.yml`](.github/workflows/ci.yml) and -[`.github/workflows/pr.yml`](.github/workflows/pr.yml). The composite action -[`.github/actions/setup-jdk-21/action.yml`](.github/actions/setup-jdk-21/action.yml) -centralizes the JDK distribution and Maven cache configuration. The POM's -`21` remains the compiler and Enforcer source of -truth; CI must keep the action input aligned. - -Release-time secrets (`OSSRH_*`, `GPG_*`) are configured at the repository / -environment level out of band by the maintainer. Do not edit `release.yml` -to add secrets — open an issue first. +[`.github/workflows/pr.yml`](.github/workflows/pr.yml). The composite +[setup-jdk-21 action](.github/actions/setup-jdk-21/action.yml) centralizes JDK +and Maven cache setup; `pom.xml` remains the Java-version source of truth. + +Release-time `OSSRH_*` and `GPG_*` secrets are configured at repository or +environment level out of band. Do not edit `release.yml` to add secrets; open +an issue first. Release behavior and the current publication boundary are in +[`docs/OPERATIONS.md`](docs/OPERATIONS.md). diff --git a/README.md b/README.md index fdf2b9d4..81d0e07d 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,56 @@ # ResiCache -**Protection-in-depth for Spring Cache on Redis.** ResiCache adds declarative +**Protection-in-depth for Spring Cache on Redis.** ResiCache adds explicit cache-penetration, cache-breakdown, cache-avalanche, and hot-key refresh -protection through a composable responsibility chain, while keeping Spring -Cache as the application-facing model. +protection while keeping Spring Cache as the application-facing model. [![CI](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml/badge.svg)](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[English](README.md) · [简体中文](README.zh-CN.md) +[English](README.md) · [简体中文](README.zh-CN.md) · +[Documentation map](docs/README.md) > [!WARNING] > ResiCache is pre-1.0 (`v0.0.2`), non-SLA, and currently maintained by one -> maintainer. The current `main` line targets Spring Boot 4.0 and Java 21, but -> it does not have a matching Maven Central artifact yet. Read -> [Compatibility](COMPATIBILITY.md) and [Limitations](#limitations) before -> adopting it in production. +> maintainer. The current source line targets Spring Boot 4.0 and Java 21, but +> it does not have a matching Maven Central artifact. Read the +> [compatibility matrix](COMPATIBILITY.md) before adopting it. -## Contents +## What it provides -- [Overview](#overview) -- [Features](#features) -- [Compatibility and requirements](#compatibility-and-requirements) -- [Quick start](#quick-start) -- [How it works](#how-it-works) -- [Configuration](#configuration) -- [Extension points](#extension-points) -- [Runtime semantics](#runtime-semantics) -- [Comparison](#comparison) -- [Limitations](#limitations) -- [Not in scope](#not-in-scope) -- [Project layout](#project-layout) -- [Development](#development) -- [Project status and support](#project-status-and-support) -- [Security](#security) -- [License](#license) - -## Overview - -Spring Cache provides a consistent programming model for caching, but it does -not by itself provide protection against cache penetration, breakdown, avalanche, -or hot-key expiry. ResiCache adds those protections as explicit capabilities on -top of Spring Cache and Redis. - -ResiCache is designed to: - -- **Keep Spring Cache at the boundary**: applications still use - `@EnableCaching`, cache operations, and Spring's cache abstractions. -- **Make protection composable**: each protection mechanism is a typed handler - in one ordered chain rather than unrelated advice scattered across services. -- **Fail closed at important boundaries**: missing distributed-lock support, - unsafe serialization, and invalid configuration are observable instead of - silently becoming weaker guarantees. -- **Stay focused**: it is not a replacement for a multi-level cache, - circuit-breaker, rate-limiter, or reactive cache framework. - -## Features - -| Capability | What it provides | +| Capability | Purpose | |---|---| -| Bloom filter | Rejects requests for keys that are not known to exist, reducing cache penetration | -| Distributed lock | Redisson-based coordination for cache breakdown protection | -| TTL jitter | Randomizes expiration to reduce synchronized cache avalanche | -| Null-value caching | Caches negative lookups when explicitly enabled | -| Early expiration | Refreshes hot keys before their normal expiration boundary | -| Composable handler chain | Orders protection mechanisms through `HandlerOrder` and `@HandlerPriority` | -| Safe serialization | Uses a controlled deserialization whitelist and an internal wire envelope | -| Spring Cache integration | Works with Spring Cache instead of introducing a second AOP model | - -All five protection attributes on `@RedisCacheable` default to `false`; enable -the mechanisms that match the application's risk profile. - -## Compatibility and requirements - -The repository currently ships one supported build line: +| Bloom filter | Avoids loading keys that are known not to exist | +| Distributed lock | Coordinates concurrent loads with Redisson or another `LockManager` | +| TTL jitter | Spreads expiration boundaries | +| Null-value caching | Retains negative lookups when explicitly enabled | +| Early expiration | Refreshes hot keys before normal expiry | +| Responsibility chain | Orders protection through `HandlerOrder` | +| Secure serialization | Uses a whitelisted `{version, payload}` envelope | +| Spring Cache integration | Reuses Spring's cache API and `@EnableCaching` boundary | -| Component | Current `main` line | -|---|---:| -| Java | 21 | -| Spring Boot | 4.0.0 | -| Spring Framework / Spring Cache | 7.x, through Boot 4 | -| Spring Data Redis | 4.0.x | -| Redis Server | 7.x | -| Redisson | 3.50.0, optional | -| Caffeine | 3.1.8, bundled for internal support | -| Docker | Required for Testcontainers-backed verification | -| Testcontainers | 1.20.6, test scope | +Protection is opt-in. The default `native-annotation-mode` is `SELECTIVE`, and +the five protection attributes on `@RedisCacheable` default to disabled. The +current product boundary and non-goals are in [`docs/PRODUCT.md`](docs/PRODUCT.md). -See the complete matrix and known compatibility boundaries in -[COMPATIBILITY.md](COMPATIBILITY.md). +## Compatibility -### Published artifact status +The supported repository line is: -The current Boot 4 / Java 21 line is source-first and has not been published to -Maven Central. The `io.github.davidhlp:ResiCache:0.0.2` coordinate is a -historical Boot 3 / Java 17 artifact; do not use it as a dependency for the -current `main` line. +- Spring Boot 4.0.0 / Spring Framework 7 / Spring Data Redis 4.0.x +- Java 21 +- Redis 7.x +- Redisson 3.50.0 when distributed synchronization is needed +- Caffeine 3.1.8 for internal support -To try the current line, [build it from source](#development) or use a release -that explicitly declares its supported compatibility line. +Boot 3.x is not a maintained compatibility line. The complete matrix, +serialization migration boundary, failure semantics, and known limitations are +in [`COMPATIBILITY.md`](COMPATIBILITY.md). ## Quick start -### 1. Build and install the current source line locally +The current line is source-first. Build and install the checkout for a local +consumer; this does not publish anything: ```bash git clone https://github.com/davidhlp/ResiCache.git @@ -111,28 +59,11 @@ cd ResiCache ./mvnw install -DskipTests -B ``` -The first command is the no-Docker contributor check. The second installs the -current source checkout into the local Maven repository for a local consumer; -it does not publish an artifact. Do not confuse that local build with the -historical `0.0.2` artifact on Maven Central. +Use the coordinates and version from the checkout's root `pom.xml` in the +consumer application. Do not treat the historical Maven Central `0.0.2` +artifact as the current Boot 4 line. -For a local consumer application, add the current checkout's coordinates to -that application's `pom.xml`: - -```xml - - io.github.davidhlp - ResiCache - 0.0.2 - -``` - -The dependency resolves from the local Maven repository. Continue with the -configuration and application examples below in that consumer application, -not in the ResiCache checkout. The full Redis and Redis Cluster verification -command is documented in [Development](#development). - -### 2. Configure Redis +Configure the two Redis clients explicitly when using synchronization: ```yaml spring: @@ -140,470 +71,86 @@ spring: redis: host: localhost port: 6379 - # timeout: 2s resi-cache: redis: mode: single host: localhost port: 6379 database: 0 - tls-enabled: false ``` -`spring.data.redis.*` and `resi-cache.redis.*` configure separate clients. -`spring.data.redis.*` configures the Spring Data Redis connection factory used -for ResiCache cache I/O. `resi-cache.redis.*` configures the Redisson deployment -used by distributed locking and synchronization; `resi-cache.redisson.*` -controls its pool, timeout, and retry settings. The `resi-cache.redis.*` defaults -are `localhost:6379` and database `0`, so omitted endpoint fields do not inherit -`spring.data.redis.*`. In single mode, an absent `resi-cache.redis.password` may -fall back to the Spring Data Redis password. For single, cluster, or sentinel -deployments, configure matching endpoints or topology explicitly in both -namespaces when using `sync=true`; do not rely on one namespace to configure the -other. - -ResiCache is discovered through Spring Boot auto-configuration via -`RedisCacheAutoConfiguration`. It does not add `@EnableCaching` for the -application; the application remains responsible for enabling Spring Cache. - -### 3. Enable Spring Cache +The Spring Data Redis namespace supplies cache I/O. The `resi-cache.redis.*` +namespace supplies the Redisson deployment used by distributed locking; the +namespaces are not implicitly copied into one another. The application remains +responsible for enabling Spring Cache: ```java +import io.github.davidhlp.spring.cache.redis.annotation.RedisCacheable; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; + @SpringBootApplication @EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } -} -``` - -### 4. Use the protected annotation -```java -@Service -public class UserService { - - @RedisCacheable( - value = "users", - key = "#id", - cacheNullValues = true, - randomTtl = true, - variance = 0.2, - enableEarlyExpiration = true) - public User getUserById(Long id) { - return userRepository.findById(id); + @Bean + UserService userService() { + return new UserService(); } -} -``` - -The protection attributes are deliberately explicit. A plain `@Cacheable` -remains compatible with Spring Cache, but it does not gain ResiCache protection -in the default `nativeAnnotationMode=SELECTIVE` mode: -```java -@Cacheable(value = "users", key = "#id") -public User getUserById(Long id) { - // Spring's native cache path; use @RedisCacheable for protection. + public static class UserService { + @RedisCacheable(value = "users", key = "#id") + public String getUserById(Long id) { + return "user-" + id; + } + } } ``` -## How it works - -### Handler chain - -Protection is assembled as a responsibility chain. The order is defined by the -`HandlerOrder` enum and attached to handlers through `@HandlerPriority`: - -```text -┌─────────────────────────────────────────────────────────────┐ -│ CacheHandlerChain │ -├─────────────────────────────────────────────────────────────┤ -│ ① BloomFilter (100) ── Bloom filter, anti-penetration │ -│ ② SyncLock (200) ── Distributed lock, anti-breakdown│ -│ ③ EarlyExpiration (250) ── Early expiry, hot-key guard │ -│ ④ TTL (300) ── TTL jitter, anti-avalanche │ -│ ⑤ NullValue (400) ── Null caching, anti-penetration │ -│ ⑥ ActualCache (500) ── Actual Redis write │ -└─────────────────────────────────────────────────────────────┘ -``` - -Each handler returns a typed `HandlerResult` with explicit `FlowControl` -decisions (`CONTINUE`, `SKIP_ALL`, or `TERMINATE`). A handler can opt into -post-processing through `requiresPostProcess` and -`afterChainExecution`; the chain engine owns progression and keeps the main -path separate from isolated post-processing failures. - -Built-in handlers are registered by the library's auto-configuration, which -scans the library-internal runtime package. A host application's custom handler -must be discovered by the host application as an `@Component` or `@Bean`; the -library's internal scan does not scan host packages. - -### Protection boundaries - -- **Cache penetration**: the Bloom filter treats a missing membership bit as a - definite miss. The default implementation is populated after successful cache - writes; it does not scan or rebuild from the data source. Before enabling it - for a cache whose keys are not already in the filter, seed or maintain the - public `BloomIFilter` seam. -- **Cache breakdown**: the sync handler coordinates concurrent loads through a - distributed lock when Redisson or another `LockManager` is available. -- **Cache avalanche**: TTL jitter spreads expiration times when `randomTtl=true`. -- **Hot-key expiry**: early expiration schedules refresh work before the normal - expiry boundary when explicitly enabled. -- **Negative lookups**: null-value caching can retain an absent result when the - annotation or configuration enables it. - -## Configuration - -Most settings use the `resi-cache.*` prefix and bind to -`RedisProCacheProperties`. Configuration is resolved during startup; changing -protection switches requires an application restart. - -### Enablement and protection switches - -```yaml -resi-cache: - enabled: true - native-annotation-mode: SELECTIVE # FULL | NONE | SELECTIVE - protection: - enabled: true - bloom-filter-enabled: null # null inherits protection.enabled - sync-lock-enabled: null - early-expiration-enabled: null - null-value-enabled: null -``` - -`resi-cache.enabled=false` disables the library auto-configuration. The -`protection.enabled` switch disables Bloom, sync-lock, early-expiration, and -null-value protection while keeping the base TTL and actual-cache handlers. -Per-mechanism `true` cannot re-enable a mechanism when the total protection -switch is `false`. - -`native-annotation-mode` controls Spring's native cache annotations: - -- `SELECTIVE` (default): pure-native methods with no ResiCache annotation remain - on Spring's native path and are not converted by the ResiCache operation - source. When a ResiCache annotation is present, a matching native operation - is skipped by ResiCache's converter (internal conversion de-duplication). - An explicitly mixed method can still be seen by both the ResiCache advisor - and Spring's native cache advisor; do not mix annotations on one method - without testing the result. -- `FULL`: converts all supported Spring cache annotations, including plain - native-only methods. When `@EnableCaching` or another Spring native advisor - is active, those methods can also be handled by both advisors; enable `FULL` - only when that duplicate interception or execution is intentional and tested. -- `NONE`: ignores native Spring cache annotations in the ResiCache operation - source. - -### Global settings - -```yaml -resi-cache: - default-ttl: 30m - key-prefix: "" - transaction-aware: false -``` - -### Metrics and health - -Metrics are opt-in and require both the property and an available -`MeterRegistry`: - -```yaml -resi-cache: - metrics: - enabled: true -``` - -The Redis cache health indicator uses the same explicit property and requires -Spring Boot Actuator. - -### Bloom filter - -```yaml -resi-cache: - bloom: - prefix: "bf:" - bit-size: 8388608 - hash-functions: 3 - hash-cache-size: 10000 -``` -These settings are bound under the `resi-cache.bloom.*` prefix. - -### Distributed lock - -```yaml -resi-cache: - sync-lock: - timeout: 3000 - unit: MILLISECONDS - prefix: "cache:lock:" - local-only: false -``` - -`sync=true` requires a distributed lock implementation such as Redisson. With -`local-only=false` (the default), missing distributed support fails closed -instead of silently claiming multi-instance protection. Set `local-only=true` -only when single-JVM degradation is an explicit, acceptable choice. +The cacheable method is intentionally self-contained so the snippet can be +copied into a small Boot application without inventing a repository type. -### Early expiration +`sync=true` fails closed if no distributed lock is available unless the +application explicitly opts into `resi-cache.sync-lock.local-only=true` for a +single-JVM deployment. Configure a serializer allowlist for application value +packages before reading custom cached types. -```yaml -resi-cache: - protection: - early-expiration-enabled: true - early-expiration: - pool-size: 2 - max-pool-size: 10 - queue-capacity: 100 -``` - -### Redis deployment - -```yaml -resi-cache: - redis: - mode: single # single | cluster | sentinel - host: localhost - port: 6379 - database: 0 - tls-enabled: false - # cluster-nodes: [host1:6379, host2:6379] - # sentinel-master: mymaster - # sentinel-nodes: [host1:26379] -``` - -The deployment validator checks mode-specific fields during configuration -binding. `resi-cache.redis.*` is the Redisson deployment path; `spring.data.redis.*` -configures the separate connection factory used for cache I/O. The -`resi-cache.redis.host`, `port`, and `database` defaults are `localhost`, `6379`, -and `0`, so omitted endpoint fields do not adopt Spring Data Redis values. In -single mode, an absent `resi-cache.redis.password` may fall back to the Spring -Data Redis password. For single, cluster, or sentinel deployments, explicitly -configure matching endpoints or topology in both namespaces when using -`sync=true`; do not rely on one namespace to configure the other. The -`resi-cache.redisson.*` namespace controls Redisson pool, timeout, and retry -settings. Credentials, when needed, belong in the application's secret -management system rather than in a committed README snippet. - -### Serialization safety - -```yaml -resi-cache: - serializer: - type-property: "@class" - polymorphic-typing-enabled: false - fail-on-unknown-type: true - allowed-package-prefixes: - - "io.github.davidhlp" - - "com.example.*" -``` - -The default whitelist is the literal prefix `io.github.davidhlp`, so -applications should add their own packages explicitly. For dot-boundary subtree -matching, use a prefix ending in `.*`; for example, `com.example.*` matches -`com.example.User` and `com.example.orders.Order`, but not `com.exampleX.User`. - -### Per-cache overrides - -```yaml -resi-cache: - caches: - users: - ttl: 10m - cache-null-values: true - key-prefix: "users:" -``` - -### `@RedisCacheable` attributes - -| Attribute | Default | Purpose | -|---|---:|---| -| `ttl` | `60` | Cache TTL in seconds | -| `cacheNullValues` | `false` | Cache `null` results | -| `useBloomFilter` | `false` | Enable Bloom-filter protection | -| `expectedInsertions` | `100000` | Expected Bloom-filter insertions | -| `falseProbability` | `0.01` | Bloom false-positive target | -| `randomTtl` | `false` | Enable TTL jitter | -| `variance` | `0.2` | TTL jitter amplitude | -| `enableEarlyExpiration` | `false` | Enable hot-key early refresh | -| `earlyExpirationThreshold` | `0.3` | Remaining-TTL ratio that triggers refresh | -| `sync` / `syncTimeout` | `false` / `10` | Enable synchronized loading and its wait timeout | - -## Extension points - -### Annotation family - -The public annotation family mirrors Spring Cache operations: - -- `@RedisCacheable` for read-through caching and protection attributes. -- `@RedisCachePut` for explicit cache writes. -- `@RedisCacheEvict` for cache removal. -- `@RedisCaching` for grouping multiple ResiCache operations on one method or - type. - -`@RedisCaching` can expose operations at type level, but the protection-policy -fields inside its composed annotations are evaluated at method level. A -type-level declaration does not apply those fields to otherwise unannotated -methods; repeat the relevant `@RedisCacheable`, `@RedisCachePut`, or -`@RedisCacheEvict` at method level when the method needs that policy. - -### Custom handlers - -A custom handler implements the public `CacheHandler` contract and uses -`@HandlerPriority` with a `HandlerOrder` value. It must be registered in the -host application's component scan or supplied as an application bean. The -handler can return a typed `HandlerResult` and can opt into the post-processing -hooks without managing a linked-list successor itself. - -Only the documented public seams are intended for replacement. See -[STABILITY.md](STABILITY.md) before depending on a public type or changing -handler behavior. +## Where to go next -## Runtime semantics - -### Cache I/O failure behavior - -| Operation | Behavior | +| Question | Canonical page | |---|---| -| GET | Degrades to a cache miss and records the internal failure | -| PUT / PUT_IF_ABSENT / CLEAN | Throws a typed runtime failure retaining the original cause | -| REMOVE | Observable best-effort removal; does not throw for the removal failure | -| `get(key, loader)` with a successful loader | Returns the loader value even if write-back fails; write-back failure is logged without the raw key | -| `get(key, loader)` with a failed loader | Surfaces Spring's `Cache.ValueRetrievalException` | - -`@CacheEvict(allEntries=true)` / CLEAN is best-effort and non-atomic, using a -SCAN cursor and batched deletion. Bloom membership bits are a separately -maintained set, populated by successful writes by default; they are not -automatically rebuilt from the data source. Clearing cache entries does not -remove those bits. - -### Serialization migration - -ResiCache stores values in an internal `{version, payload}` envelope. It is not -wire-compatible with Spring's `GenericJackson2JsonRedisSerializer` or -`JdkSerializer`. Existing applications should use a bounded -shadow-read → dual-write → cutover migration instead of assuming an in-place -serializer swap is safe. See the migration guidance in -[COMPATIBILITY.md](COMPATIBILITY.md). - -## Comparison - -ResiCache is intentionally narrower than general-purpose caching frameworks: - -| Capability | JetCache | Caffeine | Raw Redisson | **ResiCache** | -|---|:---:|:---:|:---:|:---:| -| Multi-level local + remote cache | Yes | Local only | — | — | -| Bloom filter | — | — | Manual | Yes | -| TTL jitter | — | — | Manual | Yes | -| Distributed breakdown lock | — | — | Manual | Yes | -| Null-value caching | — | — | Manual | Yes | -| Hot-key early refresh | — | — | Manual | Yes | -| Declarative protection chain | Partial | — | — | Yes | -| Broadcast invalidation | Yes | — | — | — | - -JetCache is a multi-level and broadcast-invalidation option. ResiCache is a -Redisson-oriented protection chain. They solve different parts of the caching -problem and are not presented as drop-in substitutes. - -## Limitations - -- **Pre-1.0 API**: minor releases may still change public contracts; review - [STABILITY.md](STABILITY.md) before pinning an extension point. -- **Protection is opt-in**: the five `@RedisCacheable` protection attributes - default to `false`. -- **Bloom membership is write-populated by default**: it does not scan existing - data, and an absent bit short-circuits the loader. Seed or maintain the public - `BloomIFilter` seam before enabling `useBloomFilter` for existing keys. -- **Current artifact gap**: the current Boot 4 / Java 21 line is not yet - published to Maven Central; `0.0.2` is the historical Boot 3 / Java 17 line. -- **Serializer migration required**: existing Spring-native serialized values - are not automatically compatible with ResiCache's envelope. -- **Reactive caching is unsupported**: WebFlux `Mono` and `Flux` methods do not - use the blocking ResiCache interceptor. -- **Async cached methods**: `@Async` methods are not supported for sync-lock and - Bloom-filter enhancements. -- **Protection switches are startup-only**: changing `resi-cache.protection.*` - requires an application restart. -- **Time-to-idle reads**: the native writer path intentionally does not refresh - TTL on read, avoiding write amplification. - -For the complete, tested boundary list, use -[COMPATIBILITY.md](COMPATIBILITY.md). - -## Not in scope - -ResiCache deliberately does not implement capabilities that are better owned by -specialized components: - -- **Circuit breaking and rate limiting** → [Resilience4j](https://resilience4j.readthedocs.io/) -- **Multi-level local plus remote caching** → [Caffeine](https://github.com/ben-manes/caffeine) for the local tier -- **Reactive caching** → not supported by the current blocking interceptor - -## Project layout - -```text -ResiCache/ -├── src/main/java/io/github/davidhlp/spring/cache/redis/ -│ ├── annotation/ # Public ResiCache annotations -│ ├── cache/ # Internal runtime, AOP, chain, operations, assembly -│ ├── chain/ # Stable handler, operation, and result contracts -│ ├── config/ # Auto-configuration and RedisProCacheProperties -│ ├── protection/ # Stable BloomIFilter and LockManager seams -│ └── serialization/ # Serializer and migration contracts -├── src/test/java/ # Unit, contract, and integration tests -├── resicache-bench/ # Standalone JMH benchmark module -├── scripts/ci/ # CI and repository guards -└── docs/adr/ # Accepted architecture decisions -``` - -## Development - -### Prerequisites - -- JDK 21 -- Maven 3.x, or the bundled Maven Wrapper -- Docker for Testcontainers-backed Redis and Cluster verification - -### Verification commands - -```bash -# No-Docker daily path -./mvnw -Punit test -B - -# Full Redis/Testcontainers verification -./mvnw clean verify -B - -# Separate style and naming gates -./mvnw checkstyle:check -B -bash scripts/ci/check-test-names.sh - -# Package without tests -./mvnw clean package -DskipTests -B -``` - -`./mvnw clean verify -B` enforces the JaCoCo gate of 70% line coverage and 40% -branch coverage. The full development and contribution workflow is documented -in [CONTRIBUTING.md](CONTRIBUTING.md). +| What is in scope, and what is not? | [`docs/PRODUCT.md`](docs/PRODUCT.md) | +| How are modules and handlers connected? | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | +| What are annotation, configuration, and error semantics? | [`docs/REFERENCE.md`](docs/REFERENCE.md) | +| How do I build, test, or debug? | [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | +| How do I operate, migrate, or release? | [`docs/OPERATIONS.md`](docs/OPERATIONS.md) | +| Which public types are stable? | [`STABILITY.md`](STABILITY.md) | +| Which runtime versions are supported? | [`COMPATIBILITY.md`](COMPATIBILITY.md) | +| What changed? | [`CHANGELOG.md`](CHANGELOG.md) | +| How do I contribute? | [`CONTRIBUTING.md`](CONTRIBUTING.md) | +| How do I report a vulnerability? | [`SECURITY.md`](SECURITY.md) | + +## Deliberate non-goals + +ResiCache does not implement circuit breaking, rate limiting, a multi-level +local-plus-remote cache, reactive caching, a hosted cache service, or a +production backup/deployment controller. Use the appropriate platform or +specialized component for those responsibilities. ## Project status and support -- **Version**: `v0.0.2`; semantic-versioning guarantees are intentionally - limited before 1.0. -- **Maintenance**: solo-maintained, non-SLA, best-effort support. -- **Change history**: [CHANGELOG.md](CHANGELOG.md) -- **Compatibility policy**: [COMPATIBILITY.md](COMPATIBILITY.md) -- **API stability**: [STABILITY.md](STABILITY.md) -- **Performance baseline**: [PERFORMANCE.md](PERFORMANCE.md) -- **Architecture decisions**: [ADR index](docs/adr/README.md) -- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md) - -## Security - -Do not open public issues for suspected vulnerabilities. Follow the private -reporting process in [SECURITY.md](SECURITY.md). - -## License +- **Version:** `0.0.2`; pre-1.0 compatibility is limited to the documented + stability contract. +- **Maintenance:** solo-maintained, non-SLA, best-effort support. +- **History:** [`CHANGELOG.md`](CHANGELOG.md). +- **Benchmarks:** historical, non-SLO evidence in [`PERFORMANCE.md`](PERFORMANCE.md). +- **License:** [`LICENSE`](LICENSE). -[MIT License](LICENSE) © 2026 DavidHLP +The repository's exact configuration model is defined by +`RedisProCacheProperties` and its generated metadata, not by a manually copied +configuration table in this README. diff --git a/README.zh-CN.md b/README.zh-CN.md index aee80432..acdc7d37 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,102 +1,51 @@ # ResiCache -**面向 Redis 的 Spring Cache 防护增强。** ResiCache 通过可编排的责任链, -为 Spring Cache 增加缓存穿透、缓存击穿、缓存雪崩和热点 key 提前刷新防护, -同时保留 Spring Cache 作为应用侧的使用模型。 +**面向 Redis 的 Spring Cache 防护增强。** ResiCache 在保留 Spring Cache +作为应用侧模型的前提下,提供缓存穿透、缓存击穿、缓存雪崩和热点 key 提前 +刷新防护。 -[![CI](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml/badge.svg)](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) - -[English](README.md) · [简体中文](README.zh-CN.md) +[English](README.md) · [简体中文](README.zh-CN.md) · +[文档地图](docs/README.md) > [!WARNING] -> ResiCache 仍处于 1.0 之前(`v0.0.2`),不提供 SLA,目前由单人维护。 -> 当前 `main` 构建线目标为 Spring Boot 4.0 与 Java 21,但尚未发布匹配的 -> Maven Central 产物。正式采用前请先阅读[兼容性矩阵](COMPATIBILITY.md)和 -> [已知限制](#已知限制)。 - -## 目录 - -- [概览](#概览) -- [功能特性](#功能特性) -- [兼容性与要求](#兼容性与要求) -- [快速开始](#快速开始) -- [工作原理](#工作原理) -- [配置](#配置) -- [扩展点](#扩展点) -- [运行时语义](#运行时语义) -- [方案对比](#方案对比) -- [已知限制](#已知限制) -- [不在范围内](#不在范围内) -- [项目结构](#项目结构) -- [开发与验证](#开发与验证) -- [项目状态与支持](#项目状态与支持) -- [安全](#安全) -- [许可证](#许可证) - -## 概览 - -Spring Cache 提供了统一的缓存编程模型,但本身不负责缓存穿透、缓存击穿、 -缓存雪崩或热点 key 过期防护。ResiCache 在 Spring Cache 与 Redis 之上, -将这些防护能力显式化、可组合化。 +> ResiCache 仍处于 1.0 之前(`v0.0.2`),不提供 SLA,目前由单人维护。当前源码 +> 构建线目标为 Spring Boot 4.0 与 Java 21,但尚未发布匹配的 Maven Central +> 产物。正式采用前请先阅读[兼容性矩阵](COMPATIBILITY.md)。 -ResiCache 的设计目标是: +## 能力概览 -- **保持 Spring Cache 作为边界**:应用继续使用 `@EnableCaching`、缓存操作 - 和 Spring 的缓存抽象。 -- **让防护可组合**:每种机制都是一条有明确顺序的类型化 Handler,而不是 - 分散在业务服务中的独立 AOP 逻辑。 -- **关键边界默认失败关闭**:缺少分布式锁支持、不安全的序列化和非法配置都 - 应当可观测,而不是静默地降低保证等级。 -- **保持职责聚焦**:ResiCache 不是多级缓存、熔断器、限流器或 Reactive 缓存 - 框架的替代品。 - -## 功能特性 - -| 能力 | 说明 | +| 能力 | 作用 | |---|---| -| 布隆过滤器 | 拦截已知不存在的 key,降低缓存穿透 | -| 分布式锁 | 基于 Redisson,为缓存击穿提供并发加载协调 | -| TTL 抖动 | 随机化过期时间,减少缓存雪崩 | -| 空值缓存 | 显式开启后缓存负查询结果 | -| 提前过期 | 在正常过期边界前刷新热点 key | -| 可编排责任链 | 通过 `HandlerOrder` 与 `@HandlerPriority` 统一排列防护机制 | -| 安全序列化 | 使用受控的反序列化白名单和内部 wire envelope | -| Spring Cache 集成 | 复用 Spring Cache,而不是引入第二套 AOP 模型 | +| 布隆过滤器 | 避免为已知不存在的 key 执行加载 | +| 分布式锁 | 使用 Redisson 或其他 `LockManager` 协调并发加载 | +| TTL 抖动 | 分散过期边界 | +| 空值缓存 | 显式开启后保留负查询结果 | +| 提前过期 | 在正常过期前刷新热点 key | +| 责任链 | 通过 `HandlerOrder` 统一排列防护机制 | +| 安全序列化 | 使用白名单保护的 `{version, payload}` envelope | +| Spring Cache 集成 | 复用 Spring Cache 与 `@EnableCaching` 边界 | -`@RedisCacheable` 上的五类防护属性默认全部为 `false`;请根据应用的风险 -模型显式开启需要的机制。 +防护能力默认要求显式开启。默认 `native-annotation-mode` 为 `SELECTIVE`, +`@RedisCacheable` 的五类防护属性默认关闭。产品范围与非目标见 +[`docs/PRODUCT.md`](docs/PRODUCT.md)。 -## 兼容性与要求 +## 兼容性 当前仓库只维护一条构建线: -| 组件 | 当前 `main` 构建线 | -|---|---:| -| Java | 21 | -| Spring Boot | 4.0.0 | -| Spring Framework / Spring Cache | 7.x,由 Boot 提供 | -| Spring Data Redis | 4.0.x | -| Redis Server | 7.x | -| Redisson | 3.50.0,可选 | -| Caffeine | 3.1.8,供内部能力使用 | -| Docker | Testcontainers 验证必需 | -| Testcontainers | 1.20.6,仅用于测试 | - -完整版本矩阵和边界说明见 [COMPATIBILITY.md](COMPATIBILITY.md)。 - -### 已发布产物状态 +- Spring Boot 4.0.0 / Spring Framework 7 / Spring Data Redis 4.0.x +- Java 21 +- Redis 7.x +- 需要分布式同步时使用 Redisson 3.50.0 +- Caffeine 3.1.8(内部支持) -当前 Boot 4 / Java 21 构建线以源码为主,尚未发布到 Maven Central。 -`io.github.davidhlp:ResiCache:0.0.2` 是历史上的 Boot 3 / Java 17 产物, -不要把它当作当前 `main` 构建线的依赖。 - -如需试用当前构建线,请[从源码构建](#开发与验证),或使用明确声明兼容性 -构建线的正式 release。 +Boot 3.x 不是当前维护的兼容线。完整版本矩阵、序列化迁移边界、失败语义和 +已知限制见 [`COMPATIBILITY.md`](COMPATIBILITY.md)。 ## 快速开始 -### 1. 在本地构建并安装当前源码 +当前构建线以源码为主。可以把当前检出版本构建并安装到本地 Maven 仓库, +这不会发布产物: ```bash git clone https://github.com/davidhlp/ResiCache.git @@ -105,25 +54,10 @@ cd ResiCache ./mvnw install -DskipTests -B ``` -第一条命令是不依赖 Docker 的贡献者检查;第二条命令会把当前源码按 POM -版本安装到本地 Maven 仓库,供本地消费者试用,不会发布产物。不要把这个本地 -构建与 Maven Central 上历史版本的 `0.0.2` 混淆。 - -对于本地消费者应用,将当前检出版本的坐标加入该应用的 `pom.xml`: - -```xml - - io.github.davidhlp - ResiCache - 0.0.2 - -``` - -该依赖从本地 Maven 仓库解析。下面的配置和应用示例都应在这个消费者应用 -中执行,而不是在 ResiCache 源码检出目录中执行。完整的 Redis 与 Redis -Cluster 验证命令见[开发与验证](#开发与验证)。 +消费者应用使用当前检出目录根 `pom.xml` 中的坐标和版本。不要把 Maven +Central 上历史的 `0.0.2` 产物当作当前 Boot 4 构建线。 -### 2. 配置 Redis +使用同步能力时,显式配置两个 Redis 命名空间: ```yaml spring: @@ -131,431 +65,78 @@ spring: redis: host: localhost port: 6379 - # timeout: 2s resi-cache: redis: mode: single host: localhost port: 6379 database: 0 - tls-enabled: false ``` -`spring.data.redis.*` 与 `resi-cache.redis.*` 配置的是相互独立的客户端。 -`spring.data.redis.*` 配置 ResiCache 缓存 I/O 使用的 Spring Data Redis -连接工厂。`resi-cache.redis.*` 配置分布式锁和同步能力使用的 Redisson -部署;`resi-cache.redisson.*` 控制其连接池、超时和重试设置。`resi-cache.redis.*` -的默认 host、port、database 分别为 `localhost`、`6379`、`0`,因此省略 -端点字段不会继承 `spring.data.redis.*`。在 single 模式下,如果 -`resi-cache.redis.password` 未设置,Redisson 可能回退使用 Spring Data Redis -密码。使用 `sync=true` 时,对于 single、cluster 或 sentinel 部署,都必须在 -两个命名空间中显式配置匹配的端点或拓扑;不要依赖一个命名空间配置另一个。 - -ResiCache 通过 Spring Boot 自动配置入口 `RedisCacheAutoConfiguration` 被发现。 -它不会替应用添加 `@EnableCaching`;是否启用 Spring Cache 仍由应用负责。 - -### 3. 启用 Spring Cache +Spring Data Redis 命名空间提供缓存 I/O;`resi-cache.redis.*` 提供分布式锁 +使用的 Redisson 部署,两个命名空间不会自动互相复制。是否启用 Spring Cache +仍由应用负责: ```java +import io.github.davidhlp.spring.cache.redis.annotation.RedisCacheable; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; + @SpringBootApplication @EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } -} -``` - -### 4. 使用防护注解 -```java -@Service -public class UserService { - - @RedisCacheable( - value = "users", - key = "#id", - cacheNullValues = true, - randomTtl = true, - variance = 0.2, - enableEarlyExpiration = true) - public User getUserById(Long id) { - return userRepository.findById(id); + @Bean + UserService userService() { + return new UserService(); } -} -``` - -防护属性刻意要求显式开启。普通 `@Cacheable` 仍可与 Spring Cache 共存, -但在默认的 `nativeAnnotationMode=SELECTIVE` 模式下不会获得 ResiCache 防护: -```java -@Cacheable(value = "users", key = "#id") -public User getUserById(Long id) { - // 走 Spring 原生缓存路径;需要防护时请使用 @RedisCacheable。 + public static class UserService { + @RedisCacheable(value = "users", key = "#id") + public String getUserById(Long id) { + return "user-" + id; + } + } } ``` -## 工作原理 - -### Handler 责任链 - -防护能力以责任链方式组装。执行顺序由 `HandlerOrder` 枚举统一定义,并通过 -`@HandlerPriority` 绑定: - -```text -┌─────────────────────────────────────────────────────────────┐ -│ CacheHandlerChain │ -├─────────────────────────────────────────────────────────────┤ -│ ① BloomFilter (100) ── 布隆过滤器,防缓存穿透 │ -│ ② SyncLock (200) ── 分布式锁,防缓存击穿 │ -│ ③ EarlyExpiration (250) ── 提前过期,热点 key 保护 │ -│ ④ TTL (300) ── TTL 抖动,防缓存雪崩 │ -│ ⑤ NullValue (400) ── 空值缓存,防缓存穿透 │ -│ ⑥ ActualCache (500) ── 实际 Redis 写入 │ -└─────────────────────────────────────────────────────────────┘ -``` - -每个 Handler 返回带有明确 `FlowControl` 决策(`CONTINUE`、`SKIP_ALL` 或 -`TERMINATE`)的类型化 `HandlerResult`。Handler 可以通过 -`requiresPostProcess` 与 `afterChainExecution` 参与后置处理;链引擎负责 -推进主链,并隔离后置处理失败。 - -内置 Handler 由库的自动配置注册,只扫描库内部 runtime 包。应用自定义 -Handler 必须由应用自己的组件扫描发现,或作为应用 Bean 提供;库的内部扫描 -不会扫描宿主应用包。 - -### 防护边界 - -- **缓存穿透**:Bloom 缺少 membership bit 时会被视为确定 miss。默认实现只在缓存 - 写入成功后回填,不会扫描或从数据源自动重建。对于过滤器中尚未存在的缓存 key, - 启用前必须通过公共 `BloomIFilter` seam 预填充或维护 membership set。 -- **缓存击穿**:当 Redisson 或其他 `LockManager` 可用时,sync Handler 通过 - 分布式锁协调并发加载。 -- **缓存雪崩**:`randomTtl=true` 时,TTL 抖动会分散过期时间。 -- **热点 key 过期**:显式开启后,提前过期机制会在正常过期边界前安排刷新。 -- **负查询**:显式开启后,空值缓存可以保留不存在数据的结果。 - -## 配置 - -大多数配置使用 `resi-cache.*` 前缀,并绑定到 -`RedisProCacheProperties`。配置在启动阶段解析;修改防护开关需要重启应用。 - -### 启用与防护开关 - -```yaml -resi-cache: - enabled: true - native-annotation-mode: SELECTIVE # FULL | NONE | SELECTIVE - protection: - enabled: true - bloom-filter-enabled: null # null 继承 protection.enabled - sync-lock-enabled: null - early-expiration-enabled: null - null-value-enabled: null -``` - -`resi-cache.enabled=false` 会关闭库的自动配置。`protection.enabled` 会关闭 -布隆、同步锁、提前过期和空值防护,但保留基础 TTL 与实际缓存 Handler。 -当总防护开关为 `false` 时,机制级 `true` 不能重新开启对应机制。 - -`native-annotation-mode` 控制 Spring 原生缓存注解: - -- `SELECTIVE`(默认):没有 ResiCache 注解的纯原生方法保留在 Spring - 原生路径中,不会被 ResiCache operation source 转换。存在 ResiCache 注解 - 时,匹配的原生 operation 会被 ResiCache 转换器跳过(这是内部转换去重), - 但显式混用注解的方法仍可能同时被 ResiCache Advisor 和 Spring 原生缓存 - Advisor 看到;同一方法不要混用注解,除非已经验证实际结果。 -- `FULL`:转换所有受支持的 Spring 原生缓存注解,包括只有原生注解的方法。 - 当 `@EnableCaching` 或其他 Spring 原生 Advisor 生效时,这些方法也可能被 - 两个 Advisor 处理;只有在明确需要并测试了这种重复拦截或执行时才应启用。 -- `NONE`:在 ResiCache operation source 中忽略 Spring 原生缓存注解。 - -### 全局配置 - -```yaml -resi-cache: - default-ttl: 30m - key-prefix: "" - transaction-aware: false -``` - -### 指标与健康检查 - -指标默认选择性关闭,需要同时显式设置属性并提供可用的 `MeterRegistry`: - -```yaml -resi-cache: - metrics: - enabled: true -``` - -Redis cache health indicator 使用同一个显式属性,并且需要 Spring Boot Actuator。 - -### 布隆过滤器 - -```yaml -resi-cache: - bloom: - prefix: "bf:" - bit-size: 8388608 - hash-functions: 3 - hash-cache-size: 10000 -``` -这些配置绑定在 `resi-cache.bloom.*` 前缀下。 - -### 分布式锁 - -```yaml -resi-cache: - sync-lock: - timeout: 3000 - unit: MILLISECONDS - prefix: "cache:lock:" - local-only: false -``` - -`sync=true` 需要 Redisson 等分布式锁实现。当 `local-only=false`(默认)时, -缺少分布式支持会失败关闭,而不是静默声称具备多实例防护。只有在单 JVM 降级 -明确可接受时,才应设为 `local-only=true`。 - -### 提前过期 - -```yaml -resi-cache: - protection: - early-expiration-enabled: true - early-expiration: - pool-size: 2 - max-pool-size: 10 - queue-capacity: 100 -``` - -### Redis 部署方式 - -```yaml -resi-cache: - redis: - mode: single # single | cluster | sentinel - host: localhost - port: 6379 - database: 0 - tls-enabled: false - # cluster-nodes: [host1:6379, host2:6379] - # sentinel-master: mymaster - # sentinel-nodes: [host1:26379] -``` - -部署配置会在绑定阶段校验与模式相关的字段。`resi-cache.redis.*` 是 -Redisson 部署配置路径;`spring.data.redis.*` 配置缓存 I/O 使用的独立连接 -工厂。`resi-cache.redis.host`、`port`、`database` 的默认值分别为 -`localhost`、`6379`、`0`,因此省略端点字段不会采用 Spring Data Redis 的值。 -在 single 模式下,如果 `resi-cache.redis.password` 未设置,Redisson 可能 -回退使用 Spring Data Redis 密码。使用 `sync=true` 时,对于 single、cluster -或 sentinel 部署,都必须在两个命名空间中显式配置匹配的端点或拓扑;不要 -依赖一个命名空间配置另一个。`resi-cache.redisson.*` 控制 Redisson 连接池、 -超时和重试设置。生产环境凭据应交由应用的密钥管理系统处理,不要写入提交的 -README 示例。 - -### 序列化安全 - -```yaml -resi-cache: - serializer: - type-property: "@class" - polymorphic-typing-enabled: false - fail-on-unknown-type: true - allowed-package-prefixes: - - "io.github.davidhlp" - - "com.example.*" -``` - -默认白名单是字面前缀 `io.github.davidhlp`,应用自己的业务包必须显式加入。 -如需 dot 边界保护的子树匹配,请使用以 `.*` 结尾的前缀;例如 -`com.example.*` 会匹配 `com.example.User` 与 `com.example.orders.Order`,但不会 -匹配 `com.exampleX.User`。 - -### 按缓存覆盖配置 - -```yaml -resi-cache: - caches: - users: - ttl: 10m - cache-null-values: true - key-prefix: "users:" -``` - -### `@RedisCacheable` 属性 - -| 属性 | 默认值 | 作用 | -|---|---:|---| -| `ttl` | `60` | 缓存 TTL,单位为秒 | -| `cacheNullValues` | `false` | 是否缓存 `null` 结果 | -| `useBloomFilter` | `false` | 是否启用布隆过滤防护 | -| `expectedInsertions` | `100000` | 布隆过滤器预期插入量 | -| `falseProbability` | `0.01` | 布隆过滤器误判率目标 | -| `randomTtl` | `false` | 是否启用 TTL 抖动 | -| `variance` | `0.2` | TTL 抖动幅度 | -| `enableEarlyExpiration` | `false` | 是否启用热点 key 提前刷新 | -| `earlyExpirationThreshold` | `0.3` | 触发刷新的剩余 TTL 比例 | -| `sync` / `syncTimeout` | `false` / `10` | 是否启用同步加载及等待超时 | - -## 扩展点 - -### 注解族 - -公共注解族与 Spring Cache 操作相对应: - -- `@RedisCacheable`:读穿缓存以及防护属性。 -- `@RedisCachePut`:显式写入缓存。 -- `@RedisCacheEvict`:移除缓存。 -- `@RedisCaching`:在一个方法或类型上组合多个 ResiCache 操作。 - -`@RedisCaching` 可以在类型级别暴露操作,但其中组合注解的保护策略字段按 -方法级别求值。仅有类型级声明不会把这些字段应用到未添加方法级注解的方法; -如果某个方法需要该策略,请在方法级别重复相关的 `@RedisCacheable`、 -`@RedisCachePut` 或 `@RedisCacheEvict`。 - -### 自定义 Handler - -自定义 Handler 实现公共 `CacheHandler` 契约,并使用带有 `HandlerOrder` 值的 -`@HandlerPriority`。它必须被应用自己的组件扫描发现,或作为应用 Bean 提供。 -Handler 返回类型化 `HandlerResult`,也可以参与后置处理,而不需要自行维护一个 -指向下一个节点的链表。 - -只有文档明确声明的公共 seam 才适合替换。依赖公共类型或修改 Handler 行为前, -请先阅读 [STABILITY.md](STABILITY.md)。 +这个缓存方法是自包含的,可以直接复制到小型 Boot 应用中,不需要另造 +`User` 或 `userRepository` 类型。 -## 运行时语义 +如果没有分布式锁,`sync=true` 默认失败关闭;只有单 JVM 场景明确接受降级时, +才设置 `resi-cache.sync-lock.local-only=true`。读取自定义缓存类型前,先为 +业务包配置序列化白名单。 -### 缓存 I/O 失败行为 +## 按问题阅读 -| 操作 | 行为 | +| 问题 | 权威文档 | |---|---| -| GET | 降级为 cache miss,并记录内部失败 | -| PUT / PUT_IF_ABSENT / CLEAN | 抛出保留原始 cause 的类型化运行时异常 | -| REMOVE | 可观测的 best-effort 移除;移除失败不向上抛出 | -| `get(key, loader)` 且 loader 成功 | 即使写回失败也返回 loader 值;写回失败日志不包含原始 key | -| `get(key, loader)` 且 loader 失败 | 抛出 Spring 的 `Cache.ValueRetrievalException` | - -`@CacheEvict(allEntries=true)` / CLEAN 是 best-effort、非原子操作,使用 SCAN 游标 -和批量删除。Bloom bit 是独立维护的 membership set,默认由成功写入回填;不会从 -数据源自动重建。清理缓存条目不会删除这些 bit。 - -### 序列化迁移 - -ResiCache 将值存储在内部 `{version, payload}` envelope 中,与 Spring 的 -`GenericJackson2JsonRedisSerializer` 或 `JdkSerializer` 不兼容。已有应用不应 -假定直接切换序列化器是安全的,应采用有边界的 -shadow-read → dual-write → cutover 迁移流程。迁移指引见 -[COMPATIBILITY.md](COMPATIBILITY.md)。 - -## 方案对比 - -ResiCache 刻意比通用缓存框架更聚焦: - -| 能力 | JetCache | Caffeine | 裸 Redisson | **ResiCache** | -|---|:---:|:---:|:---:|:---:| -| 多级本地 + 远端缓存 | 有 | 仅本地 | — | — | -| 布隆过滤器 | — | — | 手写 | 有 | -| TTL 抖动 | — | — | 手写 | 有 | -| 分布式击穿锁 | — | — | 手写 | 有 | -| 空值缓存 | — | — | 手写 | 有 | -| 热点 key 提前刷新 | — | — | 手写 | 有 | -| 声明式防护责任链 | 部分 | — | — | 有 | -| 跨实例广播失效 | 有 | — | — | — | - -JetCache 侧重多级缓存和跨实例广播失效;ResiCache 侧重面向 Redisson 的防护 -责任链。两者解决缓存问题的不同部分,不是互相替代的同类方案。 - -## 已知限制 - -- **仍处于 1.0 之前**:小版本仍可能改变公共契约;依赖扩展点前请阅读 - [STABILITY.md](STABILITY.md)。 -- **防护默认关闭**:`@RedisCacheable` 的五类防护属性默认都是 `false`。 -- **Bloom membership 默认由写入回填**:不会扫描已有数据,缺少 bit 会短路 loader。 - 对已有 key 启用 `useBloomFilter` 前,请先通过公共 `BloomIFilter` seam 预填充或维护。 -- **当前构建线尚无产物**:Boot 4 / Java 21 构建线尚未发布到 Maven Central; - `0.0.2` 是历史上的 Boot 3 / Java 17 构建线。 -- **需要序列化迁移**:已有 Spring 原生序列化值不会自动兼容 ResiCache envelope。 -- **不支持 Reactive 缓存**:WebFlux 的 `Mono` 与 `Flux` 方法不会进入阻塞式 - ResiCache interceptor。 -- **异步缓存方法有限制**:`@Async` 方法不支持同步锁和布隆过滤增强。 -- **防护开关只在启动时解析**:修改 `resi-cache.protection.*` 后必须重启应用。 -- **Time-to-idle 读取**:底层 writer 读取路径刻意不在读时刷新 TTL,以避免额外 - 写放大。 - -完整、经过测试的边界列表请以 [COMPATIBILITY.md](COMPATIBILITY.md) 为准。 - -## 不在范围内 - -ResiCache 刻意不实现以下更适合由专业组件负责的能力: - -- **熔断与限流** → [Resilience4j](https://resilience4j.readthedocs.io/) -- **多级本地 + 远端缓存** → 使用 [Caffeine](https://github.com/ben-manes/caffeine) 作为本地层 -- **Reactive 缓存** → 当前阻塞式 interceptor 不支持 - -## 项目结构 - -```text -ResiCache/ -├── src/main/java/io/github/davidhlp/spring/cache/redis/ -│ ├── annotation/ # 公共 ResiCache 注解 -│ ├── cache/ # 内部 runtime、AOP、责任链、操作与装配 -│ ├── chain/ # 稳定的 Handler、operation、result 契约 -│ ├── config/ # 自动配置与 RedisProCacheProperties -│ ├── protection/ # 稳定的 BloomIFilter 与 LockManager seam -│ └── serialization/ # 序列化与迁移契约 -├── src/test/java/ # 单元、契约与集成测试 -├── resicache-bench/ # 独立 JMH 基准模块 -├── scripts/ci/ # CI 与仓库守卫脚本 -└── docs/adr/ # 已接受的架构决策 -``` - -## 开发与验证 - -### 前置条件 - -- JDK 21 -- Maven 3.x,或仓库自带的 Maven Wrapper -- Docker:运行 Testcontainers Redis 与 Cluster 验证时必需 - -### 验证命令 - -```bash -# 不依赖 Docker 的日常路径 -./mvnw -Punit test -B - -# 完整 Redis/Testcontainers 验证 -./mvnw clean verify -B - -# 独立的代码风格与测试命名检查 -./mvnw checkstyle:check -B -bash scripts/ci/check-test-names.sh - -# 跳过测试打包 -./mvnw clean package -DskipTests -B -``` - -`./mvnw clean verify -B` 强制 JaCoCo 覆盖率门槛:行覆盖率 70%,分支覆盖率 -40%。完整开发与贡献流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。 - -## 项目状态与支持 - -- **版本**:`v0.0.2`;1.0 之前只提供有限的语义化版本保证。 +| 产品范围与非目标 | [`docs/PRODUCT.md`](docs/PRODUCT.md) | +| 模块、责任链和扩展边界 | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | +| 注解、配置和错误语义 | [`docs/REFERENCE.md`](docs/REFERENCE.md) | +| 构建、测试与调试 | [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | +| 运行、迁移与发布 | [`docs/OPERATIONS.md`](docs/OPERATIONS.md) | +| 稳定公共面 | [`STABILITY.md`](STABILITY.md) | +| 支持版本与运行限制 | [`COMPATIBILITY.md`](COMPATIBILITY.md) | +| 变更历史 | [`CHANGELOG.md`](CHANGELOG.md) | +| 历史性能证据 | [`PERFORMANCE.md`](PERFORMANCE.md) | +| 贡献方式 | [`CONTRIBUTING.md`](CONTRIBUTING.md) | +| 漏洞报告 | [`SECURITY.md`](SECURITY.md) | + +## 明确不做什么 + +ResiCache 不实现熔断、限流、多级本地加远端缓存、Reactive 缓存、托管缓存 +服务或生产备份/部署控制器;这些职责交给专门组件或应用平台。 + +## 状态与支持 + +- **版本**:`0.0.2`;1.0 之前以已公布的稳定性契约为准。 - **维护**:单人维护、无 SLA、尽力支持。 -- **变更历史**:[CHANGELOG.md](CHANGELOG.md) -- **兼容性策略**:[COMPATIBILITY.md](COMPATIBILITY.md) -- **API 稳定性**:[STABILITY.md](STABILITY.md) -- **性能基线**:[PERFORMANCE.md](PERFORMANCE.md) -- **架构决策**:[ADR 索引](docs/adr/README.md) -- **贡献指南**:[CONTRIBUTING.md](CONTRIBUTING.md) - -## 安全 - -疑似漏洞不要直接创建公开 issue,请按照 [SECURITY.md](SECURITY.md) 中的私有 -报告流程提交。 - -## 许可证 - -[MIT License](LICENSE) © 2026 DavidHLP +- **历史**:[`CHANGELOG.md`](CHANGELOG.md)。 +- **基准**:[`PERFORMANCE.md`](PERFORMANCE.md) 中的历史、非 SLO 性能证据。 +- **许可证**:[`LICENSE`](LICENSE)。 diff --git a/SECURITY.md b/SECURITY.md index bc0fe0ea..e4628678 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -41,7 +41,7 @@ advisory (with credit, if desired) will follow once the report is confirmed. deserialization to `resi-cache.serializer.allowed-package-prefixes` (default: `io.github.davidhlp`). You **must** add your own package prefixes for custom cached types, otherwise deserialization throws. See - [README → Serialization](README.md#serialization-safety). + [configuration and serialization reference](docs/REFERENCE.md#serialization-and-compatibility). - **Redisson config file path** (`resi-cache.redis.redisson-config-path`) is read via `Config.fromYAML` and **must only come from trusted ops/deploy sources** (application.yml, diff --git a/STABILITY.md b/STABILITY.md index 45935040..a48fd80a 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -215,6 +215,6 @@ notes is published in `COMPATIBILITY.md`. - [`CHANGELOG.md`](./CHANGELOG.md) — per-version changelog including ⚠️ BREAKING markers. -> Accepted architecture decisions and their rationale live in the -> [`docs/adr/`](./docs/adr/README.md) index; Git history records ordinary -> implementation history and commit-level details. +> Current architecture ownership and design constraints live in +> [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md); Git history records +> ordinary implementation history and commit-level details. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..7516c36e --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,144 @@ +# Architecture + +This is the current implementation map for the Boot 4 / Java 21 source line. +The source tree, tests, public-surface allowlists, and retained contract +documents are the evidence sources; this document summarizes ownership and +constraints without replacing them. + +## System boundary + +```text +Host application + ├─ Spring Cache annotations / Cache API + ├─ application configuration + └─ optional handler or stable-seam beans + │ + ▼ +RedisCacheAutoConfiguration + ├─ binds RedisProCacheProperties + ├─ scans only the package-private cache runtime + └─ assembles the proxy, cache manager, writer, chain, serializer, and observers + │ + ├─ Spring Data Redis cache I/O + └─ optional Redisson coordination / Redis topology +``` + +`RedisCacheAutoConfiguration` is conditional on Redis classes and +`resi-cache.enabled`; it does not add `@EnableCaching`. The internal component +scan is deliberately limited to `io.github.davidhlp.spring.cache.redis.cache` +and excludes tests and operator-only/configuration seams listed in the source. +Host application packages are not scanned by the library. + +## Module ownership + +| Package | Responsibility | Contract status | +|---|---|---| +| `annotation/` | `@RedisCacheable`, `@RedisCachePut`, `@RedisCacheEvict`, `@RedisCaching` | Public annotation surface | +| `config/` | Auto-configuration, bound properties, deployment validation, enablement checks | Public entry/config surface | +| `chain/` and `chain/model/` | Handler, operation, result, flow, continuation, priority, and typed decision contracts | Documented SPI/value surface; see `STABILITY.md` | +| `chain/observer/` | Chain observer hooks and scope-token protocol | Documented observer SPI | +| `protection/` | `BloomIFilter`, `LockManager`, and early-expiration policy/value types | `BloomIFilter` and `LockManager` are stable replacement seams; `EarlyExpirationMode` is a public configuration/value enum, not an independent extension seam | +| `serialization/` and `serialization/migration/` | Envelope/serialization errors and operator migration surface | Public operator/migration contracts; implementation remains internal | +| `cache/` | AOP, operation assembly, chain engine/handlers, Redis writer/manager, refresh, metrics, and serializer wiring | Internal runtime module and non-default extension boundary; only declarations explicitly listed by `STABILITY.md` and the allowlist are supported | +| `src/test/` and `scripts/ci/` | Contract, unit, Redis/Testcontainers, packaged-consumer, and repository guard evidence | Verification, not runtime API | + +A public Java declaration is not automatically a supported extension point. The +compiled allowlist and `STABILITY.md` classify the public surface; internal +classes can move or disappear without becoming a compatibility promise. + +## Cache execution chain + +`HandlerOrder` is the single ordering source. The current sequence is: + +| Order | Handler | Responsibility | +|---:|---|---| +| 100 | `BloomFilterHandler` | membership gate / penetration protection | +| 200 | `SyncLockHandler` | distributed or explicit local-only synchronization | +| 250 | `EarlyExpirationHandler` | hot-key refresh decision and scheduling | +| 300 | `TtlHandler` | base TTL and jitter calculation | +| 400 | `NullValueHandler` | negative-result encoding | +| 500 | `ActualCacheHandler` | actual cache operation | + +`CacheHandlerChainFactory` discovers internal handlers, orders them through +`@HandlerPriority(HandlerOrder.X)`, applies startup protection switches, and +assembles observers. `ChainEngine` owns advancement, flow decisions, observer +hook ordering, and post-processing isolation. A custom handler must be supplied +by the host application's component scan or as an application bean. + +## Annotation and policy flow + +The annotation path is intentionally split into two views: + +1. `AnnotationParser` creates an immutable parsed snapshot. +2. `RedisCacheOperationSource` validates and adapts Spring-native operations. +3. The final snapshot is registered for policy lookup. +4. `CacheOperationResolver` resolves the policy namespace for the operation. +5. GET uses the read declaration; explicit write-only PUT uses the PUT + declaration; read-through write-back remains governed by the read side. + +The split is required by the Spring operation source and the chain-side policy +resolver. It is not permission to reintroduce per-invocation parsing or to +collapse the two operation representations without a new contract decision. +Class-level operation discovery and method-level policy application retain the +current documented behavior in `COMPATIBILITY.md`. + +## Data and failure ownership + +- Spring's cache abstraction remains the host-facing boundary. +- `RedisProCache` / `RedisProCacheWriter` own cache-manager and writer behavior; + `CacheResult` carries typed operation outcomes internally. +- `LoaderOrchestrator` owns the shared read → load → write-back protocol. A + successful loaded value is returned even when write-back fails. +- `CacheErrorHandler` owns count-once failure reporting for chain failures; the + failure metric uses finite operation/kind/strategy dimensions and diagnostics + omit raw keys at WARN/ERROR. +- `SecureJacksonRedisSerializer` owns whitelist-backed serialization and the + `{version, payload}` envelope. Refresh metadata required by policy/CAS is + persisted; process-local monotonic time is not. +- Redis is an acceleration layer, not the application source of truth. A + tolerated write-back failure can leave stale cache state and has no implicit + retry/backoff contract. + +## Supported seams and non-seams + +Supported seams include the documented annotations/configuration keys, the wire +format, `CacheHandler`/observer contracts, `BloomIFilter`, `LockManager`, and +related typed value surfaces listed in `STABILITY.md`. The full refresh +executor, internal policies, metadata resolver, serializers' implementation +classes, handler internals, and unlisted declarations in `cache/` are not +supported replacement contracts. + +New public types require a real production consumer, a concrete change point, +explicit failure/lifecycle semantics, and a second-adapter contract test. This +follows the documented stability and allowlist boundary; do not create a public +seam for a single implementation or a speculative future use. + +## Durable decisions + +The rules below summarize current source, tests, and retained contract +documents. Update the owning current-state document when a behavior changes. + +| Current rule | Reason / boundary | Evidence | +|---|---|---| +| GET degrades to an observable miss; PUT/PIFA/CLEAN are typed fail-fast; REMOVE is observable best-effort | prevents writes being reported as success while preserving safe loader fallback | `REFERENCE.md`; cache operation tests | +| Read-through loader write-back is availability-first | a cache write failure must not discard a loaded business value | `PRODUCT.md`; loader tests | +| Only internal `cache/` is scanned | avoids implicit host-package registration and keeps implementation hidden | `RedisCacheAutoConfiguration`; auto-configuration tests | +| Protection switches resolve once and global-off wins | no runtime chain rebuild or hot-update contract exists | `CacheHandlerChainFactoryTest` | +| Bloom CLEAN keeps membership bits | membership is not current cache-entry state; stale bits are safe false-positives | `COMPATIBILITY.md`; Bloom tests | +| Reactive caching is unsupported | the interceptor is blocking and no compatible adopter/CI matrix exists | `COMPATIBILITY.md` | +| Native-image support is deferred | RuntimeHints/reflection inventory and native toolchain evidence are absent | task ledger | +| Public surface is allowlist-driven during 0.x | Java visibility alone would overstate compatibility | `STABILITY.md`; allowlist gate | + +## Verification anchors + +Use the current test and script names rather than copying their implementation: + +- `PublicSurfaceContractTest` and `src/test/resources/allowlist/` for the + packaged public boundary; +- `CacheHandlerChainFactoryTest` for switch precedence and chain assembly; +- `RedisCacheSemanticsIntegrationTest` and the other + `*IntegrationTest.java` classes for real Redis behavior; +- `RedisClusterSlotIntegrationTest` for cluster lock/data slot co-location; +- `bash scripts/ci/check-test-names.sh` for container-test naming; +- `bash scripts/ci/check-external-consumer.sh` for a packaged-JAR consumer; +- `bash scripts/ci/check-docs-contracts.sh` for documentation/source guards. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 00000000..a6a1f1d5 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,85 @@ +# Development + +This document is the contributor and maintainer execution guide. Exact +versions remain in the build files and CI configuration; this page explains +which command proves which boundary. + +## Environment + +- JDK 21, matching `pom.xml` and the Maven Enforcer range. +- Maven 3.x or the bundled `./mvnw` wrapper. +- Docker for Redis/Testcontainers integration and cluster tests. +- A working local Maven cache; no committed credentials are required. + +The root build is the core library. `resicache-bench/` is a separate JMH module +with its own POM and is not a release-compatibility proof for the core library. + +## Command matrix + +Run the smallest command that covers the change, then the broader gate required +by the change type. + +| Command | Evidence | +|---|---| +| `./mvnw -Punit test -B` | no-Docker unit path; excludes `**/*IntegrationTest*.java` | +| `./mvnw clean verify -B` | full core build, Redis/Testcontainers tests, Javadoc attachment, and JaCoCo gate | +| `./mvnw checkstyle:check -B` | explicit Checkstyle gate; it is separate from `verify` | +| `bash scripts/ci/check-test-names.sh` | all executable container tests use `*IntegrationTest.java` | +| `bash scripts/ci/check-docs-contracts.sh` | stale contract strings, removed Javadoc references, and required docs guards | +| `bash scripts/ci/check-external-consumer.sh` | packaged-JAR public contract; no Spring context or Redis claim | +| `./mvnw clean package -DskipTests -B` | packaged artifact without test execution | +| `./mvnw javadoc:javadoc -B` | Javadoc source consistency when public API docs change | + +`verify` enforces at least 70% line and 40% branch coverage. The no-Docker +profile does not establish Redis, Redis Cluster, or Testcontainers behavior. +Use a real Docker environment for those boundaries and report infrastructure +failures separately from test failures. + +## Test layers + +- **Unit tests** cover parsers, properties, chain decisions, serialization + rules, and public value contracts without Redis. +- **Redis integration tests** use `AbstractRedisIntegrationTest` and + Testcontainers with Redis 7-based fixtures. +- **Redis Cluster tests** use the separate + `AbstractRedisClusterIntegrationTest` fixture and prove topology-sensitive + behavior such as lock/data key slot co-location. +- Classes containing container markers must use the `*IntegrationTest.java` + suffix, with only the explicit helper allowlist exempted by the naming script. +- **Public-surface tests** compare the compiled package against the allowlists. +- **External-consumer tests** compile against the packaged JAR and declared + compile dependencies only. They prove importability and value-path protocol, + not a real Redis deployment. + +Tests mirror the source package under `src/test/java`. Integration fixtures and +application test resources are part of the test contract; do not change a test +profile to make a local environment appear green. + +## Source and style pointers + +The module map and dependency direction are in +[`ARCHITECTURE.md`](ARCHITECTURE.md). The public stability rules are in +[`STABILITY.md`](../STABILITY.md). Follow Java naming, existing Lombok usage, +focused classes, and public Javadoc conventions. Put design rationale in +Javadoc only when it explains a non-obvious constraint; keep current behavior +in the canonical docs instead of duplicating it in comments and guides. + +## Documentation changes + +Update the owner in [`docs/README.md`](README.md) when a current fact changes. +Keep README's quick start runnable and move detailed semantics to the relevant +core page. Update `STABILITY.md`, `COMPATIBILITY.md`, or `CHANGELOG.md` when +the change actually affects that document's contract or history. + +For a documentation-only change, run the link/reference checks and the docs +contract script; a Java build is not required unless source or generated API +behavior also changed. For code or build changes, use the command matrix and +follow [`CONTRIBUTING.md`](../CONTRIBUTING.md)'s PR checklist. + +## CI shape + +Pushes to `main`/`master` and pull requests run lint, docs consistency, quality, +core build, benchmark, and packaging jobs. The docs job is a required input to +the build job. Product packaging is conditional in the PR pipeline, but the +core build and docs gates still run for documentation changes. Release tags +use the separate release workflow described in [`OPERATIONS.md`](OPERATIONS.md). diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 00000000..428920e5 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,103 @@ +# Operations + +ResiCache is a library, not a hosted service. This document covers the +operator-controlled runtime and release boundaries that are present in the +repository. It does not invent a deployment platform, backup promise, SLA, or +on-call policy. + +## Runtime activation + +1. The host application enables Spring Cache with `@EnableCaching`. +2. Spring Boot loads `RedisCacheAutoConfiguration` when Redis classes are + present and `resi-cache.enabled` is not false. +3. `RedisProCacheProperties` binds and validates `resi-cache.*`. +4. The chain and its infrastructure are assembled once; protection-switch + changes require an application restart. + +The library does not enable Spring Cache for the host and does not scan host +application packages. The application's deployment mechanism owns process +lifecycle, secrets injection, and Redis availability. + +## Redis topology and configuration + +`spring.data.redis.*` configures the Spring Data Redis connection used for cache +I/O. `resi-cache.redis.*` configures the Redisson deployment used for locks and +synchronization; `resi-cache.redisson.*` controls its pool, timeout, and retry +settings. Configure matching topology explicitly in both namespaces when +`sync=true` is used. + +Supported deployment modes are `single`, `cluster`, and `sentinel`, with +binding-time validation for mode-specific fields and TLS requirements. The +advanced `resi-cache.redis.redisson-config-path` value is a trusted operator +input only: it is read as a local YAML path and must never come from an +end-user request. + +Redisson is optional until an operation requests distributed synchronization. +With no distributed `LockManager`, `sync=true` fails closed by default. +`resi-cache.sync-lock.local-only=true` is an explicit single-JVM degradation +and must not be treated as multi-instance protection. + +## Observability and diagnosis + +Metrics and the Redis health indicator are opt-in. Metrics require both +`resi-cache.metrics.enabled=true` and the application's `MeterRegistry`; the +health indicator additionally requires the optional Actuator dependency and +the same metrics property to be enabled. Writer statistics and failure +reporting are bounded by the contracts in `STABILITY.md` and `COMPATIBILITY.md`; +pre-1.0 metric names and log wording are not a general compatibility promise. + +WARN/ERROR diagnostics omit raw cache keys. The source uses cache-name or a +short diagnostic fingerprint where available and keeps fuller detail at lower +log levels. Do not paste raw keys, credentials, Redis payloads, or full +production logs into repository documentation. + +When triaging an incident, first identify the operation and configuration +source, then check binding failures, lock availability, serializer allowlists, +and Redis topology. Use the operation table in [`REFERENCE.md`](REFERENCE.md) +and the focused tests named in [`DEVELOPMENT.md`](DEVELOPMENT.md); do not infer +success from a process that merely started. + +## Serialization rollout and rollback boundary + +The `{version, payload}` envelope is not wire-compatible with Spring's generic +JSON or JDK serializers. A safe adoption flow is: + +1. shadow-read through the new serializer; +2. dual-write the old and new representations for a bounded window; +3. confirm hit/error behavior and cut over; +4. retain a rollback path until the new representation is trusted. + +The migration CLI and properties are operator-directed surfaces. They are not +run automatically at application startup. A serializer change without this +workflow can make existing values unreadable; a cache flush is not the only +rollback strategy and is not required by the documented migration flow. + +## Release and publication boundary + +The root POM is versioned independently from the benchmark POM. A `vX.Y.Z` tag +enters the release workflow, which validates SemVer, runs lint/docs/build gates, +updates the POM version in the workflow workspace, deploys with repository +credentials, and creates a GitHub release. Release credentials are configured +out of band; contributors must not add secrets to `release.yml`. + +The current Boot 4 / Java 21 line is source-first and has no matching Maven +Central artifact in the repository's compatibility evidence. A local +`./mvnw install` is a consumer-development step, not a publication or release +claim. Same-line publication/signing and adopter evidence remain deferred in +the local task ledger. + +## Backup, restore, and hosted-service limits + +The repository does not ship Redis backup/restore automation, a deployment +controller, a managed Redis service, or a production incident-response SLA. +Those responsibilities belong to the host application's platform and Redis +operator. Record and verify those external procedures in the deployment system +rather than adding a repository document that pretends they are implemented. + +## Security boundary + +Follow [`SECURITY.md`](../SECURITY.md) for private vulnerability reports. Keep +serializer allowlists, Redisson file paths, credentials, and deployment +configuration in trusted application/operator channels. The library's secure +serialization defaults and known constraints are part of the runtime contract; +changing them requires source, test, and compatibility review. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md new file mode 100644 index 00000000..4e74ea15 --- /dev/null +++ b/docs/PRODUCT.md @@ -0,0 +1,125 @@ +# Product + +This document describes the current product boundary of the checked-out +ResiCache source line. It is not a roadmap and does not promise behavior that +is only proposed or unverified. + +## Purpose + +ResiCache adds protection-in-depth to Spring Cache backed by Redis. It keeps +Spring Cache as the application-facing model and composes cache protection in a +typed responsibility chain rather than introducing a second application AOP +model. + +The product is intended for application developers who need explicit controls +for common Redis cache failure modes, and for operators who need those controls +to fail visibly instead of silently claiming guarantees they do not have. + +## Current capability set + +- **Bloom membership protection** for known data-source membership. +- **Synchronized loading** through a distributed `LockManager`, with an + explicit local-only fallback for single-JVM deployments. +- **TTL jitter** to spread expiration boundaries. +- **Null-value caching** when enabled. +- **Early expiration** for hot-key refresh. +- **Spring Cache annotations and cache operations** through the library's + auto-configuration. +- **Controlled serialization** using an internal `{version, payload}` envelope + and a deserialization package allowlist. +- **Optional metrics and health integration** when the relevant application + infrastructure is present. + +Protection attributes are opt-in. The current default annotation mode is +`SELECTIVE`, so a plain Spring `@Cacheable` method does not automatically gain +ResiCache protection unless the application chooses another supported mode. +See [`REFERENCE.md`](REFERENCE.md) for semantic details. + +## Terms and roles + +- **Protection chain**: ordered handlers that evaluate a cache operation before + the actual Redis operation. +- **Membership bit**: Bloom-filter state describing possible data-source + membership, not current cache-entry presence. +- **Native annotation path**: Spring's cache annotations adapted according to + `native-annotation-mode`. +- **ResiCache annotation path**: `@RedisCacheable`, `@RedisCachePut`, + `@RedisCacheEvict`, and `@RedisCaching` declarations. +- **Application developer**: supplies Spring Cache usage, configuration, and + any custom handler or stable seam implementation. +- **Operator**: supplies Redis topology, trusted secrets/configuration, rollout + and migration control, and observability infrastructure. +- **Contributor**: changes the library, tests, documentation, or CI within the + repository contracts. + +## Main flows + +### Application startup + +The application enables Spring Cache. ResiCache's auto-configuration is then +loaded when the Redis classes are available and `resi-cache.enabled` is not +false. Configuration is validated during binding; the protection chain is +assembled once and configuration changes require a restart. + +### Protected cache operation + +A protected ResiCache operation, or a native Spring operation adapted by the +selected `native-annotation-mode`, is resolved into the library's operation +model. In `SELECTIVE` mode, an otherwise plain Spring cache annotation remains +on Spring's native path. The internal chain then evaluates Bloom, +synchronization, early expiration, TTL, null-value, and actual-cache behavior +in the order defined by `HandlerOrder`. The public extension contracts are +intentionally narrower than the internal implementation module. + +### Read-through loading + +A successful loader value remains available even when the cache write-back +fails. Loader failures remain observable as the documented Spring failure. This +availability-first behavior is not a general retry guarantee; the cache is a +derived acceleration layer. Exact operation semantics are in +[`REFERENCE.md`](REFERENCE.md), and the implementation boundary is in +[`ARCHITECTURE.md`](ARCHITECTURE.md). + +### Serializer migration + +The internal envelope is not wire-compatible with Spring's generic JSON or JDK +serializer. Existing applications must use a bounded shadow-read, +dual-write, and cutover process before relying on ResiCache values. The +migration tooling is operator-directed and is not run automatically at +application startup. + +## Product rules + +- The supported runtime is the blocking Spring Cache integration. Reactive + `Mono`/`Flux` caching is outside the current product boundary. +- Missing distributed-lock support with `sync=true` fails closed by default; + `local-only=true` is an explicit single-JVM degradation, not a cluster + guarantee. +- Bloom state is populated by successful writes by default; the library does + not rebuild it from the data source. A missing bit can short-circuit a load, + so existing keys require a seed/maintenance strategy before enabling Bloom. +- `protection.enabled=false` disables the four protection handlers but keeps + the TTL handler. Per-mechanism values cannot re-enable protection after the + global switch is off. +- A configuration change to protection switches is startup-only and requires a + restart; no dynamic chain rebuild is supported. +- The current build line is pre-1.0, non-SLA, and source-first. Version and + runtime boundaries belong to [`COMPATIBILITY.md`](../COMPATIBILITY.md), not to + this summary. + +## Non-goals + +ResiCache does not replace: + +- circuit breaking or rate limiting (use a resilience component); +- a multi-level local-plus-remote cache (use a cache tiering component); +- a reactive cache interceptor; +- a hosted cache service, deployment controller, backup service, or guaranteed + support operation. + +## Approved but not implemented + +AOT/native-image certification remains deferred. Its entry conditions remain +in the local task ledger; this product document makes no native support claim. +Other open or deferred work is owned by the existing task ledger, not by a new +roadmap document. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f63f6a12 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,92 @@ +# Documentation Map + +This directory is the current-state documentation entry point. The English +files are authoritative for repository facts; `README.zh-CN.md` is the +translated quick-start companion and does not override the English contract. + +## Read by task + +| Need | Start here | Then verify with | +|---|---|---| +| Adopt the library or run the smallest example | [`README.md`](../README.md) | [`PRODUCT.md`](PRODUCT.md), [`REFERENCE.md`](REFERENCE.md) | +| Understand product scope and supported behavior | [`PRODUCT.md`](PRODUCT.md) | [`COMPATIBILITY.md`](../COMPATIBILITY.md) | +| Trace modules, ownership, or extension seams | [`ARCHITECTURE.md`](ARCHITECTURE.md) | current source and contract tests | +| Build, test, debug, or contribute code | [`DEVELOPMENT.md`](DEVELOPMENT.md) | `pom.xml`, `scripts/ci/`, CI workflows | +| Configure, migrate, release, or handle runtime incidents | [`OPERATIONS.md`](OPERATIONS.md) | `src/main/java/.../RedisProCacheProperties.java`, workflows | +| Look up API semantics, configuration meaning, or errors | [`REFERENCE.md`](REFERENCE.md) | source, generated metadata, tests | +| Check stable public surface | [`STABILITY.md`](../STABILITY.md) | public-surface allowlists and `PublicSurfaceContractTest` | +| Check supported versions and runtime limits | [`COMPATIBILITY.md`](../COMPATIBILITY.md) | `pom.xml` and integration tests | +| Review released or unreleased change history | [`CHANGELOG.md`](../CHANGELOG.md) | Git history and the linked contract source | +| Review historical benchmark evidence | [`PERFORMANCE.md`](../PERFORMANCE.md) | JMH module, recorded environment, and non-SLO caveat | +| Find current deferred work or blockers | `.agent/tasks/resicache-maturity.yaml` when present | current branch, HEAD, source, and tests | + +The task ledger is an ignored local status file, not a public contract. It may +be absent in a fresh clone; do not infer task state from its historical copy. + +## Governance roles + +| Role | Canonical source | What it owns | +|---|---|---| +| Constitution | `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `SECURITY.md` | rules, contribution boundaries, and policy | +| Map | this file | navigation and authority assignment | +| Status | `.agent/tasks/resicache-maturity.yaml` when present | active work, deferred work, and blockers | +| History | `CHANGELOG.md`, `PERFORMANCE.md` | durable changes and versioned evidence | + +The role names describe ownership, not an automatic loader. The active agent +harness must follow the reading order below; linked documents remain contextual +reference and are independently verified. + +## Authority by topic + +| Topic | Canonical source | Scope / version | Verification entry | +|---|---|---|---| +| Product goals, terms, non-goals, and capability boundary | [`PRODUCT.md`](PRODUCT.md) | Checked-out source line | README quick start and compatibility limits | +| Module boundaries, data flow, chain order, and architecture constraints | [`ARCHITECTURE.md`](ARCHITECTURE.md) | Current implementation | `src/main/java/io/github/davidhlp/spring/cache/redis/` and contract tests | +| Exact build coordinates and dependency versions | `pom.xml`, `resicache-bench/pom.xml`, Maven wrapper | The checked-out build | Maven model and CI setup action | +| Exact configuration keys and defaults | `RedisProCacheProperties.java`, generated configuration metadata | Current source binding model | configuration binding tests and `src/main/resources/META-INF/` | +| Exact public type surface | `src/test/resources/allowlist/`, `PublicSurfaceContractTest` | Current packaged JAR contract | `./mvnw test` and external-consumer gate | +| API stability promises | [`STABILITY.md`](../STABILITY.md) | 0.x caller-observable surface | allowlist, contract tests, CHANGELOG markers | +| Version compatibility and known runtime limits | [`COMPATIBILITY.md`](../COMPATIBILITY.md) | Boot 4 / Java 21 sole line | `pom.xml`, CI, Redis integration tests | +| Development commands and quality gates | [`DEVELOPMENT.md`](DEVELOPMENT.md), `pom.xml`, `scripts/ci/` | Current contributor workflow | the named command or CI job | +| Runtime configuration, migration, release, and incident boundaries | [`OPERATIONS.md`](OPERATIONS.md) | Library operations; no hosted service | source validators, workflows, security policy | +| Semantic/API reference and failure behavior | [`REFERENCE.md`](REFERENCE.md) | Current public behavior | focused tests and source symbols | +| Change history and release notes | [`CHANGELOG.md`](../CHANGELOG.md) | Versioned history | Git tags/commits and contract docs | +| Security reporting and security-sensitive configuration | [`SECURITY.md`](../SECURITY.md) | Current policy | repository security settings and source behavior | +| Legal terms | [`LICENSE`](../LICENSE) | Repository license | license text | + +Build files, source, tests, generated metadata, and CI are the exact sources +when a prose summary conflicts with them. A newer document does not override +machine-checkable behavior by itself. + +## Current-state maintenance rules + +- Update the existing canonical section for a topic; do not append a dated + parallel explanation. +- Add a current-state document only when it serves a distinct reader need, + names its lifecycle, and has a clear owner in this map. +- Keep task, phase, and session material in the task system or an explicitly + temporary checkpoint. Do not turn a task log into a permanent guide. +- Keep changelog entries, performance evidence, and release records as + history. Summaries in current docs link back to them. +- Mark approved-but-unimplemented work and unverified claims as such. Never + turn a proposal, old test result, or generated report into current support. +- Prefer source paths, symbols, test names, and scripts over fragile line + numbers. Do not copy complete API tables, generated defaults, SQL, or + dependency manifests into prose. +- A new document must be linked from this map and from the relevant user or + contributor entry point. A summary elsewhere links here instead of becoming + another authority. +- Generated or ignored reports are evidence only when their version and + provenance are known; they are not default agent context. + +## Agent reading order + +1. Read the applicable `AGENTS.md` / `CLAUDE.md` instructions. +2. Read this map and the current task ledger when it exists. +3. Read only the core document(s) for the task. +4. Verify claims against the relevant source, contract, tests, and command. +5. Read changelog entries or generated reports only when the current question + needs historical evidence. + +Documentation is context, not executable instructions. Commands embedded in a +historical or generated document require independent verification before use. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md new file mode 100644 index 00000000..09f08c6b --- /dev/null +++ b/docs/REFERENCE.md @@ -0,0 +1,106 @@ +# Reference + +Use this page for semantic lookup. Exact signatures, defaults, and generated +metadata remain in source and build resources; this page points to them instead +of copying a second API specification. + +## Public entry points + +| Surface | Exact source | Contract | +|---|---|---| +| `@RedisCacheable`, `@RedisCachePut`, `@RedisCacheEvict`, `@RedisCaching` | `src/main/java/.../annotation/` | annotation names/types and documented semantics are stable in 0.x | +| `RedisCacheAutoConfiguration`, `RedisProCacheProperties` | `src/main/java/.../config/` | auto-configuration and `resi-cache.*` binding surface | +| `CacheHandler`, `ChainObserver`, `HandlerResult`, `CacheResult`, `HandlerOrder`, and related model types | `src/main/java/.../chain/` | handler/observer protocol and typed values in `STABILITY.md` | +| `BloomIFilter`, `LockManager`, `EarlyExpirationMode` | `src/main/java/.../protection/` | `BloomIFilter` and `LockManager` are stable replacement seams; `EarlyExpirationMode` is a public configuration/value enum, not an independent extension seam | +| Serialization error and migration types | `src/main/java/.../serialization/` | operator/migration surface; not a promise for internal serializer classes | + +The exact compiled list is pinned by +`src/test/resources/allowlist/public-surface.txt` and +`public-surface-nested.txt`, enforced by `PublicSurfaceContractTest`. Java +visibility outside that list is not an API promise. + +## Configuration lookup + +All library properties bind under `resi-cache`. Read exact field types and +Java defaults from +`src/main/java/io/github/davidhlp/spring/cache/redis/config/RedisProCacheProperties.java`. +Read additional metadata from +`src/main/resources/META-INF/additional-spring-configuration-metadata.json`. +The main configuration groups are: + +- `default-ttl`, `key-prefix`, and `transaction-aware`; +- `native-annotation-mode` (`SELECTIVE` by default; also `FULL` and `NONE`); +- `protection.*` global/per-mechanism switches; +- `resi-cache.bloom.*`, `resi-cache.early-expiration.*`, + `resi-cache.sync-lock.*`, and `resi-cache.redisson.*`; +- `redis.*` topology/TLS/deployment fields; +- `serializer.*` and operator migration settings; +- per-cache overrides under `caches.*`; +- optional `disabled-handlers`, metrics, and feature controls defined by the + current source. + +Configuration is validated at binding time. Do not infer a default from an old +README snippet when the properties class or generated metadata differs. + +## Annotation and operation semantics + +- `@RedisCacheable` describes read-through caching and its protection policy. +- `@RedisCachePut` describes an explicit write and supplies policy for a + write-only declaration. +- `@RedisCacheEvict` describes removal/clear operations. +- `@RedisCaching` groups operations on a method or type. Type-level discovery + does not apply policy fields to otherwise unannotated methods; add the needed + method-level declaration when the policy matters. +- In `SELECTIVE` mode, a plain Spring cache annotation stays on the native + Spring path unless a ResiCache annotation causes the matching operation to be + adapted. `FULL` and `NONE` change that adaptation boundary; mixed advisors + require an explicit, tested choice. +- Protection switches are resolved at startup. A global protection-off keeps + TTL and disables Bloom, sync-lock, early-expiration, and null-value handlers; + a per-mechanism true cannot re-enable one after global-off. + +## Cache operation outcomes + +| Operation | Current behavior | +|---|---| +| GET | Redis failure degrades to a miss while retaining internal failure data and logging the failure | +| PUT | typed runtime failure with the original cause on cache-operation failure | +| PUT_IF_ABSENT | typed runtime failure on failure; it does not become an existing/null result | +| CLEAN / `allEntries=true` eviction | typed failure boundary, while SCAN plus batched deletion can be partial/non-atomic | +| REMOVE | observable best-effort removal; removal failure does not throw through the operation | +| read-through with successful loader | returns the loaded value even if write-back fails; the warning is redacted | +| read-through with failed loader | surfaces the Spring `Cache.ValueRetrievalException` path with the documented diagnostic handling | + +This table summarizes current source behavior and focused tests; update it when +the operation contract changes. + +## Serialization and compatibility + +ResiCache stores an internal `{version, payload}` envelope through +`SecureJacksonRedisSerializer`. It is not wire-compatible with +`GenericJackson2JsonRedisSerializer` or `JdkSerializer`. The whitelist default +and `.*` dot-boundary behavior are defined by `WhitelistPolicy` and the +serializer properties. Polymorphic typing is off by default. + +Use the bounded shadow-read → dual-write → cutover migration described in +[`COMPATIBILITY.md`](../COMPATIBILITY.md) and [`OPERATIONS.md`](OPERATIONS.md). +Do not claim that an in-place serializer swap, a cache flush, or a historical +Maven Central artifact proves compatibility with the current line. + +## Errors and diagnostics + +Binding validation reports concrete property paths. Missing distributed lock +support is a fail-closed runtime boundary unless local-only degradation is +explicit. WARN/ERROR output and typed failure messages omit raw keys; the current +key-privacy contract and its ownership are documented in [`ARCHITECTURE.md`](ARCHITECTURE.md). +failure metric is an internal bounded diagnostic, not a public per-key alerting +API. + +## Compatibility and stability references + +- [`STABILITY.md`](../STABILITY.md): stable 0.x caller-observable surfaces, + handler/observer protocol, nested public types, and 1.0 markers. +- [`COMPATIBILITY.md`](../COMPATIBILITY.md): supported Boot 4 / Java 21 line, + Redis/Redisson boundaries, serialization migration, and known limitations. +- [`CHANGELOG.md`](../CHANGELOG.md): versioned changes and breaking markers. +- [`ARCHITECTURE.md`](ARCHITECTURE.md): current ownership and design constraints. diff --git a/docs/adr/0001-interface-contract-closure.md b/docs/adr/0001-interface-contract-closure.md deleted file mode 100644 index 7dd9e52e..00000000 --- a/docs/adr/0001-interface-contract-closure.md +++ /dev/null @@ -1,553 +0,0 @@ -# ADR-0001: ResiCache interface contract closure - -**Date**: 2026-09-04 -**Status**: accepted -**Deciders**: ResiCache maintainers - -This ADR records the contract decisions that govern the current Boot 4 / Java -21 line. Verification status belongs to the repository's current CI and local -check results; it does not change the decisions below. -## 1. Cache read/write failure semantics - -**Context**: The chain returned failure results without causes, and the Writer -ignored failures for writes and cleanup. That made Redis errors look like -successful cache operations. - -**Decision**: GET returns a graceful miss while retaining failure status, kind, -and cause internally and logging the failure. PUT, PUT_IF_ABSENT, and CLEAN -fail fast through one typed internal `CacheOperationException` with the -original cause. REMOVE is observable best-effort and does not throw. - -**Alternatives**: Make every operation best-effort (rejected because writes -would be reported as success); add a configurable dual mode (rejected because -it doubles behavior and test surface). - -**Consequences**: Write callers can now receive an exception. GET preserves -safe loader fallback. REMOVE can leave stale data, but the failure is logged. - -**Known limitation**: CLEAN is SCAN plus batched UNLINK/DEL and can be partial. -**Re-evaluate**: after an adopter reports a consistency incident or requires a -new operation-specific policy. - -## 2. GET miss and write consistency - -**Context**: A miss and a Redis error both produced null-like results. - -**Decision**: `CacheResult` distinguishes `MISS`, `HIT`, `INSERTED`, `EXISTING`, -and `FAILURE`; PIFA failure never becomes an existing/null result. - -**Alternatives**: Infer state from nullable bytes (rejected because inserted, -existing-null, and failure collide). - -**Consequences**: Internal callers can audit state without changing Spring's -nullable byte API. - -**Known limitation**: Spring's `putIfAbsent` byte return still exposes null -for both inserted and an existing entry without bytes. **Re-evaluate** if the -underlying Spring contract exposes a richer result. - -## 3. Metadata context propagation - -**Context**: The old async wrapper captured metadata on the worker thread and -used a default-resolver `instanceof` check for restore. - -**Decision**: Capture on the submitting thread. Restore through the resolver -contract, use LIFO `ScopedActivation`, and restore the worker's prior MDC and -ThreadLocal state in `finally`. Custom resolvers use the same lifecycle. - -**Alternatives**: Keep a static default resolver hook (rejected because custom -resolvers cannot restore); clear all MDC blindly (rejected because unrelated -worker context would be lost). - -**Consequences**: retrieve/store preserve method metadata across worker reuse -without leaking state. - -**Known limitation**: Reactive context propagation is out of scope. -**Re-evaluate**: only with a real reactive adopter and a nonblocking design. - -## 4. Async retrieve/store - -**Context**: Spring Data Redis 4 supports async writer paths, but the cache -operation metadata must cross the common-pool boundary. - -**Decision**: The Writer captures `MethodSnapshot` and MDC before submitting -retrieve/store work. The resolver owns activation and cleanup; the Writer does -not know ThreadLocal implementation details. - -**Alternatives**: Disable async support (rejected because the existing writer -contract supports it); put propagation in each handler (rejected because it -duplicates lifecycle logic). - -**Consequences**: async failures complete their future exceptionally, and -worker cleanup is paired. - -**Known limitation**: The default path still uses the JVM common pool. -**Re-evaluate**: when an executor injection contract is required by a real -adopter. - -## 5. Public SPI admission - -**Context**: Java visibility exceeds the supported API promise, and one-method -callbacks were mistaken for extension points. - -**Decision**: Only documented annotations, configuration keys, wire format, -and behavior-tested deep SPIs are supported. A new public type requires a real -production implementation, a real consumer, a concrete change point, failure -and lifecycle semantics, and a second-adapter contract test. - -**Alternatives**: Freeze every public declaration (rejected because it freezes -implementation details); use Javadoc alone (rejected because it has no gate). - -**Consequences**: public implementation types remain unstable during 0.x. - -**Known limitation**: External usage cannot be proven from this repository. -**Re-evaluate**: before 1.0 and whenever an adopter supplies an implementation. - -## 6. Auto-configuration back-off - -**Context**: Root-package component scanning registered defaults and migration -components implicitly, while concrete injection defeated replacement. - -**Decision**: Remove root-package scanning. The public auto-configuration -scans only the package-private `cache` runtime module (excluding test classes), -while stable defaults retain typed `@ConditionalOnMissingBean` contracts. -`NullValueEncoder` is the shared handler dependency. Bloom's default -is one explicit local-plus-Redis composition replaced by one user `BloomIFilter` -bean. Redisson lock creation remains behind its class-level optional -configuration. - -**Alternatives**: Keep scanning (rejected because registration is implicit); -use bean names or `@Primary` as the user override mechanism (rejected because -names and ordering are not a stable contract). Internal infrastructure -qualifiers for the secure Redis template and internal executor are not user -override mechanisms; they prevent collisions with host-provided generic beans -while typed conditions govern supported replacements. - -**Consequences**: internal implementation beans are isolated behind one -package boundary; migration components remain operator-only and are not normal -application beans. - -**Known limitation**: The targeted ApplicationContextRunner and reflection -checks are green; a broader external-environment matrix remains a CI concern. - -## 7. Public API stability - -**Context**: The project is pre-1.0 and the current clone has no release tag. -Maven Central publishes `io.github.davidhlp:ResiCache` 0.0.1–0.0.5, 0.0.7, -and 3.2.4; all of them (verified 2026-09-05 from the published POMs) are -the earlier Spring Boot 3.2.4 / Java 17 / Redisson 3.17.6 line, not the -current Boot 4 / Java 21 line. A bounded public search the same date found -no external consumers of any line (one first-party usage example only; -private adopters unprovable). - -**Decision**: Keep the documented annotation/configuration/wire surfaces -stable and make implementation types package-private in the internal `cache` -module. The compiled public-surface allowlist is the machine gate; no release, -binary-compatibility, or old-consumer migration gate is enabled. - -**Alternatives**: Treat the old Central artifact as a same-line baseline -(rejected because its Boot/Java line and provenance do not match); delete types -immediately (rejected because adopter use is unknown). - -**Consequences**: accidental implementation imports fail at compile time and -the report-only comparison remains out of scope until a matching release is -requested. - -**Compatibility-only annotation members**: `RedisCacheEvict.unless` remains - declared for source/binary compatibility but is not evaluated because Spring's - `CacheEvictOperation` has no after-invocation `unless` slot; callers should use - `condition` for supported eviction gating. `RedisCacheable.type` and - `RedisCachePut.type` remain declaration metadata and do not coerce or validate - runtime values. Turning either into active behavior requires a separate - operation-semantics decision and regression contract. - -**Known limitation**: A same-line release tag and artifact are still required -before enabling a blocking Revapi/Japicmp gate. **Re-evaluate** after a -same-line published artifact and matching tag are verified. - -## 8. Integration test lifecycle - -**Context**: Failsafe was declared without executions, so four `*IT.java` -classes were omitted while most Testcontainers tests ran under Surefire. - -**Decision**: Use Surefire for the current integration model, rename the four -classes to `*IntegrationTest.java`, remove dead Failsafe configuration, and -run a naming guard in CI. - -**Alternatives**: Move all integration tests to Failsafe (rejected because it -would reclassify many existing tests and risk duplicate/report drift). - -**Consequences**: one local/CI report location and no known omitted `*IT` -classes. - -**Known limitation**: Docker/Testcontainers lifecycle is verified locally with -Redis 7-alpine; a separate cross-platform Docker daemon matrix remains -unverified. Re-evaluate if test cost requires a separate profile. - -## 9. Reactive no-go - -**Context**: The interceptor is blocking and no real reactive adopter, -architecture, or CI matrix exists. - -**Decision**: Reactive (`Mono`/`Flux`) is unsupported and not part of this -closure. - -**Alternatives**: Add a partial reactive adapter (rejected because it would be -pseudo-support). - -**Consequences**: scope remains one blocking Boot 4 line. - -**Known limitation**: WebFlux methods bypass ResiCache. **Re-evaluate** only -with adopter demand, nonblocking design, maintenance budget, and independent -compatibility CI. - -## 10. AOT/native entry conditions - -**Context**: Reflection, metadata, and serialization risks were not fully -assessed and no RuntimeHints are present. - -**Decision**: AOT smoke is conditional on the four core contract gates and -public boundary closure. Full native certification is deferred. No hints or -native support claim is added now. - -**Alternatives**: Add speculative RuntimeHints (rejected because it could mask -contract defects and claim unsupported behavior). - -**Consequences**: JVM support remains the only verified target line. - -**Verification note**: The local -`mise x java@temurin-21.0.12+101.0.LTS -- ./mvnw spring-boot:process-aot --DskipTests -B` smoke passed on JDK 21.0.12.1. The `native-image` probe exited -127 with `native-image: unavailable`; this validates JVM AOT processing only -and does not establish native-image support. - -**Known limitation**: Native behavior is unverified. **Re-evaluate** after a -real adopter, reflection inventory, and passing native-image validation. - -## 11. Refresh executor boundary - -**Context**: the early-refresh module (`EarlyRefresh`, called by -`EarlyExpirationHandler`) needs submit, while `ActualCacheHandler` -needs only cancellation. `RefreshCancellation` cannot represent full executor -lifecycle. - -**Decision**: `ThreadPoolEarlyExpirationExecutor` is an internal implementation -retained for construction compatibility. No public submit interface is added; -`RefreshCancellation` remains the narrow cross-package cancel seam. Executor -cleanup and shutdown stay internal. - -**Alternatives**: Expose submit/cancel/retry/cleanup/shutdown as a public SPI -(rejected because there is no second production adapter or adopter contract); -make Actual depend on the concrete executor (rejected because implementation -shape leaks into the chain). - -**Consequences**: the extension surface stays small and the chain depends only -on cancellation. - -**Known limitation**: Replacing the full executor is not a supported public -contract. **Re-evaluate** with a second real executor adapter and lifecycle -contract tests. - -## 12. Bloom CLEAN semantics - -**Context**: Treating Bloom as a cache-membership index made ordinary CLEAN -clear bits and depend on a failure-prone rebuilding marker and TTL window. - -**Decision**: CLEAN removes cache entries only; it never clears Bloom. Bloom -represents possible data-source membership, so retained bits can cause only -false-positives. Rebuilding markers, marker TTLs, and -`rebuild-window-seconds` are removed. - -**Consequences**: CLEAN no longer requires distributed rebuilding state, and -Bloom or Redis failures fail open so a valid loader remains executable. - -**Known limitation**: An explicit data-source Bloom rebuild operation is not -part of the cache eviction contract. - -## 13. Read-through write-back failure contract - -**Context**: `get(key, loader)` spans three phases — cache read, loader -(data source), and cache write-back. Previously the sync path merged loader -and write-back exceptions into a single `LoadFailed`, and the default path -(let Spring's `RedisCache.get(key, loader)` drive the writer's 5-arg `get`) -wrapped a write-back failure so the already-loaded business value was lost or -turned into a `ValueRetrievalException`. - -**Decision**: Availability-first, and one protocol. `LoaderOrchestrator.readThrough` -owns the whole read → load → write-back cycle, including write-back-failure -tolerance and the single redacted WARN. Both loader entries delegate to it; their -only entry-specific differences are the read-value representation and loader -failure translation. - -- **Default path**: the cache adapter supplies the chain read primitive, - converts `Cache.ValueWrapper` to the business value, and writes through - `RedisProCache.put`. The same protocol carries the put metrics and cannot drift - from the sync path. -- **Native writer path**: `RedisProCacheWriter` supplies the byte-oriented read - and write primitives and delegates to the same `readThrough` implementation. - Its low-level SPI contract keeps loader exceptions raw. `cacheTti` remains - intentionally ignored: native reads do not refresh TTL because that would add - write amplification. -- **Configuration errors are not write-back failures**: an - `IllegalArgumentException` from the write-back (for example a loader that - returns `null` while null caching is disabled) propagates instead of - degrading to a warn-and-return, so the misconfiguration is not silent. -- **Sync path**: `SyncSupport.executeSync` wraps the same cache adapter and - protocol, so followers share the leader's outcome — including a write-back failure. -- Cache loader exceptions propagate as `Cache.ValueRetrievalException` in both - cache paths; native writer loader exceptions propagate raw. A write-back failure - produces `LoadOutcome.LoadedWithWriteBackFailure(value, cause)`; the shared - protocol logs the cause redacted (no raw key) and both entries answer with the - loaded value. -- Explicit `PUT` / `PUT_IF_ABSENT` / `CLEAN` remain fail-fast typed - (`CacheOperationException`); `REMOVE` stays observable best-effort. - -**Consequences**: cache read failures degrade to miss; a cache write failure -never discards a successful loader result. Downstream caches may be stale -until the next write, which is observable through the failure metric and -redacted logs. - -**Known limitation**: The cache is a derived acceleration layer, not the -source of truth — eventual consistency after a failed write-back is accepted. -A per-write retry/backoff policy is not part of this contract. The async -`Cache.retrieve(key, supplier)` entry point is outside this contract: it -writes back through `store(...)` and completes its future exceptionally on a -write-back failure. - -## 14. Configuration binding validation - -**Context**: `RedisProCacheProperties` had only partial local constraints -(root `@Validated`, `defaultTtl @NotNull`, Redisson `@Min`s). Nested -properties lacked `@Valid` cascade; numeric fields (thread pools, ports, -bloom sizes, sync timeout) lacked bounds; cross-field relationships (Redis -deployment mode → host/cluster/sentinel; `tlsRequired` → `tlsEnabled`) had no -binding-time check; and bloom parameters bypassed the properties model via -raw `@Value` reads. - -**Decision**: All nested property groups are `@Valid @NotNull` cascaded; -numeric fields carry Jakarta `@Min`/`@Max` bounds; cross-field rules are a -class-level `@RedisDeploymentValidator` that binds violations to the concrete -property node; `resi-cache.bloom.*` moved into `RedisProCacheProperties.Bloom` -and the `bloomFilterConfig` bean consumes the bound properties instead of -`@Value`. - -**Consequences**: Invalid configuration fails once at binding time with a -full property path (`resi-cache.bloom.bitSize`, `redis.clusterNodes`, -`redis.tlsEnabled`), not at first runtime use. Defaults boot clean. - -**Known limitation**: The validator runs at `@ConfigurationProperties` -binding; programmatic `new RedisProCacheProperties()` mutation after bind is -not re-validated. - -## 15. Failure metrics and key privacy - -**Context**: Failures were scattered across `CacheErrorHandler`, Bloom and -read-through paths, counted mainly by logs; WARN/ERROR and exception messages -carried raw keys. The read-through write-back path may surface a writer -PUT-chain failure, so its metric ownership must remain explicit. - -**Decision**: A single internal `CacheFailureReporter` (not public, not in the -allowlist) exposes one metric `resicache.cache.failure` tagged only by finite -enums `operation`, `kind`, `strategy`. `CacheErrorHandler` is the single -count-once exit for chain failures, including read-through write-back failures -that go through the writer's PUT chain. `LoaderOrchestrator` does not call the -reporter; it only emits a redacted WARN and returns -`LoadedWithWriteBackFailure`. Exceptions outside that chain are not implicitly -reclassified by this metric. WARN/ERROR and typed exception messages omit the -raw key; `cacheName` (config-level, low cardinality) is kept for correlation. -`CacheOperationException` carries no raw-key field/getter. - -Where a diagnostic has no `cacheName` (distributed-lock keys, single-flight -role failures, async early-expiration retries) the raw key is replaced by -`FailureDiagnostics.keyFingerprint` — a short content token that keeps the -line correlatable without carrying the key. It is a correlation token, not a -security boundary: low-entropy keys remain brute-forceable from it. The -fingerprint rule covers lock acquisition/release, `SyncRole` leader/follower -failures (logs *and* their `IllegalStateException`/`RuntimeException` -messages), the async early-expiration retry path, and chain post-processing. -`DEBUG`/`INFO` sites are outside this rule — they are the tracing channel the -fingerprint correlates back to. Throwables follow the same rule: a WARN/ERROR -line renders the exception **type chain** (`FailureDiagnostics.sanitizedFailure`) -and the full stack goes to DEBUG, because exception messages can embed the key -(`Cache.ValueRetrievalException` states it verbatim) and SLF4J prints the -stack with the message. - -**Consequences**: GET degrade, write fail-fast, REMOVE best-effort and -writer-PUT-chain-originated read-through write-back failures are alertable by -bounded tags. `LoaderOrchestrator` preserves loaded values and emits only the -redacted tolerated-outcome warning, so the same failure is not counted twice. -The Bloom filter's own `bloomsift.*` counters and fail-open paths are -deliberately *not* routed here — fail-open is a successful protection behavior, -not a cache-operation failure, so reporting it would corrupt degradation -alerts. - -**Known limitation**: No per-key alerting; correlation relies on MDC -requestId or the `keyFingerprint` token. - -## 16. AOT/native deferred - -**Context**: Spring Boot `process-aot` was observed starting the -`SerializationMigrationCli` rather than the host auto-configuration — not a -host-config smoke. No `spring-boot-maven-plugin` AOT wiring exists in the pom. - -**Decision**: AOT/native-image compatibility is recorded **DEFERRED** -(non-blocking debt in the local task ledger as AOT-001). The serialization envelope -(`{version, payload}` + whitelist) is a known native-image reflection -surface that would need explicit `RuntimeHints` before GraalVM support is -claimed. - -**Consequences**: No native-image claim in README/STABILITY. Revisit when a -real native deployment requirement appears. - -## 17. Internal runtime module closure - -**Context**: The public-surface Gate still accounted for 86 implementation -types as in-progress because their source packages exposed Spring adapters, -operation builders, protection policies, metadata, and serializer details. - -**Decision**: Move the implementation collaborators into the package-private -`io.github.davidhlp.spring.cache.redis.cache` runtime module and physically -co-locate their tests. `RedisCacheAutoConfiguration` scans only that internal -package (excluding test classes); stable SPI types keep their original package -names. The wire envelope remains `serialization.VersionEnvelope`, and -`CacheContext` exposes only `InputView`/`CachePolicyView`. - -**Verification**: The compiled public surface is locked by the 35-entry -allowlist and the in-progress manifest is empty. Exact test counts belong to -the current CI/local verification record rather than this durable decision. - -## 18. Protection switch resolution semantics - -**Context**: The protection properties Javadoc said "startup-only" in the -summary but "runtime kill-switch" in per-mechanism entries, and implied a -per-mechanism `true` could override a global `false`. In reality the handler -chain is built once at first `createChain()` and global-off disables all four -protections regardless of per-mechanism values. - -**Decision**: Resolution is startup-static. `protection.enabled=false` wins and -disables every protection; a per-mechanism `false` can only further disable; -a per-mechanism `true` cannot re-enable from global off; any change requires a -restart. No dynamic chain rebuild is implemented. - -**Alternatives**: Runtime configuration listener with chain rebuild (rejected: -single cached chain, no hot-update requirement evidence, and rebuild would -introduce concurrency and metric-registration problems). - -**Consequences**: switch behavior is predictable and documented consistently in -`RedisProCacheProperties`, README, and COMPATIBILITY. - -**Test lock**: `CacheHandlerChainFactoryTest` combination tests, including -cached-chain invariance under property changes. - -**Known limitation**: no runtime toggle. **Re-evaluate** only with a real -hot-update requirement. - -## 19. Secure type-id validation across JSON forms - -**Context**: Jackson field-level type metadata can arrive as a property or as a -wrapper array, while the configured global type-property name may differ from -`@class`. A preflight that checks only one property does not protect every form. - -**Decision**: Install the whitelist-backed `PolymorphicTypeValidator` on every -serializer mapper and run one streaming preflight that checks `@class`, the -configured type-property, and wrapper-array type ids for the envelope/value -polymorphic fields before Jackson instantiates a value. - -**Consequences**: disallowed type ids fail before object construction; ordinary -allowed business values and collections retain their existing round-trip shape. - -## 20. CachedValue refresh metadata wire contract - -**Context**: The envelope carried the business value but Jackson ignored the -metadata required by early-expiration policy and version CAS. - -**Decision**: Persist `ttl`, `createdTime`, `lastAccessTime`, `visitTimes`, -`expired`, and `version` as explicit fields. Keep `startNanoTime` process-local -and reset it on deserialization; missing fields in older payloads retain their -zero/default values and use the wall-clock fallback. - -**Consequences**: refresh policy and value-version CAS work across JVMs without -persisting an invalid monotonic-clock origin. - -## 21. Early-expiration value-version CAS - -**Context**: The Lua CAS compared the envelope format version (`2`) with the -cached value version token, so concurrent writes could be accepted or valid -refreshes rejected for the wrong reason. - -**Decision**: Unwrap the envelope and compare `payload.version` (including the -supported wrapper-array shape) with the expected value-version argument using -exact string equality. A format-version change is handled by serializer -compatibility, not by this refresh CAS. - -## 22. Policy-driven early expiration - -**Context**: An absolute 60-second fast path skipped policy evaluation for long -TTL entries that were already inside a configured percentage refresh window. - -**Decision**: When early expiration is enabled, `EarlyRefresh` reads the -`CachedValue` and decides from its TTL, creation time, and configured threshold. -The prefetched hit is handed to the actual handler through `PrefetchDecision`; -the decision is never bypassed by an unrelated absolute threshold. - -## 23. Internal context and loader seams - -**Context**: `CacheContext` was described as immutable while exposing mutable -byte arrays, and `RedisProCache` rebuilt its loader callbacks on every call. - -**Decision**: Copy byte input at `CacheInput` construction and at the public -context read. Bind the loader callbacks once in the internal -`LoaderOrchestrator` constructor (three callbacks: redis key, cache read, -write-back); the full-parameter overload remains for internal isolation tests. The engine-only `markSkipRemaining` member remains as -a compatibility shim until a real external implementation justifies a migration. - -**Consequences**: callers receive a smaller production seam and cannot mutate -chain input bytes; no new public SPI or runtime reconfiguration is introduced. - -## 24. Per-operation policy resolution - -**Context**: The chain asked `RedisCacheRegister` for one namespace only -(`CACHEABLE`). `@RedisCachePut` and `@RedisCacheEvict` declarations were -registered but never read back, so a method annotated only with -`@RedisCachePut` silently ran without its `ttl`, `useBloomFilter`, `sync`, -`cacheNullValues` or early-expiration attributes — a write that never filled -the Bloom filter could then be judged "definitely missing" by a reader that -enabled Bloom, and the read would answer from nothing until a loader refilled -it. - -**Decision**: Resolve the namespace from the chain-side operation -(`OperationKind.forCacheOperation`, an exhaustive switch): GET reads the -`@RedisCacheable` declaration, PUT / PUT_IF_ABSENT read `@RedisCachePut`, and -REMOVE / CLEAN have no policy namespace at all, so no lookup is attempted. - -The read declaration wins whenever the method has one: read-through write-back -is part of the read operation and must keep that method's `ttl`, -`cacheNullValues` and Bloom declaration rather than picking up the write -annotation's defaults. `@RedisCachePut` therefore supplies policy only for -methods that are write-only. Distinguishing "the write-back of a read" from "an -explicit put" inside the chain is not possible — both reach the writer as -`CacheOperation.PUT` — so one of the two has to be the method's policy, and the -read side is the one that also governs the cache entry's lifetime. - -Writes inside the lock take `SyncSupport.executeExclusive` rather than the -single-flight `executeSync`: a write must never join another request's -in-flight result, because the joining thread would then skip its own work and -still report success. - -The chain consumes the stable `CachePolicyView.Source` rather than the internal -operation classes, and `RedisProCacheWriter`'s five-argument `put` (whose only -caller was a test) is deleted. - -**Consequences**: a method annotated only with `@RedisCachePut` now honours its -`ttl`, `useBloomFilter`, `sync`, `cacheNullValues` and early-expiration -attributes — including `ttl()`, whose annotation default is 60 seconds and -therefore now applies where the cache-level TTL used to. Methods that declare -both annotations are unchanged from before this decision. - -**State**: each annotated element is parsed once and then sealed into an immutable -`ParsedAnnotations` snapshot. The Spring operation source completes native -annotation adaptation before returning operations from that snapshot, writes its -policy operations to `RedisCacheRegister`, and the annotation chain reads the same -snapshot during invocation. Namespace entries remain indexed by operation kind, -so the resolution rules above are unchanged. The register is populated at element -resolution and is not written per invocation. diff --git a/docs/adr/README.md b/docs/adr/README.md deleted file mode 100644 index 1ed6fdd3..00000000 --- a/docs/adr/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Architecture Decision Records - -| ADR | Title | Status | Date | -|---|---|---|---| -| [0001](0001-interface-contract-closure.md) | ResiCache interface contract closure | accepted | 2026-09-04 | diff --git a/scripts/ci/check-docs-contracts.sh b/scripts/ci/check-docs-contracts.sh index c4b155a4..3880dd89 100755 --- a/scripts/ci/check-docs-contracts.sh +++ b/scripts/ci/check-docs-contracts.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash set -euo pipefail -docs=(README.md README.zh-CN.md STABILITY.md COMPATIBILITY.md CHANGELOG.md CONTRIBUTING.md CLAUDE.md) +# Keep this guard small: source/configuration remain the exact contract, while +# this script catches missing entry points and known documentation drift. +docs=( + README.md README.zh-CN.md AGENTS.md CLAUDE.md + STABILITY.md COMPATIBILITY.md CHANGELOG.md CONTRIBUTING.md SECURITY.md PERFORMANCE.md + docs/README.md docs/PRODUCT.md docs/ARCHITECTURE.md docs/DEVELOPMENT.md + docs/OPERATIONS.md docs/REFERENCE.md +) +required_docs=( + README.md README.zh-CN.md AGENTS.md CLAUDE.md + STABILITY.md COMPATIBILITY.md CHANGELOG.md CONTRIBUTING.md SECURITY.md PERFORMANCE.md + docs/README.md docs/PRODUCT.md docs/ARCHITECTURE.md docs/DEVELOPMENT.md + docs/OPERATIONS.md docs/REFERENCE.md +) + +for doc in "${required_docs[@]}"; do + if [[ ! -f "$doc" ]]; then + printf 'Missing canonical documentation entry point: %s\n' "$doc" >&2 + exit 1 + fi +done for forbidden in \ 'pr-checks.yml' \ @@ -12,7 +32,7 @@ for forbidden in \ '@ComponentScan' \ 'Java 21+' \ 'JDK 21+'; do - if git grep -nF -- "$forbidden" -- "${docs[@]}"; then + if grep -nF -- "$forbidden" "${docs[@]}"; then printf 'Forbidden stale documentation value: %s\n' "$forbidden" >&2 exit 1 fi @@ -23,14 +43,30 @@ for required in \ 'Reactive' \ 'Testcontainers | 1.20.6' \ 'resi-cache.bloom'; do - if ! git grep -qF -- "$required" -- "${docs[@]}"; then + found=0 + for doc in "${docs[@]}"; do + if grep -qF -- "$required" "$doc"; then + found=1 + break + fi + done + if [[ "$found" -ne 1 ]]; then printf 'Missing required contract documentation value: %s\n' "$required" >&2 exit 1 fi done -# Dead-javadoc-reference gate (Phase 8-A): known-removed/private members must not -# be {@link}-referenced in main source. Add patterns as members are removed. +# The map must expose every current-state owner; otherwise new pages can become +# orphaned even when the files themselves exist. +for owner in PRODUCT.md ARCHITECTURE.md DEVELOPMENT.md OPERATIONS.md REFERENCE.md; do + if ! grep -qF -- "$owner" docs/README.md; then + printf 'Documentation map does not name current-state owner: %s\n' "$owner" >&2 + exit 1 + fi +done + +# Dead-javadoc-reference gate: known-removed/private members must not be +# referenced in main source. Add patterns as members are removed. for dead_ref in \ 'RedisProCache#lookupOperation' \ 'RedisProCacheWriter#resolveOperation' \ @@ -42,11 +78,42 @@ for dead_ref in \ done # Test resources must describe the same Testcontainers BOM as pom.xml. +pom_testcontainers_version="$( + awk ' + /testcontainers-bom<\/artifactId>/ { in_bom=1; next } + in_bom && // { + sub(/.*/, "") + sub(/<\/version>.*/, "") + print + exit + } + ' pom.xml +)" +if [[ -z "$pom_testcontainers_version" ]]; then + printf 'Could not read Testcontainers BOM version from pom.xml\n' >&2 + exit 1 +fi + for resource in \ src/test/resources/testcontainers.properties \ src/test/resources/docker-java.properties; do - if grep -nF -- '1.20.4' "$resource"; then - printf 'Forbidden stale resource value in %s\n' "$resource" >&2 + if [[ ! -f "$resource" ]]; then + printf 'Missing Testcontainers configuration resource: %s\n' "$resource" >&2 + exit 1 + fi + resource_version="$( + awk -F 'testcontainers-bom:' ' + NF > 1 { + value = $2 + sub(/[^0-9.].*/, "", value) + print value + exit + } + ' "$resource" + )" + if [[ "$resource_version" != "$pom_testcontainers_version" ]]; then + printf 'Testcontainers BOM mismatch in %s: expected %s, found %s\n' \ + "$resource" "$pom_testcontainers_version" "${resource_version:-missing}" >&2 exit 1 fi done diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandler.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandler.java index a22d5145..e0c1113a 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandler.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandler.java @@ -34,7 +34,7 @@ * {@link #handleError} 方法 → per-operation 调度失去入口。两条路径都让 seam * 失去价值 — 真 seam。 * - *

typed failure(ADR-03):本类产出 {@link CacheResult#failure(CacheOperation, FailureKind, Throwable)} + *

typed failure contract:本类产出 {@link CacheResult#failure(CacheOperation, FailureKind, Throwable)} * — operation/kind 均为 typed 枚举,失败必须可归类、可分流。 */ @Slf4j @@ -106,7 +106,7 @@ static void finalizeFailure( /** - * 统一失败指标上报(ADR-06)— null 表示未装配(测试/registry 缺失 → no-op)。 + * 统一失败指标上报 contract— null 表示未装配(测试/registry 缺失 → no-op)。 * 每个失败事件在此唯一出口上报一次,不重复计数。 */ private final io.github.davidhlp.spring.cache.redis.cache.CacheFailureReporter failureReporter; @@ -160,13 +160,13 @@ private CacheResult handleException( FailureKind failureKind) { CacheResult result = CacheResult.failure(operation, failureKind, e); String operationName = operation == null ? "UNKNOWN" : operation.name(); - // ADR-06:每次失败恰好一次统一指标(operation/kind/strategy 有限枚举 tag) + // Failure-metrics contract:每次失败恰好一次统一指标(operation/kind/strategy 有限枚举 tag) if (failureReporter != null) { failureReporter.report(operation, failureKind, strategy); } return switch (strategy) { case FAIL_FAST -> { - // ADR-06 key 隐私:ERROR 不打印 raw key / 异常 message(可能嵌 key); + // Key-privacy contract:ERROR 不打印 raw key / 异常 message(可能嵌 key); // 完整栈(含 cause message)仅留 DEBUG 供开发诊断 log.error("Cache {} failed: cacheName={}, kind={}, cause={}", operationName, cacheName, failureKind, @@ -176,7 +176,7 @@ private CacheResult handleException( yield result; } case GRACEFUL_DEGRADATION -> { - // key 隐私:WARN 不打印 raw key / exception message(ADR-06) + // Key-privacy contract:WARN 不打印 raw key / exception message log.warn("Cache {} failed, degrading to miss: cacheName={}, kind={}, cause={}", operationName, cacheName, failureKind, e == null ? "null" : e.getClass().getSimpleName()); diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporter.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporter.java index e35cce87..af1b2111 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporter.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporter.java @@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentMap; /** - * 内部失败上报单点(ADR-06)— 不进入 public allowlist。 + * 内部失败上报单点— 不进入 public allowlist。 * *

唯一指标 {@code resicache.cache.failure},tag 仅低基数有限枚举: * {@code operation}({@link CacheOperation})、{@code kind}({@link FailureKind})、 @@ -24,7 +24,7 @@ * {@code CacheErrorHandler} 是唯一报告出口;{@code LoaderOrchestrator} 只保留脱敏 WARN * 与 {@code LoadedWithWriteBackFailure},不重复计数。 * - *

边界(ADR-06):本指标只统计缓存操作失败(GET degrade / 写 fail-fast / + *

边界:本指标只统计缓存操作失败(GET degrade / 写 fail-fast / * REMOVE best-effort / write-back failure)。Bloom 过滤器底层 Redis 位操作的 * {@code bloomsift.*} counter 与 fail-open 路径({@code BloomSupport} 吞异常放行 loader) * 不属于本指标 — fail-open 是成功的保护行为而非缓存失败,误报会污染降级告警。 diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheOperationException.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheOperationException.java index 6514d3ad..a2059658 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheOperationException.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheOperationException.java @@ -12,10 +12,10 @@ * Typed failure for a cache write operation that cannot be reported as success. * *

调用方可捕获本异常并按 {@link #getOperation()} / {@link #getFailureKind()} / cause - * 分流(ADR-07)。GET 失败有意不翻译为本异常 — 读降级为 miss,诊断留在 + * 分流(typed-exception contract)。GET 失败有意不翻译为本异常 — 读降级为 miss,诊断留在 * {@code CacheResult}。 * - *

Key 隐私(ADR-06):本异常不持有/暴露 raw key;message 也不含 raw key — + *

Key 隐私:本异常不持有/暴露 raw key;message 也不含 raw key — * 仅 operation/kind 与 cacheName(配置级低基数)。调用方如需关联具体请求,用 MDC requestId。 * *

构造限制:构造器 package-private — 仅框架内部(cache writer)可创建; @@ -41,7 +41,7 @@ public final class CacheOperationException extends RuntimeException { private static String message( CacheOperation operation, FailureKind failureKind, String cacheName) { String kind = failureKind == null ? "UNKNOWN" : failureKind.name(); - // 不含 raw key — ADR-06 key 隐私 + // 不含 raw key — key-privacy contract return "Cache " + operation + " failed (" + kind + ") for cacheName=" + cacheName; } diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ChainEngine.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ChainEngine.java index b2e70cf3..9910a225 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ChainEngine.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ChainEngine.java @@ -364,7 +364,7 @@ private void runPostProcess(CacheResult mainResult) { log.debug("Post-processing executed for: {}", CacheHandlerChain.handlerTag(handler)); } catch (Exception e) { - // ADR-0001 §15 key 隐私:ERROR 只带 cacheName + 异常类型链,不带 raw key; + // Key-privacy contract: ERROR includes cacheName and exception types only; // 完整栈留 DEBUG(异常 message 可能内嵌 key)。 log.error("Post-processing failed for: {}, operation: {}, cacheName: {}, cause={}", CacheHandlerChain.handlerTag(handler), @@ -421,7 +421,7 @@ void finish(String hookName, Object[] scopeTokens, Object result, } private void logFailure(ChainObserver observer, String hookName, Exception ex) { - // ADR-0001 §15:ERROR 只渲染异常类型链(异常 message 可能内嵌 raw key),完整栈留 DEBUG + // Key-privacy contract: ERROR renders only exception types; log.error("Observer {} {} failed: {}", observer.getClass().getSimpleName(), hookName, FailureDiagnostics.sanitizedFailure(ex)); diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/DistributedLockManager.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/DistributedLockManager.java index 2d5d3763..8538ea8f 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/DistributedLockManager.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/DistributedLockManager.java @@ -64,7 +64,7 @@ public Optional tryAcquire(final String key, final long timeoutSecon try { boolean acquired = lock.tryLock(timeoutSeconds, leaseTimeSeconds, TimeUnit.SECONDS); if (!acquired) { - // ADR-0001 §15 key 隐私:WARN 只带 keyFingerprint(关联令牌),不带 raw key / lockKey + // Key-privacy contract: WARN includes only keyFingerprint, never raw key / lockKey log.warn( "Failed to acquire distributed lock within {}s: keyFingerprint={}", timeoutSeconds, @@ -77,7 +77,7 @@ public Optional tryAcquire(final String key, final long timeoutSecon return Optional.of(new RedissonLockHandle(lock, key)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - // ADR-0001 §15 key 隐私:ERROR 与异常 message 均不带 raw key + // Key-privacy contract: ERROR and exception messages never include raw key log.error("Interrupted while waiting for distributed lock: keyFingerprint={}, cause={}", FailureDiagnostics.keyFingerprint(key), FailureDiagnostics.sanitizedFailure(e)); @@ -178,7 +178,7 @@ public void close() { return; } catch (Exception e) { if (attempt == MAX_UNLOCK_RETRIES) { - // ADR-0001 §15 key 隐私:ERROR/WARN 只带 keyFingerprint + // Key-privacy contract: ERROR/WARN includes only keyFingerprint log.error("Failed to release distributed lock after {} attempts: " + "keyFingerprint={}, cause={}", MAX_UNLOCK_RETRIES, diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/EarlyRefresh.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/EarlyRefresh.java index 2ea98e70..ac6f7220 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/EarlyRefresh.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/EarlyRefresh.java @@ -35,9 +35,10 @@ *

  • testability:异步刷新任务体({@link #performAsyncRefresh})可在无责任链的情况下直接驱动
  • * * - *

    边界(ADR-0001 §11):线程池 / 去重 / 重试 / 清理调度 / shutdown 仍归 - * {@link ThreadPoolEarlyExpirationExecutor};chain 侧取消仍只经 {@link RefreshCancellation} - * 单一方法 seam。本模块不新增对外 SPI,只是把「谁拥有提前过期这个概念」讲清楚。 + *

    Boundary: thread pool / deduplication / retry / cleanup scheduling / + * shutdown remain owned by {@link ThreadPoolEarlyExpirationExecutor}; chain-side + * cancellation still goes through the single {@link RefreshCancellation} method seam. + * This module only clarifies ownership of the early-expiration concept. */ @Slf4j @Component @@ -266,7 +267,7 @@ void performAsyncRefresh(String redisKey, String cacheName, CachedValue captured log.debug("Async early-expiration skipped: value changed: {}", redisKey); } } catch (Exception ex) { - // ADR-0001 §15 key 隐私:ERROR 只带 cacheName + keyFingerprint + 异常类型链 —— + // Key-privacy contract: ERROR includes only cacheName + keyFingerprint + exception type chain — // 异常 message / 栈可能内嵌 raw key(如 Cache.ValueRetrievalException),故不进 ERROR; // 完整栈留 DEBUG 供诊断。 log.error("Async early-expiration failed: cacheName={}, keyFingerprint={}, cause={}", diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/FailureDiagnostics.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/FailureDiagnostics.java index 0c86031e..30d396ae 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/FailureDiagnostics.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/FailureDiagnostics.java @@ -6,7 +6,7 @@ import org.springframework.lang.Nullable; /** - * 失败诊断的 key 隐私 helper(ADR-0001 §15)— 唯一 key 脱敏单点。 + * 失败诊断的 key 隐私 helper(key-privacy contract)— 唯一 key 脱敏单点。 * *

    契约:WARN/ERROR 与 typed exception message 不得出现 raw key;配置级低基数的 * {@code cacheName} 保留用于关联。当一条诊断既没有 cacheName、又需要与 DEBUG 原始日志关联时, diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestrator.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestrator.java index 130ff26f..65f63a2e 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestrator.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestrator.java @@ -98,7 +98,7 @@ public record Loaded(@Nullable T value) implements LoadOutcome { } /** - * loader 成功,但缓存写回失败(ADR-02 availability-first)。 + * loader 成功,但缓存写回失败(availability-first read-through contract)。 * *

    {@code value} 仍为 loader 产出的业务值,必须返回给调用方;{@code cause} * 为写回失败的原始异常,供 caller 记录诊断;若失败来自 writer PUT chain,指标已由 diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java index 051c45c6..819a726e 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java @@ -194,7 +194,7 @@ private Cache.ValueWrapper doubleCheckLookup(Object key) { */ RuntimeException translateFailure(Throwable cause, String cacheName) { if (cause instanceof Cache.ValueRetrievalException vre) { - // ADR-06 key 隐私:重建 VRE,key 位以 cacheName 代替原始 raw key, + // Key-privacy contract:重建 VRE,key 位以 cacheName 代替原始 raw key, // 保留类型(Spring 抽象层契约)与原始 cause。 return new Cache.ValueRetrievalException(cacheName, null, vre.getCause()); } diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheConfiguration.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheConfiguration.java index 964bf302..25bd4b59 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheConfiguration.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheConfiguration.java @@ -83,7 +83,7 @@ public MethodMetadataResolver methodMetadataResolver() { @ConditionalOnMissingBean(CacheErrorHandler.class) public CacheErrorHandler cacheErrorHandler(ResolvedMetrics resolvedMetrics) { MeterRegistry registry = resolvedMetrics.meterRegistry(); - // ADR-06:统一失败指标 reporter(registry 缺失 → 内部 no-op) + // Failure-metrics contract:统一失败指标 reporter(registry 缺失 → 内部 no-op) return new CacheErrorHandler( registry == null ? null : new io.github.davidhlp.spring.cache.redis.cache.CacheFailureReporter(registry)); diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheWriter.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheWriter.java index d62b298f..7d3ba522 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheWriter.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheWriter.java @@ -81,7 +81,7 @@ public byte[] get( } /** - * Read-through loader 入口 —— availability-first(ADR-0001 §13)。 + * Read-through loader 入口 —— availability-first read-through contract。 * *

    {@code RedisProCache.get(key, loader)} 与本方法都委派 * {@link LoaderOrchestrator#readThrough} 的同一条「读 → 回源 → 写回」协议。 diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RefreshRetryPolicy.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RefreshRetryPolicy.java index 429c8dbb..a5c22268 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RefreshRetryPolicy.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RefreshRetryPolicy.java @@ -47,7 +47,7 @@ public void executeWithRetry(String key, Runnable task) { return; // 成功,退出 } catch (Exception ex) { lastException = ex; - // ADR-0001 §15 key 隐私:WARN/ERROR 只带 keyFingerprint,不带 raw key + // Key-privacy contract: WARN/ERROR includes only keyFingerprint, never raw key log.warn("Async early-expiration failed (attempt {}/{}): keyFingerprint={}, cause={}", attempt, MAX_RETRY_COUNT, FailureDiagnostics.keyFingerprint(key), FailureDiagnostics.sanitizedFailure(ex)); diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializationMigrationEngine.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializationMigrationEngine.java index c506ed89..5fbf8a6a 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializationMigrationEngine.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializationMigrationEngine.java @@ -130,8 +130,8 @@ private void migrateSource(RedisConnection connection, byte[] key, MutableReport } catch (Exception ex) { report.failed++; record("failed"); - // ADR-0001 §15:WARN 不带 raw key,也不带异常 message(可能内嵌 key); - // 只留类型链 + 指纹,完整栈在 DEBUG。 + // Key-privacy contract: WARN omits raw key and exception message (it may contain the key); + // only the type chain and fingerprint remain; the full stack stays at DEBUG. log.warn("[ResiCache] Serialization migration rejected key fingerprint={}, cause={}", keyFingerprint(key), FailureDiagnostics.sanitizedFailure(ex)); log.debug("[ResiCache] Serialization migration rejection detail", ex); @@ -327,7 +327,7 @@ private static boolean endsWith(byte[] key, String suffix) { } /** - * key 内容指纹 — 委托 {@link FailureDiagnostics} 的单一实现(ADR-0001 §15), + * key 内容指纹 — 委托 {@link FailureDiagnostics} 的单一实现(key-privacy contract), * 使 migration 路径与锁/刷新路径的指纹形式不再各自漂移。 */ private static String keyFingerprint(byte[] key) { diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncRole.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncRole.java index 56c3c0d5..a487723a 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncRole.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncRole.java @@ -93,7 +93,7 @@ public T run() { failure = e; } catch (final InterruptedException e) { Thread.currentThread().interrupt(); - // ADR-0001 §15:异常 message 不带 raw key,只带 keyFingerprint + // Key-privacy contract: exception message omits raw key and keeps keyFingerprint failure = new IllegalStateException( "Thread interrupted while acquiring distributed lock: keyFingerprint=" + FailureDiagnostics.keyFingerprint(key), e); @@ -143,7 +143,7 @@ public T run() { long timeoutSeconds = timeout.seconds(); try { if (timeoutSeconds <= 0 && !leader.isDone()) { - // ADR-0001 §15:异常 message 不带 raw key,只带 keyFingerprint + // Key-privacy contract: exception message omits raw key and keeps keyFingerprint throw new IllegalStateException( "In-flight single-flight loader still running; waitTimeoutSeconds=" + timeoutSeconds @@ -261,7 +261,7 @@ static T run(Logger log, try (LockStack lockStack = new LockStack(log)) { for (LockManager manager : distributedManagers) { manager.tryAcquire(key, timeout.seconds()).ifPresentOrElse(lockStack::push, () -> { - // ADR-0001 §15 key 隐私:WARN 只带 keyFingerprint + // Key-privacy contract: WARN includes only keyFingerprint log.warn("Lock manager {} failed to acquire distributed lock: keyFingerprint={}", manager.getClass().getSimpleName(), FailureDiagnostics.keyFingerprint(key)); diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncSupport.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncSupport.java index d7036146..9a1e0640 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncSupport.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncSupport.java @@ -157,7 +157,7 @@ static T executeRoleWork(Logger log, FailureDiagnostics.keyFingerprint(key)); return state.executeLocalOnly(key, timeout, work); } - // ADR-0001 §15:异常 message 不带 raw key。 + // Key-privacy contract: exception message omits raw key. throw new IllegalStateException( "sync=true 已声明但无分布式锁后端 (无 RedissonClient / LockManager bean)。" + "拒绝静默退化为单 JVM synchronized (多实例下无法防击穿)。" diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ThreadPoolEarlyExpirationExecutor.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ThreadPoolEarlyExpirationExecutor.java index 87a74386..6670833c 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ThreadPoolEarlyExpirationExecutor.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ThreadPoolEarlyExpirationExecutor.java @@ -167,7 +167,7 @@ public void submit(String key, Runnable task) { inFlight.remove(k, created); metrics.recordCompleted(); if (throwable != null) { - // ADR-0001 §15 key 隐私:ERROR 只带 keyFingerprint + // Key-privacy contract: ERROR includes only keyFingerprint log.error("Async early-expiration failed after all retries: " + "keyFingerprint={}, cause={}", FailureDiagnostics.keyFingerprint(k), diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/chain/CacheResult.java b/src/main/java/io/github/davidhlp/spring/cache/redis/chain/CacheResult.java index d07f1f43..d3dd255d 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/chain/CacheResult.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/chain/CacheResult.java @@ -10,7 +10,7 @@ /** * 缓存操作结果 — 责任链出口的不可变值类型。 * - *

    合法状态模型(ADR-03):本类是不可变 {@code final} 值类型,合法状态由 + *

    合法状态模型:本类是不可变 {@code final} 值类型,合法状态由 * 受控静态工厂 + 嵌套 {@link Outcome} / {@link FailureKind} 枚举表达,非法组合 * (如「失败但没有 kind」「success + failure 字段并存」)在类型层面不可表示: * diff --git a/src/test/java/com/example/extension/ExternalCacheOperationExceptionContractTest.java b/src/test/java/com/example/extension/ExternalCacheOperationExceptionContractTest.java index 393fa51d..98e9b72e 100644 --- a/src/test/java/com/example/extension/ExternalCacheOperationExceptionContractTest.java +++ b/src/test/java/com/example/extension/ExternalCacheOperationExceptionContractTest.java @@ -13,13 +13,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * 外部包 typed-exception 契约测试(ADR-07) — 外部扩展方可捕获并按 operation/kind 分流。 + * 外部包 typed-exception 契约测试 — 外部扩展方可捕获并按 operation/kind 分流。 * *

    从 {@code com.example.extension}(框架根包之外)验证: *

      *
    • {@link CacheOperationException} 是 public final,可捕获
    • *
    • getOperation()/getFailureKind()/cause 可用(typed 枚举)
    • - *
    • message/toString 不含 raw key(ADR-06 隐私)
    • + *
    • message/toString 不含 raw key(隐私 contract)
    • *
    • 构造器不可从外部访问(不能伪造)
    • *
    */ diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/PublicSurfaceContractTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/PublicSurfaceContractTest.java index 8213dd39..af6dc981 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/PublicSurfaceContractTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/PublicSurfaceContractTest.java @@ -26,7 +26,7 @@ import static org.assertj.core.api.Assertions.fail; /** - * Public-surface allowlist Gate(ADR-04)。 + * Public-surface allowlist Gate。 * *

    编译后反射枚举 {@code io.github.davidhlp.spring.cache.redis} 下所有 public 顶层类型, * 与审定 allowlist({@code src/test/resources/allowlist/public-surface.txt})精确比较。 diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFailureLogKeyPrivacyTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFailureLogKeyPrivacyTest.java index dc2cb560..1b14981e 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFailureLogKeyPrivacyTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFailureLogKeyPrivacyTest.java @@ -15,7 +15,7 @@ import static org.mockito.Mockito.mock; /** - * Bloom 失败日志 key 隐私契约(ADR-06)。 + * Bloom 失败日志 key 隐私契约。 * *

    Bloom add/check/clear 失败时,ERROR 日志不得包含 raw key(测试 key 必须不出现); * cacheName(配置级低基数)可保留。用 Logback {@link ListAppender} 捕获日志事件断言。 @@ -52,7 +52,7 @@ void bloomSupport_addFailure_logOmitsRawKey() { .map(ILoggingEvent::getFormattedMessage) .reduce("", String::concat); assertThat(allLogs) - .as("ERROR 日志不得包含 raw key(ADR-06 key 隐私)") + .as("ERROR 日志不得包含 raw key(key-privacy contract)") .doesNotContain(SECRET_KEY); } finally { ((Logger) LoggerFactory.getLogger(BloomSupport.class.getName())).detachAppender(captured); @@ -85,7 +85,7 @@ void redisBloomFilter_checkFailure_logOmitsRawKey() { .map(ILoggingEvent::getFormattedMessage) .reduce("", String::concat); assertThat(allLogs) - .as("ERROR 日志不得包含 raw key(ADR-06 key 隐私)") + .as("ERROR 日志不得包含 raw key(key-privacy contract)") .doesNotContain(SECRET_KEY); } finally { ((Logger) LoggerFactory.getLogger(RedisBloomIFilter.class.getName())).detachAppender(captured); diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterHandlerTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterHandlerTest.java index 33d74d6d..820e48a1 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterHandlerTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterHandlerTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.when; /** - * BloomFilterHandler 单元测试 — marker-free 语义(ADR-01)。 + * BloomFilterHandler 单元测试 — marker-free 语义。 * *

    锁定契约: *

      @@ -300,7 +300,7 @@ void afterChainExecution_nullResult_doesNothing() { } @Nested - @DisplayName("CLEAN→GET scenarios (ADR-01)") + @DisplayName("CLEAN→GET scenarios (marker-free)") class CleanGetScenarios { /** diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterIntegrationTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterIntegrationTest.java index b06bba5f..45738026 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterIntegrationTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterIntegrationTest.java @@ -43,8 +43,8 @@ void setUp() { /** * 断言 Redis 中不存在任何 rebuilding marker 键(无 marker I/O 架构约束)。 * - *

      ADR-01 删除 rebuilding marker/window 后,任何 Bloom 操作都不得读写 - * {@code *rebuild*} marker 键空间。flushDb 后该 keyspace 必须始终为空。 + * Marker-free Bloom semantics: after removing the rebuilding marker/window, + * Bloom operations must never read or write the {@code *rebuild*} marker keyspace. */ private void assertNoRebuildMarkerKeys() { assertThat(redisTemplate.keys("*rebuild*")).isEmpty(); @@ -205,7 +205,7 @@ void falsePositiveRateWithinBounds() { } @Nested - @DisplayName("Marker-free Bloom semantics (ADR-01)") + @DisplayName("Marker-free Bloom semantics") class MarkerFreeSemanticsTests { @Test diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandlerTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandlerTest.java index bd942f2a..10cee3bb 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandlerTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheErrorHandlerTest.java @@ -320,11 +320,11 @@ void finalizeFailure_remove_doesNotThrow() { @Nested - @DisplayName("ADR-06 count-once failure metric") + @DisplayName("count-once failure metric") class FailureMetricTests { @Test - @DisplayName("cache-path WARN/ERROR 日志不含 raw key / exception message(ADR-06)") + @DisplayName("cache-path WARN/ERROR 日志不含 raw key / exception message") void cachePathLogs_omitRawKeyAndCauseMessage() { String secretKey = "secret-cache-key-77"; ch.qos.logback.classic.Logger logger = diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporterTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporterTest.java index ad2848c5..40fd413e 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporterTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/CacheFailureReporterTest.java @@ -17,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * CacheFailureReporter 单元测试(ADR-06)。 + * CacheFailureReporter 单元测试。 * *

      锁定契约: *

        diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/FailureLogKeyPrivacyTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/FailureLogKeyPrivacyTest.java index 7d64b679..17b835d8 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/FailureLogKeyPrivacyTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/FailureLogKeyPrivacyTest.java @@ -39,7 +39,7 @@ import static org.mockito.Mockito.when; /** - * 失败诊断 key 隐私回归(ADR-0001 §15)。 + * 失败诊断 key 隐私回归(key-privacy contract)。 * *

        契约:WARN/ERROR 日志与 typed exception message 不得出现 raw key; * 配置级低基数的 {@code cacheName} 或 {@link FailureDiagnostics#keyFingerprint} 关联令牌可保留。 @@ -47,7 +47,7 @@ *

        本测试覆盖此前直接打印 raw key 的路径:分布式锁获取/释放、single-flight 角色失败、 * 异步提前过期重试、链后置处理。每条路径用 logback {@link ListAppender} 捕获实际日志事件断言。 */ -@DisplayName("Failure Log Key Privacy Tests (ADR-0001 §15)") +@DisplayName("Failure Log Key Privacy Tests (key-privacy contract)") class FailureLogKeyPrivacyTest { /** 必须不出现的原始 key(测试专用哨兵值)。 */ @@ -118,7 +118,7 @@ private void assertErrorSites(ListAppender captured, String... fr .as("ERROR 必须渲染异常类型链") .allMatch(event -> event.getFormattedMessage().contains("IllegalStateException")); assertThat(warnAndErrorText(captured)) - .as("WARN/ERROR 不得包含 raw key 或异常 message(ADR-0001 §15)") + .as("WARN/ERROR 不得包含 raw key 或异常 message(key-privacy contract)") .doesNotContain(SECRET_KEY) .doesNotContain("boom"); } @@ -210,7 +210,7 @@ void refreshRetryPolicy_exhaustedRetries_omitsRawKey() { assertThat(attempts.get()).isEqualTo(RefreshRetryPolicy.MAX_RETRY_COUNT); assertThat(warnAndErrorText(captured)) - .as("WARN/ERROR 不得包含 raw key(ADR-0001 §15)") + .as("WARN/ERROR 不得包含 raw key(key-privacy contract)") .doesNotContain(SECRET_KEY) .contains(FailureDiagnostics.keyFingerprint(SECRET_KEY)); } finally { @@ -320,7 +320,7 @@ public HandlerResult handle(CacheContext ctx) { }), context); assertThat(warnAndErrorText(captured)) - .as("observer 失败的 ERROR 不得渲染异常 message/栈(ADR-0001 §15)") + .as("observer 失败的 ERROR 不得渲染异常 message/栈(key-privacy contract)") .doesNotContain(SECRET_KEY); assertErrorSites(captured, "onChainStart failed"); assertDebugKeepsStack(captured, "onChainStart failure detail"); diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestratorTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestratorTest.java index c9279a0a..44fb50c4 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestratorTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/LoaderOrchestratorTest.java @@ -345,7 +345,7 @@ void loaderThrows_wrapsInValueRetrievalException() { } @Test - @DisplayName("write-back fails after loader success → LoadedWithWriteBackFailure carrying value + cause (ADR-02)") + @DisplayName("write-back fails after loader success → LoadedWithWriteBackFailure carrying value + cause") void writeBackFails_returnsLoadedWithWriteBackFailure() { RedisCacheableOperation op = operation(false, true); when(syncSupport.executeSync(anyString(), any(java.util.function.Supplier.class), @@ -521,7 +521,7 @@ void writeBackConfigurationError_propagates() { } @Test - @DisplayName("write-back fails after loader success → LoadedWithWriteBackFailure (ADR-02 同 sync 路径)") + @DisplayName("write-back fails after loader success → LoadedWithWriteBackFailure (same default path)") void writeBackFails_defaultPathReturnsLoadedWithWriteBackFailure() { RedisCacheableOperation op = operation(false, false); RuntimeException putBoom = new RuntimeException("redis put failed"); diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/PathCAopContractIntegrationTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/PathCAopContractIntegrationTest.java index ae7b097e..06c6a94b 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/PathCAopContractIntegrationTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/PathCAopContractIntegrationTest.java @@ -125,7 +125,7 @@ void syncPlusBloom_warmBloom_usesActualKey_returnsValue() { } @Nested - @DisplayName("ADR-01 CLEAN 不改变 Bloom(marker-free 语义)") + @DisplayName("CLEAN 不改变 Bloom(marker-free 语义)") class CleanDoesNotChangeBloomTests { @Test @@ -134,7 +134,7 @@ void cleanThenGet_loaderStillReachable() { cacheService.getByIdWithBloomFilter(1L); assertThat(cacheService.getCallCount()).isEqualTo(1); - // 缓存数据层清空(布隆位保留 — ADR-01: CLEAN 不清布隆) + // 缓存数据层清空(布隆位保留 — CLEAN 不清布隆) cacheService.evictAll(); assertThat(redisCacheTemplate.opsForValue().get("testCache::1")).isNull(); diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheSemanticsIntegrationTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheSemanticsIntegrationTest.java index 6cba06ab..d39a1c37 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheSemanticsIntegrationTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheSemanticsIntegrationTest.java @@ -174,7 +174,7 @@ void unlessTrue_doesNotCache() { } @Nested - @DisplayName("ADR-01 CLEAN/GET race 与多上下文(marker-free)") + @DisplayName("CLEAN/GET race 与多上下文(marker-free)") class CleanGetRaceTests { /** * 同一 key:CLEAN 并发于 GET —— loader 永不因空 Bloom 被短路。 diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisDownFaultInjectionIntegrationTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisDownFaultInjectionIntegrationTest.java index f3e012d9..b5de7521 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisDownFaultInjectionIntegrationTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisDownFaultInjectionIntegrationTest.java @@ -120,7 +120,7 @@ void redisDown_get_degradesGracefully() throws Exception { } @Test - @DisplayName("RedisDown-4: writer-level read-through keeps the loaded bytes on write-back failure (ADR-02)") + @DisplayName("RedisDown-4: writer-level read-through keeps the loaded bytes on write-back failure") void redisDown_writerReadThrough_loaderValueSurvivesWriteBackFailure() { // writer 级入口(getNativeCache() 可达):缓存读 miss → loader 成功 → 写回失败。 // availability-first:必须返回 loader 值,不得被写回失败覆盖。 diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheLoadPathTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheLoadPathTest.java index 01d37aec..919f938c 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheLoadPathTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCacheLoadPathTest.java @@ -34,7 +34,7 @@ import static org.mockito.Mockito.when; /** - * 统一 read-through load 协议(ADR-0001 §13)在 cache 入口的契约测试 —— 无容器。 + * 统一 read-through load 协议在 cache 入口的契约测试 —— 无容器。 * *

        两条 loader 路径(sync / 非 sync)共用 {@code LoaderOrchestrator} 内的同一协议, * 因此非 sync 路径也必须:走带 metrics 的 {@code put} 写回、缓存命中不调 loader、 @@ -246,7 +246,7 @@ void nonSyncWriteBackFailure_returnsLoaderValueWithRedactedWarning() { String value = cache.get(SENTINEL_KEY, () -> "business-value"); assertThat(value) - .as("availability-first:写回失败不得覆盖已加载值(ADR-02)") + .as("availability-first:写回失败不得覆盖已加载值") .isEqualTo("business-value"); String warnText = captured.list.stream() @@ -255,7 +255,7 @@ void nonSyncWriteBackFailure_returnsLoaderValueWithRedactedWarning() { assertThat(warnText) .as("写回失败必须可观测") .contains("write-back failed") - .as("ADR-0001 §15:WARN 不带 raw key") + .as("key-privacy contract: WARN 不带 raw key") .doesNotContain(SENTINEL_KEY); } finally { logger.detachAppender(captured); diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/TestCacheService.java b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/TestCacheService.java index 56d2ce18..4d6829c6 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/cache/TestCacheService.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/cache/TestCacheService.java @@ -64,7 +64,7 @@ public void evictAll() { } /** - * CLEAN(allEntries) + useBloomFilter —— ADR-01 场景契约。 + * CLEAN(allEntries) + useBloomFilter —— 场景契约。 * *

        与普通 CLEAN 相同(注解使用方仍需显式开启 bloom 才会触发 * {@code BloomFilterHandler});本方法用于验证 CLEAN 即便启用 bloom diff --git a/src/test/java/io/github/davidhlp/spring/cache/redis/chain/CacheResultTest.java b/src/test/java/io/github/davidhlp/spring/cache/redis/chain/CacheResultTest.java index 5ee64a43..b61629a1 100644 --- a/src/test/java/io/github/davidhlp/spring/cache/redis/chain/CacheResultTest.java +++ b/src/test/java/io/github/davidhlp/spring/cache/redis/chain/CacheResultTest.java @@ -9,7 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * CacheResult 单元测试 — 合法状态模型(ADR-03)。 + * CacheResult 单元测试 — 合法状态模型。 * *

        锁定契约: *