From eb8ef55004fd11e03aa8831a38deea1472ef6250 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Wed, 7 Oct 2026 23:13:42 +0800 Subject: [PATCH 1/8] docs: reconcile project guides with current source and CI --- .github/PULL_REQUEST_TEMPLATE.md | 1 + AGENTS.md | 3 +- CHANGELOG.md | 19 ++++- COMPATIBILITY.md | 60 +++++++--------- CONTRIBUTING.md | 11 +-- PERFORMANCE.md | 69 +++++++++++++------ README.md | 34 +++++++-- README.zh-CN.md | 31 +++++++-- SECURITY.md | 22 ++++-- STABILITY.md | 30 ++++---- docs/ARCHITECTURE.md | 8 ++- docs/DEVELOPMENT.md | 20 ++++-- docs/OPERATIONS.md | 41 +++++++++-- docs/PRODUCT.md | 11 +-- docs/README.md | 3 +- docs/REFERENCE.md | 33 +++++++-- .../redis/cache/BloomFilterBenchmark.java | 15 ++-- .../cache/redis/cache/SyncLockBenchmark.java | 9 +-- .../cache/redis/cache/TtlJitterBenchmark.java | 7 +- .../cache/redis/cache/ActualCacheHandler.java | 6 +- .../redis/cache/CacheHandlerChainFactory.java | 6 +- .../redis/cache/RedisCacheInterceptor.java | 2 +- .../SerializerWhitelistStartupGuard.java | 5 +- .../cache/redis/chain/HandlerOrder.java | 2 +- .../redis/config/RedisProCacheProperties.java | 2 +- .../migration/SerializationMigrationCli.java | 2 +- 26 files changed, 304 insertions(+), 148 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c6285836..1d7422b5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,6 +13,7 @@ - [ ] `./mvnw checkstyle:check -B` passes when Java source changed. - [ ] New behavior has tests; bug fixes have a regression test. - [ ] Redis integration tests use `AbstractRedisIntegrationTest`; Cluster tests use `AbstractRedisClusterIntegrationTest` (Testcontainers — Docker must be running). +- [ ] `bash scripts/ci/check-workflows.sh` passes when workflows or CI scripts changed. - [ ] `bash scripts/ci/check-test-names.sh` passes; integration classes do not use `*IT.java`. - [ ] `bash scripts/ci/check-docs-contracts.sh` passes when docs, source references, or public contracts changed. - [ ] Documentation changes update the canonical owner in [`docs/README.md`](../docs/README.md); no task/date/session document was added as current-state policy. diff --git a/AGENTS.md b/AGENTS.md index b9d5defa..57a8d569 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ documentation and source. | Distributed lock | optional Redisson 3.50.0 | `pom.xml` | | Local support | Caffeine 3.1.8 | `pom.xml` | | Build | Maven 3.x / `./mvnw` | root POM and wrapper | -| Tests | JUnit 5, Testcontainers, AssertJ, Awaitility | `pom.xml` and `src/test/` | +| Tests | JUnit Jupiter (Boot-managed), Testcontainers, AssertJ, Awaitility | `pom.xml` and `src/test/` | Do not copy dependency versions into a second contract. The current source tree and module ownership are in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). @@ -105,6 +105,7 @@ runtime module. - Separate style gate: `./mvnw checkstyle:check -B`. - Packaged public-consumer gate: `bash scripts/ci/check-external-consumer.sh`. - Documentation/source guard: `bash scripts/ci/check-docs-contracts.sh`. +- Workflow/script guard: `bash scripts/ci/check-workflows.sh`. Full command semantics, test layers, CI, and contribution checks live in [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) and diff --git a/CHANGELOG.md b/CHANGELOG.md index fc2cf009..cb5e568a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,20 @@ contract. > Java 21 artifact exists yet, so a blocking compatibility gate waits for a > same-line published artifact with matching tag provenance. +The source-cleanup section labelled `0.0.2` below is repository history, not a +release note for the historical Central artifact with the same version. Current +source changes remain unreleased; publication is tracked in +[`COMPATIBILITY.md`](COMPATIBILITY.md). + ## [Unreleased] — current development +### Documentation + +- Reconcile adoption, configuration fallback, explicit serializer allowlists, + synchronization timeout, batch migration/rollback, test tooling and public + surface descriptions against the current source and CI. Preserve historical + benchmark scores while removing unsupported performance-SLO labels. + ### CI/CD - Share verification across PR/main/merge-group/manual/release workflows; fail @@ -387,8 +399,9 @@ Current milestones: engine-side rejection of a malformed `HandlerResult` is retained because `STABILITY.md` §4 documents it. - **TTL precedence in one module (c8)** — `TtlPolicy` owns the ordered - resolution and both defaults; every path keeps its previous effective TTL and - no default changed. See [`COMPATIBILITY.md`](./COMPATIBILITY.md) and + resolution. The ownership extraction preserved behavior; the separate + annotation-default change above removed the implicit 60-second fallback. See + [`COMPATIBILITY.md`](./COMPATIBILITY.md) and [`docs/REFERENCE.md`](docs/REFERENCE.md). - **Synchronization lifecycle owned by its state (c9)** — the single-flight lifecycle (enter → complete → exit → cleanup, exactly once) now lives with the @@ -414,7 +427,7 @@ Current milestones: bumped to `'21'` to match `pom.xml 21`. - Whitelist rejection message now mentions the remediation property key. -## [0.0.2] — current +## [0.0.2] — historical source cleanup ### Removed — over-engineering cleanup (~2,989 lines) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 042eb87d..a7ff73f9 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -6,18 +6,21 @@ ResiCache ships on a **single build line**: Redisson 3.50**. CI is configured to run `clean verify -B` on Java 21. Local verification also -requires JDK 21 and Docker for Testcontainers; this session is not a release -baseline. +requires JDK 21 and Docker for Testcontainers. A configured CI matrix is not +a claim that every patch of a component has been tested. > **Historical context**: Previously the repository carried a `boot3` line > (Boot 3.4.13 / Java 17 / Redisson 3.27). The migration to Boot 4 merged > into the main line; the dual-branch strategy is **abandoned**. Boot 3.x > compatibility is not maintained. See `CHANGELOG.md` for migration context. > -> Verified 2026-09-05: every Maven Central version (0.0.1–0.0.5, 0.0.7, -> 3.2.4) is the earlier Boot 3.2.4 / Java 17 line; no Boot 4 artifact is -> published yet. A bounded public adopter search on the same date found no -> external consumers of any line (private adopters remain unprovable). +> Publication checked 2026-10-07 against [Central metadata](https://repo.maven.apache.org/maven2/io/github/davidhlp/ResiCache/maven-metadata.xml) +> and each published POM: versions 0.0.1–0.0.5, 0.0.7 and 3.2.4 all belong +> to the earlier Boot 3.2.4 / Java 17 / Redisson 3.17.6 line. There is no +> matching Boot 4 artifact. The checkout's `0.0.2` build version must not be +> confused with the historical [Central 0.0.2 POM](https://repo.maven.apache.org/maven2/io/github/davidhlp/ResiCache/0.0.2/ResiCache-0.0.2.pom). +> A bounded public adopter search on 2026-09-05 found no external consumers; +> that historical search does not establish current adoption or private usage. ## Supported versions @@ -26,7 +29,7 @@ baseline. | Component | Version | Tested | |-----------|---------|--------| | Java | 21 | CI | -| Spring Boot | 4.0.0 | 4.0.x (CI) | +| Spring Boot | 4.0.0 | 4.0.0 (CI); no multi-patch matrix | | Spring Framework | 7.x | (via Boot) | | Spring Cache | 7.x | (via Boot) | | Spring Data Redis | 4.0.x | (via Boot) | @@ -52,29 +55,19 @@ baseline. | Dependency | Required? | Notes | |---|---|---| -| **Redisson** | Optional | Needed for distributed-lock (`sync=true`). Without it, a - sync operation fails fast unless `resi-cache.sync-lock.local-only=true` is - explicitly configured. | -| **Micrometer Core** | Required | Runtime handler and metrics seams reference its - types even when metrics publishing is disabled. Its version is managed by the - Spring Boot dependency management in `pom.xml`; it does not create a registry. | -| **Actuator / metrics registry provider** | Optional | Cache metrics require - `resi-cache.metrics.enabled=true` (default OFF) and a `MeterRegistry`; - otherwise the resolved metrics seam is a no-op adapter. - `RedisCacheHealthIndicator` requires Actuator and the `HealthIndicator` - class; it is not gated on the metrics property, so an application with - Actuator and Redis assembles it and each `/actuator/health` probe issues a - synchronous Redis `connection.ping()` round trip (see the probe-cost note in - [`docs/OPERATIONS.md`](docs/OPERATIONS.md)). | -| **Caffeine** | Bundled | Used internally for the local hash cache and - bloom-filter bitset; not exposed as a multi-level cache. | +| **Redisson** | Optional | Add the core dependency explicitly for the built-in distributed lock, or supply a `LockManager`. Without a backend, `sync=true` fails closed unless `resi-cache.sync-lock.local-only=true`. | +| **Micrometer Core** | Required | Runtime SPI and handler types reference it even when publishing is disabled. The Boot-managed dependency does not create an application registry. | +| **Actuator / registry provider** | Optional | Publishing requires `resi-cache.metrics.enabled=true` and a `MeterRegistry`. The Actuator health indicator is independent of that switch and performs a synchronous Redis PING per invocation; see [`docs/OPERATIONS.md`](docs/OPERATIONS.md#observability-and-diagnosis). | +| **Caffeine** | Bundled | Internal hash-position cache and local Bloom support; no multi-level cache API. | ## Serialization compatibility ⚠️ ResiCache serializes values in an internal `{version, payload}` envelope via `SecureJackson` for safe deserialization. This is **not** wire-compatible with -Spring's `GenericJackson2JsonRedisSerializer` or `JdkSerializer`. Existing caches must be **migrated** when adopting ResiCache, otherwise the -entire cache misses on cutover. Adopt a bounded **shadow-read → dual-write → +Spring's `GenericJackson2JsonRedisSerializer` or `JdkSerializer`. Existing +caches must be **migrated** when adopting ResiCache; otherwise values may miss +or fail decoding, depending on the configured failure policy. Adopt a bounded +**shadow-read → dual-write → cutover** migration workflow: run ResiCache alongside the existing cache, shadow-read through the new serializer while dual-writing, then cut over once hit rates stabilize. This preserves TTL, supports resumable rollback, and does @@ -83,8 +76,8 @@ not require a cache flush. ## Known limitations - **Reactive types**: `Mono` / `Flux` return types are **not supported**. - ResiCache's interceptor is blocking; such methods log an explicit "caching - will not take effect" warning and bypass ResiCache. + The blocking interceptor logs a warning and bypasses its cache advice for + these declared return types. Other host advisors retain their own behavior. - **Async methods**: `@Async` cached methods are not supported for sync-lock and Bloom-filter enhancements. @@ -114,18 +107,13 @@ not require a cache flush. `RedisProCacheWriter` for GET/GET-hit/GET-miss/PUT/DELETE, including exact `clear` deletion counts and PUT_IF_ABSENT insertion. `withStatisticsCollector` fully rebinds statistics; lock-wait duration remains unreported (zero). -- **Class-level cache annotations**: Spring operation resolution sees class-level ResiCache annotations, but the annotation chain does not apply their policy fields to methods without method-level annotations; this behavior is unchanged from `main`. +- **Class-level cache annotations**: Spring operation resolution sees class-level ResiCache annotations, but the annotation chain does not apply their policy fields to methods without method-level annotations; use method-level declarations when a protection policy is required. - **`value` and `cacheNames` resolution**: the three annotations are not `@AliasFor`-linked, so one declaration may set both attributes. There is one resolution for both faces (`RedisCacheAttributesProjector.resolveCacheNames`) and **`value` wins**; `cacheNames` is the fallback and only applies when `value` is empty. A declaration that sets both targets the `value` cache. - This is the operation face's `main` behaviour, so the cache in use is - unchanged. The policy face changes for that same both-set declaration: on - `main` the operation targeted `value` while the policy snapshot was - registered under `cacheNames`, so the declared policy silently did not apply - to the cache that was used. Both faces now resolve to `value`, and the policy - applies to the cache in use. + Both the Spring operation and policy snapshot resolve to that same cache. - **TTL default precedence**: one module owns the resolution (`TtlPolicy`; the ordered rule is specified in [`docs/REFERENCE.md`](docs/REFERENCE.md)). A method-level `ttl` greater than zero is the only declaration that overrides @@ -144,8 +132,8 @@ not require a cache flush. seconds, so an annotated method without an explicit `ttl` expired its entries after `60s` even when `resi-cache.default-ttl` was configured. The annotation-side implicit `60` is removed: such a method now uses the - configured default (`30m` unless overridden), and `60` is no longer - reachable from the resolution path. Methods that set `ttl` explicitly are + configured default (`30m` unless overridden), with no implicit `60s` fallback + in the resolution path. Methods that set `ttl` explicitly are unaffected. Deployments relying on the old `60s` expiry for methods that omit `ttl` must either set `ttl` explicitly or set `resi-cache.default-ttl`. A cache configured with no expiry (a caller-supplied diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03735c76..4d905647 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,7 +82,9 @@ unresolved task entries. Use the repository PR template. Summarize the behavior and evidence, identify compatibility impact, and state what was not run. Documentation-only changes -still need link/reference review and the docs contract check. +still need link/reference review and the docs contract check. The shared CI +classifies only its explicit documentation path allowlist as docs-only; changes +to Java comments, templates, workflows, or scripts still run full verification. Be respectful and constructive. This is a best-effort project; assume good intent and keep discussions focused on the code and its evidence. @@ -90,9 +92,10 @@ intent and keep discussions focused on the code and its evidence. ## Maintainers and bus factor ResiCache is currently a **single-maintainer project** — all merges, releases, -and architectural decisions flow through `DavidHLP` (the only committer with -`CODEOWNERS` write access on `main`; `master` is retained only where legacy -workflow references still exist). +and architectural decisions flow through `DavidHLP`, the owner named in +[`.github/CODEOWNERS`](.github/CODEOWNERS). +`main` is the sole maintained branch. CODEOWNERS records review ownership; +repository permissions and branch protection are managed separately on GitHub. **Bus factor: 1** is the current state, not an aspirational promise. Before a `1.0.0` tag, this section must document either a named successor or a diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 3a254758..a9930024 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -1,6 +1,8 @@ # ResiCache Performance Benchmarks -This document captures the baseline JMH throughput numbers for core protection strategies and chain execution in ResiCache. +This document preserves historical JMH throughput measurements and explains +how to run the current benchmarks. The numbers are not current-build results +or enforced performance SLOs. All measurements were produced by the `resicache-bench` module using JMH 1.37 on local development hardware. --- @@ -40,13 +42,13 @@ These are not release SLO gates. ```bash # 1. Install ResiCache core into your local Maven cache -mvn install -DskipTests -Djacoco.skip=true +./mvnw install -DskipTests -Djacoco.skip=true -B # 2. Build the fat-jar -mvn -f resicache-bench/pom.xml clean package -DskipTests +./mvnw -f resicache-bench/pom.xml clean package -DskipTests -B -# 3. Run all benchmarks (~1-2 min) -java -jar resicache-bench/target/resicache-bench.jar -f 1 -wi 1 -i 2 -w 1s -r 1s -rf json -rff results.json +# 3. Run all benchmarks (duration depends on methods and parameter combinations) +java -jar resicache-bench/target/resicache-bench.jar -f 1 -wi 1 -i 2 -w 1s -r 1s -rf json -rff resicache-bench/target/results.json # 4. Run a specific benchmark suite java -jar resicache-bench/target/resicache-bench.jar BloomFilterBenchmark @@ -54,16 +56,42 @@ java -jar resicache-bench/target/resicache-bench.jar BloomFilterBenchmark --- -## Benchmark Results +## Current benchmark names and CI smoke + +Use JDK 21 for both build and execution. List runnable methods with +`java -jar resicache-bench/target/resicache-bench.jar -l`. +Historical identifiers below retain their original scores; renamed methods are +not newly measured results: + +| Historical identifier | Current identifier / scope | +|---|---| +| `springNativeCacheLookup` | `concurrentMapLookup`: direct map access | +| `ttlJitter_concurrent_uniformity` | `ttlJitter_concurrentThroughput`: concurrent computation | +| `cost_1_handler_ttl` through `cost_5_handlers_full` | `cost_1_passthrough` through `cost_5_passthrough`: handler dispatch | + +The current serialization suite additionally measures `storageRoundTrip` at +multiple payload sizes, without Redis network traffic. CI verifies the saved +core candidate, builds this module, and runs only a bounded protocol smoke: + +```bash +java -jar resicache-bench/target/resicache-bench.jar \ + 'CacheSerializationBenchmark.storageRoundTrip' \ + -p payloadBytes=64 -wi 0 -i 1 -r 100ms -f 1 -foe true \ + -jvmArgs '-Xms128m -Xmx256m' +``` + +That smoke proves executable protocol behavior, not throughput thresholds. + +## Historical benchmark results ### Benchmark 1 — Bloom Filter (`BloomFilterBenchmark`) Measures JVM-level `LocalBloomIFilter` cache-penetration gate throughput. | Benchmark | Params (bitSize, hashFunc) | Score (ops/s) | Interpretation | Status | |---|---|---|---|---| -| `bloomMightContain_hit` | 8388608, 3 | **5,850,803** | True-positive fast path (bit-array read only) | **OK** (SLO ≥ 5.0 M ops/s) | -| `bloomMightContain_miss` | 8388608, 3 | **5,670,175** | Definitive negative, DB load bypassed | **OK** | -| `bloomPut` | 8388608, 3 | **4,296,647** | Insertion throughput on cache write | **OK** (SLO ≥ 1.0 M ops/s) | +| `bloomMightContain_hit` | 8388608, 3 | **5,850,803** | True-positive fast path (bit-array read only) | Historical | +| `bloomMightContain_miss` | 8388608, 3 | **5,670,175** | Local filter negative; no DB load measured | Historical | +| `bloomPut` | 8388608, 3 | **4,296,647** | Insertion throughput on cache write | Historical | --- @@ -73,25 +101,25 @@ Measures single-flight leader-follower synchronization under cache breakdown / t | Benchmark | Threads | Score (ops/s) | Interpretation | Status | |---|---|---|---|---| | `noSync` | 1 | **1,787,267** | Baseline: direct CPU-bound loader substitute | Reference | -| `syncLocalOnly_8threads` | 8 | **81,128,097** | Leader-follower coordination with 8 concurrent threads | **OK** | -| `syncContended_32threads` | 32 | **455,369,392** | Worst-case stampede (32 threads hammered on 1 key) | **OK** (gate remains stable) | +| `syncLocalOnly_8threads` | 8 | **81,128,097** | Leader-follower coordination with 8 concurrent threads | Historical | +| `syncContended_32threads` | 32 | **455,369,392** | Worst-case stampede (32 threads hammered on 1 key) | Historical | --- ### Benchmark 3 — TTL Jitter (`TtlJitterBenchmark`) -Measures `TtlHandler` Gaussian random variance calculation to prevent cache avalanche. +Measures `TtlPolicy` Gaussian random variance calculation to prevent cache avalanche. | Benchmark | jitterRatio | Score (ops/s) | Interpretation | Status | |---|---|---|---|---| | `ttlBaseline` | 0.1 | **478,945,558** | Direct unjittered return baseline | Reference | -| `ttlJitter_compute` | 0.1 | **54,806,459** | Gaussian jitter per cache put (~18 ns overhead) | **OK** (SLO ≥ 10.0 M ops/s) | -| `ttlJitter_compute` | 0.2 | **52,697,903** | Configurable ratio variance sweep | **OK** | -| `ttlJitter_concurrent_uniformity` | 0.1 (8 threads) | **5,551,344** | Concurrent jitter computation throughput | **OK** | +| `ttlJitter_compute` | 0.1 | **54,806,459** | Gaussian jitter per cache put (~18 ns overhead) | Historical | +| `ttlJitter_compute` | 0.2 | **52,697,903** | Configurable ratio variance sweep | Historical | +| `ttlJitter_concurrent_uniformity` | 0.1 (8 threads) | **5,551,344** | Concurrent jitter computation throughput | Historical | --- ### Benchmark 4 — Chain Pass-Through (`ChainPassThroughBenchmark`) -Measures ResiCache `ChainEngine` pass-through execution against direct method calls and Spring-native map lookup. +Measures ResiCache `ChainEngine` pass-through execution against direct method calls and a direct concurrent-map lookup. | Benchmark | Score (ops/s) | Avg Latency | Interpretation | |---|---|---|---| @@ -107,9 +135,10 @@ Measures the additive overhead per installed handler in the execution chain. | Benchmark | Installed Handlers | Score (ops/s) | Marginal Delay | |---|---|---|---| | `cost_1_handler_ttl` | 1 (dispatch) | **31,466,289** | ~31.8 ns baseline | -| `cost_2_handlers` | 2 (TTL + Bloom) | **28,108,769** | +3.8 ns | -| `cost_3_handlers` | 3 (TTL + Bloom + Null) | **25,716,085** | +3.3 ns | -| `cost_4_handlers` | 4 (TTL + Bloom + Null + Sync) | **24,240,836** | +2.4 ns | +| `cost_2_handlers` | 2 (dispatch) | **28,108,769** | +3.8 ns | +| `cost_3_handlers` | 3 (dispatch) | **25,716,085** | +3.3 ns | +| `cost_4_handlers` | 4 (dispatch) | **24,240,836** | +2.4 ns | | `cost_5_handlers_full` | 5 (dispatch) | **22,488,609** | +3.2 ns | -*Conclusion: Each additional protection handler adds ~2.5–3.8 ns of chain advancement overhead in memory.* +*In this historical run, additional pass-through dispatch nodes added roughly +2.4–3.8 ns each. These figures do not include real protection work or Redis I/O.* diff --git a/README.md b/README.md index 81d0e07d..13ac767f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,17 @@ 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. -Configure the two Redis clients explicitly when using synchronization: +The core dependency is `io.github.davidhlp:ResiCache` at the checkout's version. +Redisson and Actuator are optional dependencies and are not brought into a +consumer transitively. Add `org.redisson:redisson` at the root POM's version +for the built-in distributed lock, or provide a `LockManager` bean. Add +`spring-boot-starter-actuator` only when health/Actuator integration is needed; +Micrometer Core is already a runtime dependency, but metrics still need an +application registry and `resi-cache.metrics.enabled=true`. + +Start a Redis server before running the application (the example below uses +localhost port 6379). Configure the two Redis clients explicitly when using +synchronization: ```yaml spring: @@ -81,8 +91,9 @@ resi-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: +topology is configured separately. Single-node Redisson has limited fallback +rules; see [`docs/OPERATIONS.md`](docs/OPERATIONS.md#redis-topology-and-configuration). +The application remains responsible for enabling Spring Cache: ```java import io.github.davidhlp.spring.cache.redis.annotation.RedisCacheable; @@ -117,8 +128,21 @@ copied into a small Boot application without inventing a repository type. `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. +single-JVM deployment. The String example needs no business-type allowlist. +For custom cached types, keep the internal namespace and add the application's +value packages explicitly; the library does not derive them automatically: + +```yaml +resi-cache: + serializer: + allowed-package-prefixes: + - io.github.davidhlp + - com.example.dto.* +``` + +Replace `com.example.dto.*` with the package containing your cached values. +Allowlisting a type does not enable polymorphic typing or coerce a returned +value; see [`docs/REFERENCE.md`](docs/REFERENCE.md#serialization-and-compatibility). ## Where to go next diff --git a/README.zh-CN.md b/README.zh-CN.md index acdc7d37..b20a6e90 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -57,7 +57,14 @@ cd ResiCache 消费者应用使用当前检出目录根 `pom.xml` 中的坐标和版本。不要把 Maven Central 上历史的 `0.0.2` 产物当作当前 Boot 4 构建线。 -使用同步能力时,显式配置两个 Redis 命名空间: +核心依赖为 `io.github.davidhlp:ResiCache`,版本取自当前检出的根 `pom.xml`。 +Redisson 与 Actuator 是可选依赖,不会传递到消费者应用。使用内置分布式锁时, +显式添加根 POM 所用版本的 `org.redisson:redisson`,或提供 `LockManager` bean。 +需要健康检查时再添加 `spring-boot-starter-actuator`。Micrometer Core 已是运行时 +依赖,但发布指标仍需要应用的 registry 和 `resi-cache.metrics.enabled=true`。 + +运行应用前先启动 Redis(下例使用本机 6379 端口)。使用同步能力时,显式配置 +两个 Redis 命名空间: ```yaml spring: @@ -74,8 +81,9 @@ resi-cache: ``` Spring Data Redis 命名空间提供缓存 I/O;`resi-cache.redis.*` 提供分布式锁 -使用的 Redisson 部署,两个命名空间不会自动互相复制。是否启用 Spring Cache -仍由应用负责: +使用的 Redisson 部署,拓扑需要分别配置。单节点 Redisson 有有限的配置回退, +详见 [`docs/OPERATIONS.md`](docs/OPERATIONS.md#redis-topology-and-configuration)。 +是否启用 Spring Cache 仍由应用负责: ```java import io.github.davidhlp.spring.cache.redis.annotation.RedisCacheable; @@ -109,8 +117,21 @@ public class Application { `User` 或 `userRepository` 类型。 如果没有分布式锁,`sync=true` 默认失败关闭;只有单 JVM 场景明确接受降级时, -才设置 `resi-cache.sync-lock.local-only=true`。读取自定义缓存类型前,先为 -业务包配置序列化白名单。 +才设置 `resi-cache.sync-lock.local-only=true`。上面的 String 示例无需业务类型 +白名单;缓存自定义类型时,保留内部命名空间并显式加入业务值所在的包,库不会 +自动推导业务包: + +```yaml +resi-cache: + serializer: + allowed-package-prefixes: + - io.github.davidhlp + - com.example.dto.* +``` + +将 `com.example.dto.*` 替换为缓存值所在的包。白名单不会自动启用多态类型信息, +也不会强制转换返回值;详见 +[`docs/REFERENCE.md`](docs/REFERENCE.md#serialization-and-compatibility)。 ## 按问题阅读 diff --git a/SECURITY.md b/SECURITY.md index e4628678..5c767b09 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,14 +2,16 @@ ## Supported versions -ResiCache is **pre-1.0 (0.0.x)**. Only the latest release line receives -security fixes; there are no backports for older `0.0.x` versions. Once `1.0` -ships, a supported-versions table will appear here. +ResiCache is **pre-1.0 (0.0.x)**. Security fixes target the maintained +Boot 4 / Java 21 source line on `main`; published historical Boot 3 artifacts +are not a maintained compatibility line. There are no backports. The build +version alone does not identify the artifact lineage; see +[`COMPATIBILITY.md`](COMPATIBILITY.md). | Version | Supported | |---------|-----------| -| `0.0.x` (latest) | ✅ Security fixes | -| `< latest 0.0.x` | ❌ Upgrade required | +| Current Boot 4 / Java 21 source line | ✅ Best-effort security fixes | +| Historical Boot 3 artifacts / older source revisions | ❌ No maintained backport line | ## Reporting a vulnerability @@ -40,7 +42,10 @@ advisory (with credit, if desired) will follow once the report is confirmed. - **Deserialization is whitelisted.** `SecureJackson` restricts polymorphic 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 + for custom cached types; they are not derived automatically. Use dot-boundary + entries such as `com.example.dto.*` and retain the internal namespace when + replacing the list. Whitelist violations remain fail-fast even with + `fail-on-unknown-type=false`. See [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 @@ -50,3 +55,8 @@ advisory (with credit, if desired) will follow once the report is confirmed. - **Polymorphic Jackson typing is off by default** (`resi-cache.serializer.polymorphic-typing-enabled=false`). Enable it only if you understand the Jackson polymorphic-deserialization attack surface. +- **Legacy JDK migration is resource-bounded.** The decoder preserves the host + input filter and class allowlist, with additional byte/depth/reference/array + limits described in [`docs/REFERENCE.md`](docs/REFERENCE.md#serialization-and-compatibility). +- **Failure diagnostics redact raw keys at WARN/ERROR.** Full throwable stacks + remain at DEBUG and may contain application data; control access to those logs. diff --git a/STABILITY.md b/STABILITY.md index d43d1bd6..36a90457 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -23,7 +23,7 @@ a documented migration path (⚠️ BREAKING entry in | **Wire format** | `{version, payload}` envelope used by `SecureJacksonRedisSerializer` | Envelope is the serialization contract — kept, not loosened. | | **Extension SPI** | `CacheHandler`, `ChainObserver`, `BloomIFilter`, `LockManager`, `LockManager.LockHandle`, `HandlerPriority` | Implementations must satisfy the documented failure, lifecycle, and thread-safety contracts. | | **SPI transitive contract types** | `CacheContext`, `CachePolicyView`, `HandlerResult`, `CacheResult`, `CacheOperation`, `FlowControl`, `ChainContinuation`, `HandlerOrder`, and decision records used by handler signatures | These signature/value types and the `HandlerOrder` numeric ordering contract are part of the supported SPI surface; unrelated fields and implementation classes remain unstable. | -| **Native writer statistics** | Spring Data Redis GET/GET hit/GET miss/PUT/DELETE counters are emitted at the writer boundary; CLEAN carries its exact deleted-key count through stable `CacheResult` | `withStatisticsCollector` rebinds all statistics; lock-wait duration remains unreported (`getLockWaitDuration()` is zero) until an internal observation path can be added without expanding `CacheContext` | +| **Native writer statistics** | Spring Data Redis GET/GET hit/GET miss/PUT/DELETE counters are emitted at the writer boundary; CLEAN carries its exact deleted-key count through stable `CacheResult` | `withStatisticsCollector` rebinds all statistics; lock-wait duration remains unreported (`getLockWaitDuration()` is zero) on the current implementation | ### Compatibility-only annotation attributes @@ -53,12 +53,12 @@ line. | Area | What may change | Example | |------|-----------------|---------| -| **Internal implementation** | Source-level details inside `chain/`, `protection/`, `cache/` | Handler ordering is fixed by `HandlerOrder` enum (gap = 100), but inner algorithm of a specific handler is not contractual | +| **Internal implementation** | Source-level details inside `chain/`, `protection/`, `cache/` | Handler ordering is fixed by `HandlerOrder` enum (including the early-expiration slot between sync and TTL), but inner algorithm of a specific handler is not contractual | | **Default values of properties** | Defaults may be tuned between minor versions | `resi-cache.default-ttl` default may shift toward a better baseline | | **Unstable package layout** | Contents of internal sub-packages and unstable implementation types under `io.github.davidhlp.spring.cache.redis.*` | Stable annotations, configuration keys, wire format, and SPI signature types listed in §1 are excluded. | | **Observability metric names and tags** | Pre-1.0 metric namespace is NOT contractual | A `bloomsift.*` → `resicache.handler.*` rename is allowed pre-1.0 (with ⚠️ BREAKING CHANGELOG) | -| **Diagnostic warnings and logs** | Message text, log levels for startup probes | "whitelist auto-derived from host app root package" WARN may rephrase | -| **Behavior defaults** (e.g. protection preset) | When explicitly opted into a new default via ⚠️ BREAKING CHANGELOG entry | `resi-cache.protection.preset=NONE` (v0.0.2) → `=STANDARD` (v0.0.3) is allowed if flagged breaking | +| **Diagnostic warnings and logs** | Message text, log levels for startup probes | Empty serializer-allowlist WARN may rephrase; application packages are configured explicitly | +| **Behavior defaults** (e.g. protection preset) | When explicitly opted into a new default via ⚠️ BREAKING CHANGELOG entry | Changing a protection default requires a breaking marker; available switches are defined by `RedisProCacheProperties` | | **Internal implementation types** | `MethodMetadataResolver`, `MethodSnapshot`, `ScopedActivation`, `RefreshCancellation`, `LoaderOrchestrator`, `LoadOutcome`, and `ThreadPoolEarlyExpirationExecutor` | Package-private collaborators under the internal `cache` module; not importable extension contracts. | If you depend on items in this section, pin to an exact patch version @@ -68,10 +68,10 @@ If you depend on items in this section, pin to an exact patch version | Type | Replacement | Deprecation/removal | Impact | |---|---|---|---| -| `MethodMetadataResolver` / `MethodSnapshot` | internal resolver lifecycle via auto-configuration | internalized in the Phase 4 cache module | source/binary break for custom resolver implementations | -| `LoaderOrchestrator` / `LoadOutcome` (and the former `DefaultLoadFn`) | `RedisProCache.get(key, loader)` | internalized in the Phase 4 cache module | callers must use the cache API, not loader callbacks | +| `MethodMetadataResolver` / `MethodSnapshot` | internal resolver lifecycle via auto-configuration | internalized under `cache/` | source/binary break for custom resolver implementations | +| `LoaderOrchestrator` / `LoadOutcome` (and the former `DefaultLoadFn`) | `RedisProCache.get(key, loader)` | internalized under `cache/` | callers must use the cache API, not loader callbacks | | `CacheContext` / `HandlerResult` / decision records | documented SPI value surface for handler signatures; implementation-only members may evolve | no removal while `CacheHandler`/`ChainObserver` remain supported | extensions use documented fields and flow values | -| `ThreadPoolEarlyExpirationExecutor` | documented stable SPI only; concrete executor remains internal | internalized in the Phase 4 cache module | custom code uses stable interfaces, not implementation classes | +| `ThreadPoolEarlyExpirationExecutor` | documented stable SPI only; concrete executor remains internal | internalized under `cache/` | custom code uses stable interfaces, not implementation classes | Removal is not activated solely from local source evidence. A published artifact, adopter usage, or external implementation supersedes this default @@ -111,7 +111,8 @@ custom implementation must satisfy. chain completes. Exceptions thrown there are caught and logged by the engine; they never alter the main-chain result. 4. **Ordering**: `@HandlerPriority(HandlerOrder.X)` is the single source of - truth (gap = 100). Unannotated handlers sort last. + truth. The slots include 200 (sync), 250 (early expiration), and 300 (TTL); + unannotated handlers sort last. 5. **Thread safety**: one handler instance is shared across concurrent executions; keep per-call state out of fields (use `CacheContext`). 6. **Nested advancement (optional)**: the engine calls @@ -187,8 +188,8 @@ The machine gate (`public-surface-nested.txt` + | `CacheResult.Outcome` / `FailureKind` | user | stable value semantics | | `CacheContext.InputView` | user | read-only input view for handlers/tests | | `CachePolicyView.Source` | implementation | adapter interface implemented by internal operation models; public only so internals can implement it — do not use from host code | -| `RedisProCacheProperties.*` (9 nested classes) | user | configuration binding surface | -| `CachingEnablementValidation.CachingEnabledValidator` | operator | health/startup probe | +| `RedisProCacheProperties.*` (eight nested classes and `NativeAnnotationMode`) | user | configuration binding surface | +| `CachingEnablementValidation.CachingEnabledValidator` | operator | startup diagnostic; detects interceptor/manager beans, not the presence of an annotation | | `RedisDeploymentValidator$RedisDeploymentChecks` | implementation | internal validation payload | | `LockManager.LockHandle` | extension | stable lock handle (§1) | | `SerializationException.EnvelopeCodec` | operator | envelope helpers for migration tooling | @@ -205,9 +206,12 @@ describe *what 1.0 will mean* and are aspirational until the `1.0.0` tag is cut: 1. **Public surface stability** — §1 + §3 have held across at least one release cycle without breaking changes. -2. **Production-grade ops surface** — Maven Central publish under - `io.github.davidhlp`, CycloneDX SBOM per release, OWASP dependency-check - gate at HIGH/CRITICAL. +2. **Production-grade ops surface** — same-line Maven Central publication under + `io.github.davidhlp`, CycloneDX SBOM per release, and the proposed OWASP + dependency-check gate at HIGH/CRITICAL. Current CI uses Dependency Review + plus Maven-resolved OSV comparisons; it does not provide the named OWASP + gate or an SBOM. See [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md#ci-shape) + for the implemented checks. 3. **Adoption signal** — at least one production adopter listed in `ADOPTERS.md` (created when the first adopter lands). 4. **Bus factor** — a named successor or a documented succession plan diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index eb19dc6c..e0c1ccfb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -16,7 +16,7 @@ Host application ▼ RedisCacheAutoConfiguration ├─ binds RedisProCacheProperties - ├─ scans only the package-private cache runtime + ├─ scans only the internal cache runtime └─ assembles the proxy, cache manager, writer, chain, serializer, and observers │ ├─ Spring Data Redis cache I/O @@ -36,7 +36,9 @@ The operator CLI (`SerializationMigrationCli`) is the second assembly boundary: its context names the internal migration beans by class through `SerializationMigrationOperatorConfiguration` and excludes `RedisCacheAutoConfiguration` by class, so it never assembles the cache/AOP -runtime and needs no enablement gate. +runtime and needs no enablement gate. Its dependencies include a Spring Redis +connection and a Jackson 2 mapper; phase operations are bounded batch work, as +described in [`OPERATIONS.md`](OPERATIONS.md#serialization-rollout-and-rollback-boundary). ## Module ownership @@ -175,7 +177,7 @@ documents. Update the owning current-state document when a behavior changes. | 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 | +| Native-image support is deferred | RuntimeHints/reflection inventory and native verification job are absent | current source and CI; optional task ledger for status | | Public surface is allowlist-driven during 0.x | Java visibility alone would overstate compatibility | `STABILITY.md`; allowlist gate | ## Verification anchors diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d9876702..e46fc2ed 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -7,7 +7,8 @@ which command proves which boundary. ## Environment - JDK 21, matching `pom.xml` and the Maven Enforcer range. -- Maven 3.x or the bundled `./mvnw` wrapper. +- Maven 3.x; the bundled `./mvnw` pins its distribution in + `.mvn/wrapper/maven-wrapper.properties`. - Docker for Redis/Testcontainers integration, Cluster, Sentinel and TLS tests. - OpenSSL and JDK keytool for temporary TLS certificates; Python 3.12+ and GPG for CI contract checks. Linux x86-64 is the CI runner platform. @@ -30,19 +31,24 @@ by the change type. | `bash scripts/ci/check-docs-contracts.sh` | stale contract strings, removed Javadoc references, and required docs guards | | `bash scripts/ci/check-external-consumer.sh` | isolated packaged-JAR consumer, Boot discovery, Redis read/write, optional Redisson and observability paths | | `bash scripts/ci/check-workflows.sh` | actionlint, ShellCheck, SHA pins and CI/release contract regression tests | -| `python3 scripts/ci/pipeline.py reports` | full test execution evidence; rejects skipped/missing integration tests | +| `python3 scripts/ci/pipeline.py reports` | inspect reports immediately after a clean full test run; rejects skipped/missing integration tests | +| `python3 scripts/ci/pipeline.py reports --unit` | inspect reports immediately after `./mvnw -Punit clean test -B`; rejects integration evidence on the unit path | | `./mvnw clean package -DskipTests -B` | packaged artifact without test execution | -| `./mvnw javadoc:javadoc -B` | Javadoc source consistency when public API docs change | +| `./mvnw javadoc:javadoc -B` | Javadoc generation when API comments change; the POM disables doclint, so manually review links and semantics | +| `./mvnw -f resicache-bench/pom.xml clean package -DskipTests -B` | standalone JMH build after installing the matching core; benchmark commands are in [`PERFORMANCE.md`](../PERFORMANCE.md) | `verify` enforces at least 70% line and 40% branch coverage. The no-Docker profile does not establish Redis, Redis Cluster, or Testcontainers behavior. +Local Maven tests disable Ryuk through the root POM; CI explicitly enables it +with `-Dtestcontainers.ryuk.disabled=false`. Registry access and available +container images are therefore part of the CI environment. 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. +- **Unit tests** use Boot-managed JUnit Jupiter and 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 @@ -95,6 +101,10 @@ share `_verify.yml`. Only a PR whose changed paths are all explicitly recognized documentation can skip lint, unit, full build, consumer, benchmark and dependency jobs. Wrapper configuration, CI scripts, unknown paths and classification errors never downgrade verification. Docs and workflow checks always run. +The explicit docs-only paths are in +`scripts/ci/pipeline.py`; Java comments and PR/issue templates are outside that +allowlist. `check-workflows.sh` downloads checksum-pinned Linux actionlint and +ShellCheck into `target/ci-tools` when absent; it needs network access then. Fast checks and full verification run concurrently. The full build uses Ryuk, executes every integration test, enforces 70% line / 40% branch coverage and diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index f5aeab70..9d9659b0 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -24,13 +24,24 @@ lifecycle, secrets injection, and Redis availability. 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. +`sync=true` is used. Redisson's single-node mode falls back to Spring's host +only for a blank library host, to Spring's password for a missing/empty library +password, and to Spring's database when the library database is `0`. Its port, +username and TLS flag use library properties; their Spring counterparts are not +copied. Defaults already supply localhost and port 6379, so setting only +`spring.data.redis.host` does not redirect the default Redisson connection. +Cluster and Sentinel use the library node/ACL configuration directly. + +When Redisson is on the classpath, the library creates a client at startup +unless one already exists, even if no annotation uses synchronization. Omitting +Redisson is the minimal no-lock path; adding it requires a reachable deployment. 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. +end-user request. This override returns the file configuration directly; the +normal library topology/pool settings are not then applied. Redisson is optional until an operation requests distributed synchronization. With no distributed `LockManager`, `sync=true` fails closed by default. @@ -88,8 +99,30 @@ JSON or JDK serializers. A safe adoption flow is: 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 +run automatically at application startup. Run the full class name +`io.github.davidhlp.spring.cache.redis.serialization.migration.SerializationMigrationCli` +on a classpath containing the core JAR and its runtime dependencies. The plain +core JAR is not a self-contained executable. Configure +`spring.data.redis.*`, the serializer allowlist, and a bounded +`resi-cache.serializer.migration.pattern` before invoking it. + +| Phase | Effect | +|---|---| +| `SHADOW_READ` | Default; decodes and validates legacy values without writing. | +| `DUAL_WRITE` | Writes current-envelope sidecars with the source TTL; leaves legacy source bytes in place. | +| `CUTOVER` | Saves legacy backup sidecars, then compares/replaces unchanged source bytes with the current envelope, preserving TTL. | +| `ROLLBACK` | Uses backups; refuses to overwrite source values changed after cutover. | + +The CLI is a bounded batch conversion, not an application write interceptor. +Maintain concurrent application dual writes separately during the rollout. +`max-keys` limits actionable keys per invocation, not all SCAN traffic; +`batch-size` is a SCAN hint. Repeated batches skip completed entries. +`dry-run=true` reports planned work without mutation. Rejected/failed keys +make the CLI exit unsuccessfully. Backups share the source expiry, so rollback +is bounded by backup retention and subsequent writes; it is not a durable +backup service. + +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 diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 4e74ea15..035b35f5 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -85,8 +85,9 @@ derived acceleration layer. Exact operation semantics are in 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. +migration tooling is an operator-directed bounded batch, not an automatic +application dual-write interceptor. It does not run at startup; phase effects +and rollback constraints are in [`OPERATIONS.md`](OPERATIONS.md#serialization-rollout-and-rollback-boundary). ## Product rules @@ -119,7 +120,9 @@ ResiCache does not replace: ## 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. +AOT/native-image certification remains deferred. There is no native verification +job or RuntimeHints inventory in the checked-out source. The local task ledger, +when present, records deferred status; 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 index f747ad30..0c3823d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ translated quick-start companion and does not override the English contract. | 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 | +| Run current benchmarks or review historical evidence | [`PERFORMANCE.md`](../PERFORMANCE.md) | JMH sources, `resicache-bench/pom.xml`, `_bench.yml`, 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 @@ -51,6 +51,7 @@ reference and are independently verified. | 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 | +| Publication status | [`COMPATIBILITY.md`](../COMPATIBILITY.md), [`OPERATIONS.md`](OPERATIONS.md) | Dated Central evidence; source-first until matching publication | Public Maven metadata/POMs and verified candidate checksums | | 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 | diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 174b9a76..0e6d985b 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -28,9 +28,12 @@ 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`; +- `enabled` (auto-configuration gate, declared by annotation rather than a + properties field), `default-ttl`, `key-prefix`, and `transaction-aware`; - `native-annotation-mode` (`SELECTIVE` by default; also `FULL` and `NONE`); -- `protection.*` global/per-mechanism switches; +- `protection.enabled` and the flat per-mechanism fields + `protection.bloom-filter-enabled`, `sync-lock-enabled`, + `early-expiration-enabled`, and `null-value-enabled` under that same group; - `resi-cache.bloom.*`, `resi-cache.early-expiration.*`, `resi-cache.sync-lock.*`, and `resi-cache.redisson.*`; - `redis.*` topology/TLS/deployment fields; @@ -49,7 +52,9 @@ The main configuration groups are: context that resolves it. Its metadata comes from `additional-spring-configuration-metadata.json`. -Configuration is validated at binding time. Do not infer a default from an old +The typed properties model is validated at binding time; the separate +`enabled` and `metrics.enabled` gates are resolved during assembly. Do not infer +a default from an old README snippet when the properties class or generated metadata differs. ## Annotation and operation semantics @@ -72,6 +77,16 @@ README snippet when the properties class or generated metadata differs. TTL and disables Bloom, sync-lock, early-expiration, and null-value handlers; a per-mechanism true cannot re-enable one after global-off. +### Synchronization timeout + +`SyncLockTimeout` resolves the same timeout for locking, follower waits, and +local-only write queues. A positive annotation `syncTimeout` uses that many +seconds; `0` uses the internal 10-second fallback; a negative value uses +`resi-cache.sync-lock.timeout` converted through its configured `unit`. +A converted nonpositive global value falls back to 10 seconds. The annotation +default is 10 seconds, so set `syncTimeout=-1` to inherit the global setting. +This is a coordination bound, not a deadline for the business loader. + ### TTL resolution precedence Effective TTL resolves once, in package-private `TtlPolicy` (`cache/`; see @@ -98,6 +113,8 @@ become `1s`; `1500ms` becomes `2s`). The final TTL from annotation seconds, Duration parameters, or jitter saturates at `Long.MAX_VALUE / 2000` seconds (`4,611,686,018,427,387s`); jitter never reduces a positive TTL below one second. Jitter arithmetic uses overflow-safe addition before applying this final cap. +Jitter applies only when a positive annotation TTL wins; `randomTtl=true` with +`ttl=0` does not jitter the cache-level default. The cap reserves half the signed millisecond range for Redis's epoch clock: Spring's Duration-to-millisecond conversion and Redis's relative-to-absolute expiry addition remain representable while the Redis clock is nonnegative and @@ -131,7 +148,13 @@ 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. +serializer properties. Literal prefixes use `startsWith`; prefer an explicit +package boundary such as `com.example.dto.*`. Retain `io.github.davidhlp` for +internal cached-value metadata when supplying a replacement list. Application +packages are not derived automatically; the startup guard only warns for a +null/empty list. Polymorphic typing is off by default. The serializer uses +Jackson 2 (`com.fasterxml.jackson`); a host Jackson 3 mapper (`tools.jackson`) +does not replace its Jackson 2 fallback. JDK legacy migration rejects inputs larger than 16 MiB and limits deserialization to depth 64, 100,000 references, and arrays of at most 1,000,000 elements before @@ -153,7 +176,7 @@ 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 +The failure metric is an internal bounded diagnostic, not a public per-key alerting API. ## Compatibility and stability references diff --git a/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterBenchmark.java b/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterBenchmark.java index c6166520..63406d90 100644 --- a/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterBenchmark.java +++ b/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/BloomFilterBenchmark.java @@ -8,21 +8,18 @@ /** * Benchmark: Bloom-filter gate (cache-penetration protection). * - *

ResiCache wraps every cache-miss path with a Bloom filter check so that - * keys that were never stored (e.g. random IDs from a DoS scan) are rejected - * before hitting the DB. This benchmark measures: + *

Bloom protection is opt-in and requires data-source membership to be + * seeded or maintained before use. A missing bit can reject a load before + * it reaches the data source. This benchmark measures: * *

    *
  • bloomMightContain_hit – fast path: key IS in the filter (true positive)
  • - *
  • bloomMightContain_miss – key is NOT in filter (definitive negative); loader skipped
  • + *
  • bloomMightContain_miss – key is NOT in filter (definitive negative); no loader is measured
  • *
  • bloomPut – insertion cost of a new key into the filter
  • *
* - *

SLO (from PERFORMANCE.md): - *

    - *
  • bloomMightContain_hit ≥ 5 M ops/s on a single thread (bit-array read only)
  • - *
  • bloomPut throughput ≥ 1 M ops/s (hash + bit-set)
  • - *
+ *

Historical measurements are recorded in PERFORMANCE.md. This suite has no + * enforced throughput SLO; correctness is verified by focused contract tests. */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) diff --git a/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncLockBenchmark.java b/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncLockBenchmark.java index d581fa06..da5bf5a5 100644 --- a/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncLockBenchmark.java +++ b/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SyncLockBenchmark.java @@ -17,15 +17,12 @@ *

We measure three scenarios: *

    *
  • noSync – baseline: all threads call the loader directly with no coordination
  • - *
  • syncLocalOnly – SyncLock in local-only (JVM {@code synchronized}) mode
  • + *
  • syncLocalOnly – SyncLock in explicit local-only coordination mode
  • *
  • syncContended – 32 threads hammering the same key (worst-case stampede)
  • *
* - *

SLO (from PERFORMANCE.md): - *

    - *
  • syncLocalOnly ≤ 2× noSync throughput overhead per operation
  • - *
  • syncContended leader fires exactly once per unique key window
  • - *
+ *

Historical measurements are recorded in PERFORMANCE.md. This suite has no + * enforced throughput SLO; correctness is verified by focused contract tests. */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) diff --git a/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/TtlJitterBenchmark.java b/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/TtlJitterBenchmark.java index 04a84086..0bed9d84 100644 --- a/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/TtlJitterBenchmark.java +++ b/resicache-bench/src/main/java/io/github/davidhlp/spring/cache/redis/cache/TtlJitterBenchmark.java @@ -20,11 +20,8 @@ *

  • ttlBaseline – raw {@code calculateFinalTtl} with no jitter (reference)
  • * * - *

    SLO (from PERFORMANCE.md): - *

      - *
    • ttlJitter_compute ≥ 10 M ops/s – must not meaningfully slow down cache writes
    • - *
    • jitter spread: within configured variance range per default config
    • - *
    + *

    Historical measurements are recorded in PERFORMANCE.md. This suite has no + * enforced throughput SLO; correctness is verified by focused contract tests. */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ActualCacheHandler.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ActualCacheHandler.java index 6c5e9e3d..060fbc42 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ActualCacheHandler.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/ActualCacheHandler.java @@ -144,8 +144,8 @@ private CacheResult processCacheHit(CacheContext context, CachedValue cachedValu context.getCacheName(), context.getRedisKey(), cachedValue.getRemainingTtl()); // 读路径默认不触发写操作,避免写放大。 - // 如需 TTI(读取刷新 TTL),应使用 Spring Data Redis 的 RedisCacheConfiguration.enableTimeToIdle(), - // 由 Redis 6.2+ 的 GETEX 命令实现,无需重写 value。 + // 本 writer 的 cacheTti 参数也不触发 GETEX;启用 Spring 配置中的 TTI + // 不会改变此处的普通 TTL 读语义,见 COMPATIBILITY.md。 byte[] result = encodeForReturn(cachedValue.getValue(), context); return CacheResult.success(result); @@ -157,7 +157,7 @@ private CacheResult processCacheHit(CacheContext context, CachedValue cachedValu *

    null 决策本身属于 {@link CacheValueCodec}(它产出占位字节),但 {@code cacheName} / * {@code key} 上下文只存在于调用点,故 codec 保持无上下文,日志留在两个持上下文的调用点上; * 判定条件用 codec 的 {@link CacheValueCodec#isNullDecision} 而非复制一份,两者不会漂移。 - * 该行是 main 上 {@code NullValueEncoder} 的原样恢复(同一措辞与级别)。 + * DEBUG 行保留既有措辞与级别,当前编码由 CacheValueCodec 统一负责。 */ private byte[] encodeForReturn(Object value, CacheContext context) { if (CacheValueCodec.isNullDecision(value)) { diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheHandlerChainFactory.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheHandlerChainFactory.java index 28475618..bf0973d4 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheHandlerChainFactory.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/CacheHandlerChainFactory.java @@ -64,8 +64,8 @@ class CacheHandlerChainFactory { * {@code ActualCacheHandler} 写入无 TTL 永久缓存(数据陈旧 + 内存泄漏)。 * *

    每条 toggle 两要素:{@code order} 字段(既是短路枚举,又是 disableName 的派生源 —— - * 配置项 {@code resi-cache.protection..enabled} 与 - * {@code HandlerOrder#getDisableName()} 同源,不再各自维护字面量)、 + * 绑定属性使用 {@code resi-cache.protection.-enabled} + * (如 {@code bloom-filter-enabled}),与 {@code HandlerOrder#getDisableName()} 对应)、 * {@code getter} 字段(per-mechanism 覆盖读取,null = 继承 enabled)。 * {@code ProtectionToggleGuardTest} 用反射按 disableName 反查 property 并断言 getter * 读的正是该 property,故新增机制漏配属性时直接红。 @@ -165,7 +165,7 @@ public CacheHandlerChain createChain() { // 2) 构建链 CacheHandlerChain chain = new CacheHandlerChain(engine); - // guide §223b:为每个 enabled AbstractCacheHandler 注入 registry + // 为每个 enabled AbstractCacheHandler 注入已解析的 registry MeterRegistry registry = resolvedMetrics.meterRegistry(); // 3) 收集禁用集合 — 用户自定义 disabled + 总开关 + per-mechanism 覆盖 diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheInterceptor.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheInterceptor.java index feda0753..45866a6b 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheInterceptor.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisCacheInterceptor.java @@ -17,7 +17,7 @@ * ResiCache 缓存拦截器 —— 单一 advice seam。 * *

    继承 Spring {@link CacheInterceptor} 以满足 {@code BeanFactoryCacheOperationSourceAdvisor} - * 对 advice 的硬约束(Spring AOP 6.x 对 {@code CacheInterceptor} 子类有特殊处理:独立 + * 对 advice 的硬约束(Spring Cache 对 {@code CacheInterceptor} 子类有特殊处理:独立 * {@code implements MethodInterceptor} 时 {@code @RedisCacheable} 装配会失效)。本类是 advisor * 直接持有的 advice —— 装配职责与拦截职责收口到同一处。 * diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializerWhitelistStartupGuard.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializerWhitelistStartupGuard.java index 656fbd89..7e1eba83 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializerWhitelistStartupGuard.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/SerializerWhitelistStartupGuard.java @@ -23,9 +23,8 @@ * 用户补回;谓词 {@link #shouldWarn()} package-private 便于单元测试。不动 default * value(默认 {@code [io.github.davidhlp]}),不改 property key,非 breaking 改动。 * - *

    与 GUIDE §4 中"whitelist auto-derive"项配套:该完整项需 host app root package - * BeanFactory 自推导 + 启动 WARN,标 ⚠️ BREAKING;本类是其中 WARN 的 scaffolding, - * 单独可发,留待 auto-derive 落地时复用。 + *

    本守卫只检查列表是否为空,不推导业务包,也不修改白名单。 + * 应用需显式配置业务类型所在的包,并保留框架内部值所需的命名空间。 */ @Slf4j @Component diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java b/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java index 0369c74a..70306345 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java @@ -6,7 +6,7 @@ * Handler 执行顺序枚举 * * 定义标准顺序,确保责任链按正确顺序执行。 - * 间隔 100,便于插入新的 Handler。 + * 顺序槽保留插入空间;提前过期使用同步锁与 TTL 之间的 250 槽。 * *

    本枚举同时是每个 slot 的身份事实源:顺序值、配置禁用名(kebab-case)、观测标签。 */ diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/config/RedisProCacheProperties.java b/src/main/java/io/github/davidhlp/spring/cache/redis/config/RedisProCacheProperties.java index ee934f6c..2fd77988 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/config/RedisProCacheProperties.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/config/RedisProCacheProperties.java @@ -164,7 +164,7 @@ public static class SerializerProperties { private String typeProperty = "@class"; /** 是否启用 Jackson 多态类型信息(默认关闭,更安全) */ private boolean polymorphicTypingEnabled = false; - /** 启动期序列化 pre-flight 探测(guide §115):采样 N keys 检测非 envelope → WARN。默认关闭(opt-in;扫描 Redis 是启动副作用)。 */ + /** 启动期序列化 pre-flight 探测:采样 N keys 检测非 envelope → WARN。默认关闭(opt-in;扫描 Redis 是启动副作用)。 */ private boolean probeEnabled = false; /** pre-flight 探测的采样 key 数上限(默认 100)。 */ @Min(1) diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/serialization/migration/SerializationMigrationCli.java b/src/main/java/io/github/davidhlp/spring/cache/redis/serialization/migration/SerializationMigrationCli.java index 705d5afc..64f3f974 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/serialization/migration/SerializationMigrationCli.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/serialization/migration/SerializationMigrationCli.java @@ -23,7 +23,7 @@ *

    Example: *

    {@code
      * java -cp resicache.jar:app-libs/* \
    - *   io.github...SerializationMigrationCli \
    + *   io.github.davidhlp.spring.cache.redis.serialization.migration.SerializationMigrationCli \
      *   --spring.data.redis.host=localhost \
      *   --resi-cache.serializer.migration.phase=SHADOW_READ
      * }
    From 0302ed65ee83e5d2e42d8e17819f9106604740f7 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 09:47:29 +0800 Subject: [PATCH 2/8] docs: clarify migration progress and startup failure boundaries --- STABILITY.md | 6 ++++-- docs/OPERATIONS.md | 8 +++++++- docs/REFERENCE.md | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/STABILITY.md b/STABILITY.md index 36a90457..96c32570 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -111,8 +111,10 @@ custom implementation must satisfy. chain completes. Exceptions thrown there are caught and logged by the engine; they never alter the main-chain result. 4. **Ordering**: `@HandlerPriority(HandlerOrder.X)` is the single source of - truth. The slots include 200 (sync), 250 (early expiration), and 300 (TTL); - unannotated handlers sort last. + truth. Use `HandlerOrder.SYNC_LOCK`, `HandlerOrder.EARLY_EXPIRATION`, and + `HandlerOrder.TTL`; their ordering values are defined only in + [`HandlerOrder.java`](src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java). + Unannotated handlers sort last. 5. **Thread safety**: one handler instance is shared across concurrent executions; keep per-call state out of fields (use `CacheContext`). 6. **Nested advancement (optional)**: the engine calls diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 9d9659b0..c7b08ae5 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -116,7 +116,13 @@ core JAR is not a self-contained executable. Configure The CLI is a bounded batch conversion, not an application write interceptor. Maintain concurrent application dual writes separately during the rollout. `max-keys` limits actionable keys per invocation, not all SCAN traffic; -`batch-size` is a SCAN hint. Repeated batches skip completed entries. +`batch-size` is a SCAN hint. Successful non-dry-run write phases can skip +completed entries while their stored state remains valid. `SHADOW_READ` and +`dry-run=true` persist neither completion state nor a SCAN cursor; repeating +an invocation with the same pattern and `max-keys` can select the same eligible +legacy keys again. To validate the whole keyspace, use disjoint bounded +patterns or a `max-keys` large enough to cover all eligible keys. Reaching the +limit does not establish that the remaining keys were validated. `dry-run=true` reports planned work without mutation. Rejected/failed keys make the CLI exit unsuccessfully. Backups share the source expiry, so rollback is bounded by backup retention and subsequent writes; it is not a durable diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 0e6d985b..ef01a436 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -151,8 +151,11 @@ and `.*` dot-boundary behavior are defined by `WhitelistPolicy` and the serializer properties. Literal prefixes use `startsWith`; prefer an explicit package boundary such as `com.example.dto.*`. Retain `io.github.davidhlp` for internal cached-value metadata when supplying a replacement list. Application -packages are not derived automatically; the startup guard only warns for a -null/empty list. Polymorphic typing is off by default. The serializer uses +packages are not derived automatically. An empty list can reach the startup +guard's warning at `ApplicationReadyEvent`. A `null` list instead fails +serializer construction (`WhitelistPolicy` calls `List.copyOf(null)`), +preventing the default runtime from starting before that event. +Polymorphic typing is off by default. The serializer uses Jackson 2 (`com.fasterxml.jackson`); a host Jackson 3 mapper (`tools.jackson`) does not replace its Jackson 2 fallback. From 1819ad874a22dedb6ac086281e2a091cf6ea7924 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 09:56:56 +0800 Subject: [PATCH 3/8] docs: require verified type preservation for POJO migration --- docs/OPERATIONS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index c7b08ae5..8fc847fb 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -106,6 +106,17 @@ core JAR is not a self-contained executable. Configure `spring.data.redis.*`, the serializer allowlist, and a bounded `resi-cache.serializer.migration.pattern` before invoking it. +For legacy custom POJOs whose concrete types must survive migration, explicitly +set `resi-cache.serializer.polymorphic-typing-enabled=true` in both the CLI +and the consuming application, with a strict allowlist limited to trusted value +packages and the required internal namespace. The default `false` can serialize +a decoded legacy DTO without type metadata; later reads may return a +`LinkedHashMap` instead of that DTO and fail a typed cache call. Before `CUTOVER`, +deserialize representative new-envelope sidecars with the application's reader +configuration and verify their concrete types and typed cache-hit behavior. +If type preservation cannot be safely verified, regenerate those custom values +through the application instead of converting them with the migration CLI. + | Phase | Effect | |---|---| | `SHADOW_READ` | Default; decodes and validates legacy values without writing. | From d974f7466ffa7fb13019ddd004d565567a07b414 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 10:26:56 +0800 Subject: [PATCH 4/8] docs: correct payload typing and handler ordering guidance --- docs/OPERATIONS.md | 21 ++++++++++--------- .../cache/redis/chain/HandlerOrder.java | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 8fc847fb..01dc6ce4 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -106,16 +106,17 @@ core JAR is not a self-contained executable. Configure `spring.data.redis.*`, the serializer allowlist, and a bounded `resi-cache.serializer.migration.pattern` before invoking it. -For legacy custom POJOs whose concrete types must survive migration, explicitly -set `resi-cache.serializer.polymorphic-typing-enabled=true` in both the CLI -and the consuming application, with a strict allowlist limited to trusted value -packages and the required internal namespace. The default `false` can serialize -a decoded legacy DTO without type metadata; later reads may return a -`LinkedHashMap` instead of that DTO and fail a typed cache call. Before `CUTOVER`, -deserialize representative new-envelope sidecars with the application's reader -configuration and verify their concrete types and typed cache-hit behavior. -If type preservation cannot be safely verified, regenerate those custom values -through the application instead of converting them with the migration CLI. +For legacy custom POJOs, use a strict allowlist limited to trusted value +packages and the required internal namespace. `VersionEnvelope.payload` embeds +the top-level DTO's type through field-level `@JsonTypeInfo`, independently of +`resi-cache.serializer.polymorphic-typing-enabled`; keep that global switch at +its default `false` for this migration path. Global default typing is not a +prerequisite for preserving the envelope payload's concrete type. +Before `CUTOVER`, deserialize representative new-envelope sidecars with the +application's reader configuration and verify concrete types, nested values, +and typed cache-hit behavior. If the required value structure cannot safely +round-trip, regenerate those values through the application instead of +converting them with the migration CLI. | Phase | Effect | |---|---| diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java b/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java index 70306345..4b6f73a4 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/chain/HandlerOrder.java @@ -6,7 +6,7 @@ * Handler 执行顺序枚举 * * 定义标准顺序,确保责任链按正确顺序执行。 - * 顺序槽保留插入空间;提前过期使用同步锁与 TTL 之间的 250 槽。 + * 顺序槽保留插入空间;{@link #EARLY_EXPIRATION} 位于 {@link #SYNC_LOCK} 与 {@link #TTL} 之间。 * *

    本枚举同时是每个 slot 的身份事实源:顺序值、配置禁用名(kebab-case)、观测标签。 */ From 23f2320f2c5035b3a1249af0aa6158d8179203a5 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 11:08:27 +0800 Subject: [PATCH 5/8] docs: constrain migration to compatible runtime cache values --- docs/OPERATIONS.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 01dc6ce4..13814ba7 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -106,17 +106,25 @@ core JAR is not a self-contained executable. Configure `spring.data.redis.*`, the serializer allowlist, and a bounded `resi-cache.serializer.migration.pattern` before invoking it. -For legacy custom POJOs, use a strict allowlist limited to trusted value -packages and the required internal namespace. `VersionEnvelope.payload` embeds -the top-level DTO's type through field-level `@JsonTypeInfo`, independently of -`resi-cache.serializer.polymorphic-typing-enabled`; keep that global switch at -its default `false` for this migration path. Global default typing is not a -prerequisite for preserving the envelope payload's concrete type. -Before `CUTOVER`, deserialize representative new-envelope sidecars with the -application's reader configuration and verify concrete types, nested values, -and typed cache-hit behavior. If the required value structure cannot safely -round-trip, regenerate those values through the application instead of -converting them with the migration CLI. +The CLI converts serializer bytes; it does not construct ResiCache's runtime +cache structure. Normal writes store a `CachedValue` wrapper containing the +chain value and expiry/refresh metadata, while `ActualCacheHandler` treats +values without that wrapper as cache misses. A directly serialized legacy +POJO or String can retain its concrete type after conversion and still be +unusable as a ResiCache cache entry. + +For ResiCache keys, use the CLI only when the decoded legacy value already +has the complete compatible `CachedValue` structure, including its nested +value/envelope representation and metadata. Regenerate bare DTO/String values +or incompatible wrappers through normal application cache writes instead of +using `CUTOVER` to convert them. Use a strict allowlist limited to trusted +value packages and the required internal namespace. Field-level type metadata +does not require global default typing, and enabling that switch does not +supply the missing runtime wrapper. +Before `CUTOVER`, verify representative converted sidecars against the +application's actual cache read path, including wrapper structure, metadata, +nested values, concrete types and typed cache-hit behavior. Successful CLI +decoding or serializer round-tripping alone is insufficient. | Phase | Effect | |---|---| From cd38eb7c385f76758910744f7a7f78721dcf0c94 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 11:25:40 +0800 Subject: [PATCH 6/8] docs: clarify publication ownership and safe allowlist prefixes --- README.md | 2 +- README.zh-CN.md | 2 +- SECURITY.md | 2 +- docs/OPERATIONS.md | 9 +++++---- docs/README.md | 4 ++-- docs/REFERENCE.md | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 13ac767f..0b97318a 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ value packages explicitly; the library does not derive them automatically: resi-cache: serializer: allowed-package-prefixes: - - io.github.davidhlp + - io.github.davidhlp.* - com.example.dto.* ``` diff --git a/README.zh-CN.md b/README.zh-CN.md index b20a6e90..5a966975 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -125,7 +125,7 @@ public class Application { resi-cache: serializer: allowed-package-prefixes: - - io.github.davidhlp + - io.github.davidhlp.* - com.example.dto.* ``` diff --git a/SECURITY.md b/SECURITY.md index 5c767b09..3bfbd156 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -43,7 +43,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; they are not derived automatically. Use dot-boundary - entries such as `com.example.dto.*` and retain the internal namespace when + entries such as `com.example.dto.*` and `io.github.davidhlp.*` when replacing the list. Whitelist violations remain fail-fast even with `fail-on-unknown-type=false`. See [configuration and serialization reference](docs/REFERENCE.md#serialization-and-compatibility). diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 13814ba7..6d1f74be 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -201,10 +201,11 @@ remain enabled; Dependabot alerts and security updates complement CI scans. Workflow files describe intended checks; remote branch/environment settings must also be verified when configuring the repository. -The Boot 4 / Java 21 line remains source-first until a matching Central artifact -is actually published and verified. A local install, candidate bundle or green -CI run is not a publication claim. Public publication is triggered separately -by a maintainer's new version tag after configuring namespace/signing access. +Current publication status and its verification evidence are owned by +[`COMPATIBILITY.md`](../COMPATIBILITY.md). A local install, candidate bundle or +green CI run is not a publication claim. Public publication is triggered +separately by a maintainer's new version tag after configuring +namespace/signing access. ## Backup, restore, and hosted-service limits diff --git a/docs/README.md b/docs/README.md index 0c3823d5..63791182 100644 --- a/docs/README.md +++ b/docs/README.md @@ -48,10 +48,10 @@ reference and are independently verified. | 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 | +| Runtime configuration, migration, release/publication procedures, 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 | -| Publication status | [`COMPATIBILITY.md`](../COMPATIBILITY.md), [`OPERATIONS.md`](OPERATIONS.md) | Dated Central evidence; source-first until matching publication | Public Maven metadata/POMs and verified candidate checksums | +| Publication status | [`COMPATIBILITY.md`](../COMPATIBILITY.md) | Dated Central evidence; source-first until matching publication | Public Maven metadata/POMs and verified candidate checksums | | 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 | diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index ef01a436..d63c00cc 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -149,7 +149,7 @@ ResiCache stores an internal `{version, payload}` envelope through `GenericJackson2JsonRedisSerializer` or `JdkSerializer`. The whitelist default and `.*` dot-boundary behavior are defined by `WhitelistPolicy` and the serializer properties. Literal prefixes use `startsWith`; prefer an explicit -package boundary such as `com.example.dto.*`. Retain `io.github.davidhlp` for +package boundary such as `com.example.dto.*`. Retain `io.github.davidhlp.*` for internal cached-value metadata when supplying a replacement list. Application packages are not derived automatically. An empty list can reach the startup guard's warning at `ApplicationReadyEvent`. A `null` list instead fails From 36c6e8ae3a90e29890abf5436818b6345ca2ed4e Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 13:38:59 +0800 Subject: [PATCH 7/8] docs: clarify migration selection and workload limits --- docs/ARCHITECTURE.md | 2 +- docs/OPERATIONS.md | 17 ++++++++++++++--- docs/PRODUCT.md | 2 +- docs/REFERENCE.md | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e0c1ccfb..51812e05 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -37,7 +37,7 @@ boundary: its context names the internal migration beans by class through `SerializationMigrationOperatorConfiguration` and excludes `RedisCacheAutoConfiguration` by class, so it never assembles the cache/AOP runtime and needs no enablement gate. Its dependencies include a Spring Redis -connection and a Jackson 2 mapper; phase operations are bounded batch work, as +connection and a Jackson 2 mapper; phase operations have selection limits rather than hard scan/attempt caps, as described in [`OPERATIONS.md`](OPERATIONS.md#serialization-rollout-and-rollback-boundary). ## Module ownership diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 6d1f74be..c3a4a2aa 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -133,10 +133,21 @@ decoding or serializer round-tripping alone is insufficient. | `CUTOVER` | Saves legacy backup sidecars, then compares/replaces unchanged source bytes with the current envelope, preserving TTL. | | `ROLLBACK` | Uses backups; refuses to overwrite source values changed after cutover. | -The CLI is a bounded batch conversion, not an application write interceptor. +The CLI is an operator-directed conversion, not an application write interceptor. Maintain concurrent application dual writes separately during the rollout. -`max-keys` limits actionable keys per invocation, not all SCAN traffic; -`batch-size` is a SCAN hint. Successful non-dry-run write phases can skip +`max-keys` limits the engine's `selected` count, not scanned keys or all +attempts. In forward phases, selection occurs only after legacy decoding and +serialization succeed; corrupt envelopes and decode/serialization failures +increment `failed` without consuming that limit. Failures after selection do +consume it. A run can therefore GET, decode and report every malformed +matching key even with a small `max-keys`. Valid current envelopes and already +completed entries also do not consume the limit. `batch-size` is only a SCAN +hint. There is no separate hard scan or attempt cap in the CLI. +Do not use `max-keys` alone as a production workload budget: restrict the +matched key population independently and apply an external execution deadline +when required. A match pattern does not bound Redis SCAN work, and externally +interrupted runs must be treated as incomplete. + Successful non-dry-run write phases can skip completed entries while their stored state remains valid. `SHADOW_READ` and `dry-run=true` persist neither completion state nor a SCAN cursor; repeating an invocation with the same pattern and `max-keys` can select the same eligible diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 035b35f5..0954109e 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -85,7 +85,7 @@ derived acceleration layer. Exact operation semantics are in 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 an operator-directed bounded batch, not an automatic +migration tooling is operator-directed, with no hard scan/attempt cap, rather than an automatic application dual-write interceptor. It does not run at startup; phase effects and rollback constraints are in [`OPERATIONS.md`](OPERATIONS.md#serialization-rollout-and-rollback-boundary). diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index d63c00cc..974b5987 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -168,7 +168,7 @@ With `serializer.fail-on-unknown-type=false`, ordinary JSON decoding failures return a miss; whitelist violations remain fail-fast regardless of this setting. WARN reports only exception types, with detailed exceptions at DEBUG. -Use the bounded shadow-read → dual-write → cutover migration described in +Use the shadow-read → dual-write → cutover migration and workload limits 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. From d4f561089b144b4ba2d7daf6d1bd475e468b2bd6 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Thu, 8 Oct 2026 20:13:51 +0800 Subject: [PATCH 8/8] docs: document migration validation and sidecar safeguards --- docs/OPERATIONS.md | 56 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index c3a4a2aa..9663e5a7 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -106,6 +106,24 @@ core JAR is not a self-contained executable. Configure `spring.data.redis.*`, the serializer allowlist, and a bounded `resi-cache.serializer.migration.pattern` before invoking it. +Require `resi-cache.serializer.fail-on-unknown-type=true` in the migration +CLI configuration. With `false`, unsupported envelope versions or ordinary +payload-binding failures can deserialize to `null`; the engine still counts +them as `envelopes` instead of `failed`, and the CLI can exit successfully. +Do not approve a keyspace based on such a permissive run; rerun validation +with fail-fast mode and verify the application's actual reads. + +Before any write phase, reserve collision-free `shadow-suffix` and +`backup-suffix` namespaces under `resi-cache.serializer.migration`. Preflight +the complete intended source set: no application source key may end in either +suffix, and each derived `` destination must be absent or +verified as a sidecar belonging to this migration. Stop on unrelated or +unverifiable destinations. The CLI does not enforce this reservation: +`writeSidecar` overwrites differing destination bytes with UPSERT, and forward +scans silently skip keys ending in either suffix. A dry run is not a collision +check. Prevent application writers from creating keys in the reserved +namespaces throughout migration and cleanup. + The CLI converts serializer bytes; it does not construct ResiCache's runtime cache structure. Normal writes store a `CachedValue` wrapper containing the chain value and expiry/refresh metadata, while `ActualCacheHandler` treats @@ -131,7 +149,7 @@ decoding or serializer round-tripping alone is insufficient. | `SHADOW_READ` | Default; decodes and validates legacy values without writing. | | `DUAL_WRITE` | Writes current-envelope sidecars with the source TTL; leaves legacy source bytes in place. | | `CUTOVER` | Saves legacy backup sidecars, then compares/replaces unchanged source bytes with the current envelope, preserving TTL. | -| `ROLLBACK` | Uses backups; refuses to overwrite source values changed after cutover. | +| `ROLLBACK` | Uses backups; rejects differing existing source bytes but recreates missing sources from legacy backups. | The CLI is an operator-directed conversion, not an application write interceptor. Maintain concurrent application dual writes separately during the rollout. @@ -147,17 +165,43 @@ Do not use `max-keys` alone as a production workload budget: restrict the matched key population independently and apply an external execution deadline when required. A match pattern does not bound Redis SCAN work, and externally interrupted runs must be treated as incomplete. - Successful non-dry-run write phases can skip + +Successful non-dry-run write phases can skip completed entries while their stored state remains valid. `SHADOW_READ` and `dry-run=true` persist neither completion state nor a SCAN cursor; repeating an invocation with the same pattern and `max-keys` can select the same eligible legacy keys again. To validate the whole keyspace, use disjoint bounded patterns or a `max-keys` large enough to cover all eligible keys. Reaching the limit does not establish that the remaining keys were validated. -`dry-run=true` reports planned work without mutation. Rejected/failed keys -make the CLI exit unsuccessfully. Backups share the source expiry, so rollback -is bounded by backup retention and subsequent writes; it is not a durable -backup service. +`dry-run=true` prevents mutation. Forward-phase reports expose decoded legacy +counts, but rollback dry runs do not expose the private selected/planned +counter: `written` remains zero, and `scanned` includes pending restorations +and already-restored/no-op backups. They cannot establish how many keys would +be restored. Independently compare the scoped backups and source state before +authorizing rollback. Reported rejected/failed keys make the CLI exit +unsuccessfully; this requires the fail-fast validation configuration above. + +Rollback does not protect deletions: if a source is missing while its backup +remains, it uses SET_IF_ABSENT to recreate the legacy value, which may resurrect +a deliberately evicted, stale entry. Quiesce application writes and evictions +for the affected keys before rollback, and keep them paused until it completes. +Quiescence alone cannot identify earlier intentional deletions: reconcile +those against the backup inventory and exclude their backups from rollback +before running it. Existing changed source bytes are rejected; absence is not +treated as evidence of an intentional deletion. + +Sidecars inherit the source TTL at creation. A persistent source produces +persistent shadows/backups; neither cutover nor rollback deletes them, and the +CLI has no cleanup phase. Budget for their storage until explicitly removed. +After validated cutover and the agreed rollback window, or after a completed +rollback whose result has been verified, stop migration invocations and retire +application dual writes to these sidecars. Inventory the reserved namespaces, +review the exact sidecar keys belonging to this migration, then manually +remove only that approved set in controlled batches. Preserve backups while +rollback is still required; do not use a broad suffix-only deletion that could +include unrelated keys. Backups for expiring sources may expire before +rollback; backups for persistent sources have no automatic retention bound. +Sidecars are not a durable backup service. 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.