chore: initialize from backend template 0a6dd0e
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
# adapter:outbound:cache-redis — 설계 결정 참조
|
||||
|
||||
캐시/Redis 기술 capability 아웃바운드 모듈. 패키지 루트:
|
||||
`dev.caskeleton.adapter.outbound.cache`. `application-core`의 provider-neutral cache contract를
|
||||
구현할 수 있는 경계와 Redis physical key/atomic-program 기반을 소유한다.
|
||||
|
||||
허용/금지 의존 정책은 `src/config/architecture/modules.json`의
|
||||
`adapter-outbound-cache-redis` 항목이 SSOT다. 상세 목표와 미구현 단계는
|
||||
`docs/superpowers/specs/2026-07-26-redis-production-capability-design.md`에 있다.
|
||||
|
||||
## 현재 readiness
|
||||
|
||||
readiness는 서로 다른 세 가지 질문이며 하나로 합치면 안 된다. "코드가 있다"는 "Spring이
|
||||
조립한다"가 아니고, 그 둘 다 "실서버에서 증명됐다"가 아니다. 이 표를 한 축으로 읽으면 아직
|
||||
존재하지 않는 wiring을 제공 기능으로 오독하게 된다.
|
||||
|
||||
| 축 | 뜻 | 증거 |
|
||||
| --- | --- | --- |
|
||||
| **API 구현** | 타입·정책·contract test가 존재한다 | `:adapter:outbound:cache-redis:test` |
|
||||
| **Spring composition 구현** | `APP_REDIS_ENABLED=true`에서 실제 bean이 조립된다 | `RedisSdkAutoConfigurationTest` |
|
||||
| **실서버 qualification** | 지원 topology·버전에서 실제 서버로 증명됐다 | `redisTopologyTest` lane evidence |
|
||||
|
||||
| Capability | API 구현 | Spring composition 구현 | 실서버 qualification |
|
||||
| --- | --- | --- | --- |
|
||||
| Redis SDK typed API (`…cache.redis.sdk`) | 있음 | settings bind + validate 까지만 | 없음 |
|
||||
| Topology client / connection lifecycle | 없음 | 없음 | 없음 |
|
||||
| cache / session / idempotency / rate limit / lease semantic port | 없음 | 없음 | 없음 |
|
||||
| role-aware health·readiness contributor | 없음 | 없음 | 없음 |
|
||||
|
||||
즉 현재 `APP_REDIS_ENABLED=true`가 하는 일은 `RedisSdkSettings`를 bind하고 cross-field 규칙을
|
||||
fail-fast로 검증하는 것까지다. client, connection, gateway, semantic adapter, health contributor는
|
||||
아직 조립되지 않는다. 남은 단계와 순서는
|
||||
`docs/superpowers/plans/2026-08-10-redis-optionality-and-composition.md`에 있다.
|
||||
|
||||
readiness registry에도 `selected` card가 없으므로 Redis R2 release claim은 없다. 아래 절들은
|
||||
이전 세대 semantic adapter 세트의 설계 결정을 기록한 것이며, 그 코드는 현재 이 leaf에 없다.
|
||||
복구 범위는 위 plan의 Phase E가 소유한다.
|
||||
|
||||
모듈은 Lettuce connection lifecycle,
|
||||
finite command timeout, reconnect replay 차단, finite request queue/admission, positive/negative
|
||||
TTL, absolute soft/hard expiry, deterministic bounded TTL jitter, digest-protected v2 binary
|
||||
envelope, HMAC physical key,
|
||||
invalidation, closed-catalog
|
||||
`EVALSHA -> NOSCRIPT -> SCRIPT LOAD -> digest verify -> EVALSHA` recovery를 제공한다.
|
||||
`app.cache.redis.client-mode=external`이면 프로젝트가 제공한 `RedisClient` 호환 경로를 사용하고
|
||||
managed connection을 생성하지 않는다.
|
||||
|
||||
명시적으로 최소 지원 Redis 7.2 image를 띄워 실행하는 standalone lane이 실제 expiry,
|
||||
compare-and-delete, cache `NX`, observation-token compare-and-replace, 세 rate-limit 프로그램,
|
||||
각 프로그램의 exact-boundary/denial-no-consume, clock-regression state 불변,
|
||||
token refill remainder와 malformed hash 분류를 검증한다. TLS named-user ACL에서 semantic
|
||||
readiness의 `SCRIPT LOAD`/대표 명령 거부 증거는 있지만 Sentinel/Cluster, credential rotation,
|
||||
restart/fault/eviction과 capability 전체의 운영 증거가 완성되지 않았으므로 R2가 아니다.
|
||||
|
||||
## Role policy와 health 경계
|
||||
|
||||
Canonical role binding은 startup에 다음 정책을 fail-closed로 검증한다.
|
||||
|
||||
- `CACHE`: `required=false`, `expected-eviction=allkeys-lfu|allkeys-lru`
|
||||
- `COORDINATION`: `required=true`, `expected-eviction=noeviction`
|
||||
- `SESSION`: `required=true`, `expected-eviction=noeviction`
|
||||
|
||||
Redis 모듈은 바인딩된 role router만 사용해 capability-aware semantic probe를 수행한다. PING만으로
|
||||
ready를 선언하지 않는다. 모든 plan은 `ca-health:` namespace의 bounded opaque nonce key에 먼저
|
||||
5초 TTL을 부여하고 SET/GET round trip을 검증한다. 선택 capability별 대표 프로그램은 다음과 같다.
|
||||
|
||||
- cache: `SET_IF_ABSENT_WITH_TTL`
|
||||
- rate limit: `RATE_FIXED_WINDOW_V2`
|
||||
- request-replay idempotency: `IDEMPOTENCY_CLAIM_V1`
|
||||
- efficiency lease: `LEASE_ACQUIRE_V1`
|
||||
- session: `SESSION_CREATE_V1`
|
||||
|
||||
대표 프로그램은 catalog digest의 `EVALSHA` 경로와 bounded result schema를 검증한다. 별도의
|
||||
catalog-owned `semantic-capability-acl-v1` 프로그램은 Redis Lua API의
|
||||
`redis.acl_check_cmd`로 대표 프로그램의 exact ACL command/key surface와 `SCRIPT LOAD` 권한을
|
||||
비변경 방식으로 확인하고, `redis.REDIS_VERSION_NUM`으로 명시적인 Redis `>=7.2` policy gate를
|
||||
먼저 적용한다. 두 Lua API 상수/함수는 Redis 7.0부터 제공되지만 이 템플릿이 지원을 선언하는
|
||||
minimum은 7.2다. runtime identity에 허용해야 하는 probe key pattern은
|
||||
`~ca-health:*`다. probe는 성공/실패와 무관하게 best-effort cleanup을 수행하고, cleanup이
|
||||
거절돼도 모든 생성 key는 최대 5초 안에 만료된다.
|
||||
|
||||
각 role은 startup에 full semantic qualification을 완료한 관측을 seed한다. 이후 health scrape는
|
||||
`APP_REDIS_SEMANTIC_PROBE_MINIMUM_INTERVAL`(기본 5초) 동안 같은 관측을 재사용하고 role별
|
||||
single-flight로만 refresh한다. refresh follower는 기다리지 않으며 15초 기본
|
||||
`APP_REDIS_SEMANTIC_PROBE_MAXIMUM_STALENESS` 안에서는 이전 관측과 `semanticObservedAt`,
|
||||
`semanticAgeMillis`, `semanticStale=true`를 반환한다. 최대 staleness를 넘으면
|
||||
`SEMANTIC_OBSERVATION_STALE`로 fail closed한다. eligibility와 age는 monotonic ticker를 사용해
|
||||
wall-clock jump의 영향을 받지 않는다.
|
||||
|
||||
연결 가능한 optional/required role의 ACL, Redis 7.2 minimum, program result/schema mismatch는
|
||||
모두 startup-fatal이다. 명확히 분류된 temporary connect/PING 실패만 optional CACHE를 dormant
|
||||
route와 `COMMAND_UNAVAILABLE` 관측으로 시작하게 한다. health-triggered single-flight reconnect는
|
||||
후보에 PING과 full semantic qualification을 모두 수행한 뒤에만 기존 router를 swap하며,
|
||||
required COORDINATION/SESSION과 auth/TLS/material/unknown failure는 계속 fail closed한다.
|
||||
|
||||
Cluster에서 same-slot probe가 증명하는 범위는 해당 hash slot owner 한 노드뿐이다. 이 결과를
|
||||
cluster 전체 노드나 failover target의 version/ACL/program 호환성 증거로 확대 해석하면 안 되며,
|
||||
운영 promotion 전 별도의 cluster-wide 외부 conformance가 필요하다.
|
||||
|
||||
`shared-contract`의 framework-neutral snapshot은 role, 선택된 capability, availability,
|
||||
sanitized reason, semantic observation metadata와 expected eviction만 제공한다. semantic success, read/write failure,
|
||||
program ACL denial, program failure, admission saturation, recent command failure, closed route,
|
||||
command unavailable, probe-in-progress, stale observation은 서로 다른 bounded reason이다. endpoint, deployment ID, key/value,
|
||||
username, credential/trust reference와 server exception은 health detail에 노출하지 않는다.
|
||||
Actuator 타입과 health-group 소유권은 `app-bootstrap`에 있다. CACHE 장애는
|
||||
`redisOptional`의 `state=DEGRADED` detail로만 나타나고 readiness를 내리지 않는다.
|
||||
COORDINATION/SESSION 장애는 `redisRequired`를 `DOWN`으로 만들며, 어떤 Redis contributor도
|
||||
liveness에는 포함되지 않는다. role binding이 없으면 Redis client 생성과 Redis health
|
||||
contributor 생성은 모두 0이다.
|
||||
|
||||
이 runtime은 Redis `CONFIG GET/SET` 권한을 요구하거나 노출하지 않는다. 따라서
|
||||
`expected-eviction` 검증은 설정 의도에 대한 startup 검증이며 실제 server의
|
||||
`maxmemory-policy`를 증명하지 않는다. Snapshot/health detail은 이 한계를
|
||||
`CONFIGURED_EXPECTATION_ONLY`로, 외부 증거 상태를
|
||||
`externalEvictionAttestation=INCOMPLETE`로 명시한다. 운영 readiness를 더 강하게 만들려면 배포
|
||||
파이프라인의 외부 conformance job 또는 서명된 operator attestation으로 effective policy를
|
||||
검증해야 한다. semantic probe는 runtime `CONFIG`/`ACL` 조회나 변경 권한을 요구하지 않는다.
|
||||
|
||||
## Distributed edge rate limit
|
||||
|
||||
`shared-contract`의 `EdgeRateLimitPort` 뒤에서 fixed window, sliding-window counter, token bucket을
|
||||
정확히 하나의 versioned Lua 실행으로 평가한다. 세 프로그램은 Redis `TIME`을 한 번만 읽고, server
|
||||
time, bounded clock-regression clamp, denial-no-consume, finite state TTL과 정확히 7개 필드인 응답
|
||||
계약을 공유한다. Redis `TYPE`의 status-table/string 차이를 정규화하고 malformed hash field는
|
||||
typed incompatibility로 닫는다. Token bucket은 refill division remainder를 상태로 보존해 호출
|
||||
빈도에 따라 quota가 달라지지 않는다. Sliding counter만 algorithm certainty가 approximate이고
|
||||
나머지는 certain이다.
|
||||
|
||||
모든 closed program manifest의 `minimumRedisVersion`은 실제 minimum qualification lane과 같은
|
||||
7.2다. 더 낮은 Redis 버전은 별도 service lane이 추가되기 전까지 호환을 주장하지 않는다.
|
||||
|
||||
## Redis-backed HTTP session
|
||||
|
||||
`redis-session` readiness card는 standalone을 선택 topology로 하는 implemented candidate다.
|
||||
`RedisVersionedSessionRepository`는 Spring Session의 저장소 경계만 구현하고, 쿠키·CSRF·session
|
||||
fixation 정책은 inbound web이 소유한다. 실제 Redis 상태 변경은 manifest로 닫힌 6개 Lua 프로그램
|
||||
(create/inspect/save/touch/revoke/rotate)을 통해서만 수행한다.
|
||||
|
||||
- raw session ID는 physical key에 들어가지 않고 versioned HMAC digest로 변환된다.
|
||||
- idle timeout과 absolute lifetime을 동시에 적용하며 touch 쓰기는 설정된 interval로 제한한다.
|
||||
- logout은 revision `0`의 adapter-private force-revoke를 사용한다. 하나의 Lua 실행에서 tombstone을
|
||||
먼저 만들고 live hash를 삭제하므로 concurrent stale save가 세션을 부활시킬 수 없다.
|
||||
- rotation은 old ID tombstone과 new ID 생성을 원자적으로 수행한다. old/new ID가 서로 다른 Cluster
|
||||
slot이므로 현재 activation은 standalone만 허용하고 Cluster와 Sentinel을 startup에서 거부한다.
|
||||
- 저장 payload는 N/N-1 version을 읽는 명시적 primitive allowlist envelope다. Java serialization과
|
||||
default typing을 쓰지 않는다. SHA-256 checksum은 우발적 손상 탐지용이며 authenticity 또는 공격자
|
||||
변조 방지 보장이 아니다.
|
||||
- timeout/response loss와 OOM은 성공이나 miss로 바꾸지 않고 unavailable/indeterminate로 닫는다.
|
||||
별도 요청에서 같은 operation ID를 자동 재사용해 reconcile하지 않으므로 운영자는 timeout 뒤에
|
||||
mutation 성공을 추정하면 안 된다.
|
||||
|
||||
현재 저장소는 의도적으로 unindexed baseline이다. principal lookup, 사용자 전체 logout,
|
||||
maximum-concurrent-session 제어는 제공하지 않는다. 이 기능이 필요한 프로젝트는 별도 bounded index와
|
||||
그 index의 원자성·복구 증거를 추가해야 한다. 현재 `card-redis-session` 레인은 같은 JVM 안의 서로
|
||||
독립적인 두 runtime/repository client가 하나의 standalone Redis를 공유할 때의 logout/stale-save
|
||||
race, TLS+named ACL, partition+`noeviction` OOM/recovery, Redis 7.2/7.4 compatibility를 검증한다.
|
||||
이는 multi-process/pod, rolling deployment, pod/network failure qualification이 아니다.
|
||||
|
||||
아웃바운드 provider의 기본값은
|
||||
`ca-skeleton.capabilities.rate-limit.provider=disabled`다. `redis`로 선택하면 canonical
|
||||
`COORDINATION` role, `failure-policy=fail-closed`, default policy와 secret reference가 모두
|
||||
필요하다. `app.rate-limit.enabled`는 HTTP transport enforcement만 제어하며 provider를 암묵적으로
|
||||
선택하거나 fallback을 만들지 않는다. 설정은 `app.cache.redis`를 fallback으로 사용하지 않고,
|
||||
`distributedRateLimiter`라는 semantic port bean만 외부에 제공한다. Caller deadline이 canonical
|
||||
Redis command timeout보다 짧으면 command를 보내지 않고 typed no-mutation outcome을 반환한다.
|
||||
|
||||
Rate-limit physical key는 raw principal/IP/API key를 포함하지 않고 policy ID/revision/algorithm과
|
||||
이미 pseudonymized된 subject digest를 다시 HMAC한다. Unknown policy/state/program/reply,
|
||||
pre-send admission failure, post-dispatch indeterminate failure와 unsafe Redis clock을 서로 다른
|
||||
outcome으로 보존하며 fail-open하지 않는다. 현재 standalone과 standalone TLS+named ACL의
|
||||
`implemented-candidate` evidence가 있다. Sentinel/Cluster, topology failover,
|
||||
credential/certificate rotation, effective eviction/persistence attestation과 R3 증거는 없으며,
|
||||
checked-in `selected` card가 없으므로 R2 release claim도 없다.
|
||||
|
||||
## Application cache contract
|
||||
|
||||
`application-core`의 `CacheRegionPort<K,V>`는 다음을 분리한다.
|
||||
|
||||
- fresh/stale positive hit;
|
||||
- authoritative negative hit;
|
||||
- normal absent/expired/invalidated miss;
|
||||
- incompatible schema;
|
||||
- unavailable/overloaded와 operation certainty;
|
||||
- recorded/conditional/degraded/indeterminate mutation;
|
||||
- invalidated/already-absent/degraded/indeterminate invalidation.
|
||||
|
||||
TTL, jitter, codec, topology와 Redis SDK 타입은 이 port에 들어가지 않는다. 실제 product의
|
||||
use case는 `CacheRegionPort`를 상속한 semantic subtype을 정의해야 한다.
|
||||
|
||||
`application-core`의 `CacheAsideExecutor`는 lookup/source/write 흐름을 공통화하고 다음을
|
||||
보장한다.
|
||||
|
||||
- fresh/negative hit에서 source를 호출하지 않음;
|
||||
- authoritative absence만 negative cache하고, miss refill은 `ONLY_IF_ABSENT`, stale/quarantine
|
||||
refill은 `ONLY_IF_OBSERVED`로 기록;
|
||||
- classified transient source failure에서만 hard expiry 전 stale fallback;
|
||||
- local single-flight의 in-flight key/waiter bound와 abandoned-flight opportunistic cleanup;
|
||||
- source bulkhead의 concurrency/admission/load deadline bound;
|
||||
- unclassified exception과 interrupt/cancellation 보존.
|
||||
|
||||
동기 source loader는 cooperative cancellation token을 확인해야 한다. 임의 source 코드를
|
||||
강제 종료하지 않으며, source가 token/deadline을 무시하면 bulkhead permit은 반환 시점까지
|
||||
점유된다.
|
||||
|
||||
## Physical key
|
||||
|
||||
`RedisKeyBuilder`만 다음 canonical shape를 만든다.
|
||||
|
||||
```text
|
||||
ca:<app>:<env>:<capability>:<region>:hv<hashVersion>:kv<keyVersion>:{<slot>}:<digest>:<kind>
|
||||
```
|
||||
|
||||
민감한 사용자/tenant/composite 값은 raw key에 넣지 않는다. length-prefixed canonical bytes를
|
||||
HMAC-SHA-256으로 digest한다. random opaque identifier는 SHA-256을 사용할 수 있다. builder는 slug,
|
||||
version, 정확히 하나인 hash tag와 전체 UTF-8 byte bound를 검증한다.
|
||||
|
||||
## Atomic program foundation
|
||||
|
||||
`redis/*-program-set.json`과 `redis/program-set.json`은 cache/rate/idempotency/lease/session 및
|
||||
primitive Lua resource의 exact digest, signature, status, complexity와 timeout certainty를
|
||||
기록한다. `RedisAtomicPrimitives`는 compare-delete,
|
||||
compare-expire, set-if-absent-with-TTL, replace-if-observed-with-TTL을 typed result로 노출하고
|
||||
unknown status를 compatibility failure로 처리한다. owner/value/observation/operation/TTL은
|
||||
Redis 호출 전에 제한된다. `redis/rate-program-set.json`은 structured rate-limit 프로그램의
|
||||
별도 digest/signature/status manifest다.
|
||||
Generic descriptor/catalog/executor와 typed primitive facade는 package-private collaborator다.
|
||||
Spring composition에는 raw Redis key/value/TTL을 받는 bean을 노출하지 않으며, 이후 semantic
|
||||
port adapter가 내부에서만 이 facade를 사용한다.
|
||||
이 primitive facade 자체는 application에 노출되는 범용 Redis port가 아니다. Cache, rate limit,
|
||||
idempotency, soft lease, session의 semantic provider만 closed catalog를 내부에서 소비하며, 이
|
||||
구조 자체가 release selection이나 R2 qualification을 뜻하지 않는다.
|
||||
|
||||
`RedisLuaProgramExecutor`가 catalog source로 SHA-1 script identity를 계산하여 `EVALSHA`를 먼저
|
||||
호출하고 정확히 `NOSCRIPT`일 때만 catalog script를 `SCRIPT LOAD`한다. 반환 digest가 예상 identity와
|
||||
같은지 확인한 뒤 `EVALSHA`를 한 번만 재시도한다. signature/argument bounds는
|
||||
client 호출 전에 다시 검증하고 descriptor catalog membership 및 반환 status membership을
|
||||
확인한다. unit lane은 강제 `NOSCRIPT` load/retry를 검증하고 standalone real-service lane은
|
||||
compare-and-delete, NX, bounded trailing-digest observed replace, concurrent-writer 보존을 실제
|
||||
Redis 7.2에서 검증한다. 같은 lane은 16MiB payload의 record/read/observed-replace와
|
||||
16MiB+1 사전 거부, mutation interrupt의 `INDETERMINATE` certainty와 interrupt flag 복원도
|
||||
실행한다.
|
||||
|
||||
## Managed runtime과 semantic region
|
||||
|
||||
Canonical activation은
|
||||
`ca-skeleton.capabilities.cache.bindings.default=redis`와
|
||||
`ca-skeleton.providers.redis.roles.cache`를 함께 요구한다. 전자는 semantic policy를, 후자는
|
||||
topology/TLS/ACL credential을 소유한다. Canonical region은 legacy `app.cache.redis.host`,
|
||||
`password`, raw HMAC 값을 읽지 않고 CACHE role router와
|
||||
`RedisCredentialMaterialProvider`의 `secret://` reference만 사용한다. 같은 CACHE router가 L2
|
||||
command와 invalidation Pub/Sub을 함께 route하므로 topology rotation 때 새 subscription ACK가
|
||||
확인된 뒤 route가 교체된다. Canonical/legacy 동시 활성은 precedence를 추측하지 않고 startup에서
|
||||
거절한다. 현재 템플릿이 자동 조합하는 semantic region ID는 `default` 하나이며, 여러 product
|
||||
region은 region registry/compiler가 추가되기 전까지 자동 생성한다고 주장하지 않는다.
|
||||
|
||||
`app.cache.redis.enabled=true`이고 `client-mode=managed`(기본값)이면 `LettuceRedisRuntime`이
|
||||
단일 binary connection을 생성하고 종료 시 connection/client를 닫는다. 프로젝트가
|
||||
`RedisClient`를 직접 제공하는 경우에는 `client-mode=external`을 명시해야 한다. 이 선택을
|
||||
명시함으로써 Spring configuration 처리 순서에 따라 managed/custom client 선택이 달라지지 않는다.
|
||||
Managed runtime은 reconnect 시 pending command를 replay하지 않고, disconnected command를
|
||||
pre-send 거부하며, request queue와 동시 outstanding command를 같은 finite bound로 제한한다.
|
||||
`RedisStringCacheRegion`은 `CacheRegionPort<String,String>` bean으로 제공되며 다음 결과를
|
||||
구분한다.
|
||||
|
||||
- positive hit, authoritative negative hit, normal miss;
|
||||
- unknown/corrupt/retired envelope와 fail-fast future envelope;
|
||||
- read unavailable/overloaded와 mutation not-applied/indeterminate;
|
||||
- invalidated와 already absent.
|
||||
|
||||
opaque source revision에는 대소 비교 의미가 없으므로
|
||||
`ONLY_IF_SOURCE_REVISION_NEWER`는 임의 lexical comparison을 하지 않고
|
||||
`NOT_RECORDED_PROVIDER_POLICY`를 반환한다.
|
||||
|
||||
Envelope v2는 source revision, soft/hard absolute expiry와 payload를 digest로 보호한다.
|
||||
`soft <= now < hard`는 stale, `hard <= now`는 expired miss다. Retired v1은 명시적 quarantine
|
||||
후 reload 대상이고 future/corrupt envelope는 fail-fast다. Integrity digest를 version byte보다
|
||||
먼저 검사하며, digest가 맞더라도 현재 v2 구조가 잘못되면 corrupt로 분류한다. Stale/retired
|
||||
lookup은 envelope digest를 opaque observation token으로 전달하고, cache-aside는 Lua에서 현재
|
||||
digest가 그 token과 같을 때만 새 envelope로 교체한다. 따라서 조회와 refresh 사이의 writer를
|
||||
삭제하거나 덮어쓰지 않는다. Source revision의 application invariant (1..128 characters)는
|
||||
decode 때도 다시 검사한다.
|
||||
|
||||
`positive-soft-ttl`, 기존 `positive-ttl`(hard), `negative-ttl`, `ttl-jitter`,
|
||||
`minimum-hard-ttl`은 startup에 immutable policy로 freeze된다. Jitter는 HMAC-derived physical
|
||||
key와 policy revision으로 결정적이며 positive soft/hard에는 같은 factor를 적용한다. Redis
|
||||
physical TTL은 envelope에 기록된 hard expiry와 같다.
|
||||
|
||||
추가 runtime setting은 `app.cache.redis.maximum-queued-commands=8`(범위 `1..4096`)과
|
||||
`app.cache.redis.maximum-in-flight-bytes=16777216`이다. 최대 readable envelope와 최대 command
|
||||
byte를 별도로 계산하며, command count와 retained request/response byte budget을 모두 통과해야
|
||||
Lettuce 호출을 시작한다. `queue-count × maximum-command-bytes`도 byte bound 이하여야 한다. 이 관계는
|
||||
timeout 완료 뒤 driver가 응답 decode 전까지 command args를 유지하는 경우도 유한하게 제한한다.
|
||||
timeout 직후에는 runtime admission population과 Lettuce retained population이 겹칠 수 있으므로
|
||||
최악 상한은 대략 `maximum-in-flight-bytes + queue-count × per-command-bound`이고, 설정 검증은
|
||||
두 번째 항이 첫 번째 항을 넘지 않게 해 최대 약 2배 population으로 제한한다.
|
||||
|
||||
read는 raw `GET`을 사용하지 않는다. 고정 Lua read가 `GETRANGE(0, maximum-envelope-bytes)`로
|
||||
Redis가 wire에 내보내는 bulk reply 자체를 `maximum-envelope-bytes + 1` 이하로 자르고, 초과하면
|
||||
작은 오류 응답으로 바꾼다. 따라서 다른 writer가 같은 물리 키를 오염시켜도 전체 대용량 value를
|
||||
Netty/codec에 먼저 할당하지 않는다. managed runtime을 활성화할 때 host가 누락되면
|
||||
`localhost`로 암묵 fallback하지 않고 startup을 실패시킨다.
|
||||
|
||||
Generation/revision fence는 mass/per-key invalidation과 source-load race를 막는다. Distributed
|
||||
refresh soft lease는 정상 시 중복 refresh를 줄이지만 TTL expiry/crash에서는 duplicate owner를
|
||||
허용하며, cache generation fence를 대체하는 correctness lock이 아니다.
|
||||
|
||||
`app.cache.redis.l1.enabled=true`는 semantic string cache 앞에만 optional local L1을 붙인다.
|
||||
L1은 maximum entries, maximum accounted weight, per-entry accounted weight, local TTL, generation
|
||||
recheck interval과 invalidation subscriber queue를 모두 finite하게 검증한다. Local expiry는 Redis
|
||||
envelope hard expiry보다 길어질 수 없다. Weight는 HMAC-derived local identity와 UTF-8 value,
|
||||
entry/lookup metadata에 대한 고정 conservative allowance를 더한 admission/eviction accounting
|
||||
proxy이며, JVM heap reservation이나 실제 object layout의 exact byte guarantee가 아니다.
|
||||
|
||||
Invalidation Pub/Sub payload는 raw semantic key를 포함하지 않고 HMAC-authenticated bounded
|
||||
message를 사용한다. Pub/Sub은 durable/exact invalidation 원장이 아니라 eviction hint다. Subscriber
|
||||
disconnect나 queue overflow는 L1 전체를 flush하고, monotonic local invalidation epoch가 진행 중인
|
||||
generation probe와 refill admission을 무효화한다. 재연결 뒤 generation을 다시 읽기 전에는 L1
|
||||
admission을 허용하지 않는다. Hint 유실 시 mass invalidation은 periodic generation recheck,
|
||||
per-key invalidation은 local TTL 안에서 Redis L2로 복귀한다.
|
||||
|
||||
이 local tier는 cache-only internal type을 요구하므로 session, idempotency, strict rate-limit,
|
||||
coordination provider에 적용할 수 없다. 해당 capability들은 local fail-open cache semantics를
|
||||
재사용하지 않는다.
|
||||
|
||||
Refresh-ahead와 probabilistic early refresh는 아직 구현하지 않았다. 둘 다 correctness baseline이
|
||||
아니며, refresh-ahead는 명시적인 bounded hot-set registry/scheduler 없이 full keyspace scan으로
|
||||
대체하지 않는다. Probabilistic early refresh도 versioned probability descriptor와 deterministic
|
||||
property test가 생기기 전에는 readiness guarantee로 광고하지 않는다. Cache card에는 standalone
|
||||
TLS+named ACL과 bounded fault evidence가 있지만 Sentinel/Cluster Pub/Sub/failover,
|
||||
credential/certificate rotation, persistence/restart, effective eviction attestation,
|
||||
multi-process/pod L1/L2 coherence와 R3 qualification은 아직 없다.
|
||||
|
||||
## Efficiency-only lease
|
||||
|
||||
`ca-skeleton.capabilities.lease.provider=redis`를 명시한 경우에만
|
||||
`DistributedLeasePort`가 생성되며, canonical `COORDINATION` role router와 별도 HMAC secret
|
||||
reference를 사용한다. 미선택 상태에서는 lease bean, secret resolution, native client와 thread
|
||||
side effect가 모두 0이다.
|
||||
|
||||
이 port의 guarantee는 오직 `EFFICIENCY_ONLY`다. acquire/inspect/renew/release는 같은
|
||||
owner token과 operation ID를 비교하고, response loss를 성공이나 실패로 추측하지 않고
|
||||
`INDETERMINATE`/`UNKNOWN`으로 유지한다. caller가 최초 send 전에 보관한 같은 attempt로 inspect
|
||||
또는 acquire replay를 해야 ownership을 복구할 수 있다. Handle validity는 Redis가 보고한 remaining
|
||||
TTL에서 command 왕복 monotonic elapsed와 drift budget을 차감하며, server expiry wall clock은
|
||||
telemetry 용도일 뿐이다. Watchdog는 worker와 registration 수, renewal cadence, application
|
||||
deadline이 모두 유한하고 lease loss/unknown에서 작업 취소 callback을 한 번만 전달한다.
|
||||
|
||||
`redisEfficiencyLeaseTest`는 pinned Redis 7.2와 다음/승인 버전에서 standalone concurrency,
|
||||
TLS/ACL, partition/response uncertainty와 compatibility를 별도 qualification한다. 이 test는
|
||||
readiness card가 아니며 cache-refresh soft lease나 fenced coordination의 증거로 재사용되지
|
||||
않는다. Fencing token과 protected-resource stale-token rejection은 구현하지 않았으므로
|
||||
`redis-fenced-coordination` card는 계속 `not-implemented`다. 이 lease만으로 결제, 재고,
|
||||
unique ID 또는 외부 장치 command 같은 correctness-sensitive write를 승인하면 안 된다.
|
||||
|
||||
## Legacy path
|
||||
|
||||
기존 `CacheStoreRouter`, `RedisCacheStore`, `FailOpenCacheStore`는 호환성을 위해 남아 있다. 이
|
||||
경로는 `Optional.empty()`로 miss와 backend failure를 합친다. managed runtime을 사용할 때
|
||||
legacy `put`에도 positive TTL을 적용하지만, 사용자 제공 legacy client의 TTL은 보장할 수 없으므로
|
||||
새 semantic cache port 구현의 기준으로 사용하지 않는다.
|
||||
|
||||
## Verification
|
||||
|
||||
이 leaf가 실제로 가진 task는 `test`, `check`, `redisTopologyTest` 세 개다.
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:cache-redis:test --console=plain
|
||||
./gradlew :application-core:check :adapter:outbound:cache-redis:check --console=plain
|
||||
```
|
||||
|
||||
Topology lane은 opt-in이며 fail-closed다. mode는 `standalone`, `sentinel`, `cluster`만 허용하고,
|
||||
알 수 없는 mode·endpoint 누락·해당 lane tag를 가진 test class 부재·실행 test 0건은 모두 실패다.
|
||||
(이전에는 오타 mode가 tag를 아무것도 매칭하지 못해 test 0건으로 `BUILD SUCCESSFUL`이 났다.)
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:cache-redis:redisTopologyTest \
|
||||
-Predis.topology.host=127.0.0.1 -Predis.topology.port=6379 \
|
||||
-Predis.topology.mode=standalone --console=plain
|
||||
# sentinel lane은 -Predis.topology.master=<master-name> 을 추가로 요구한다.
|
||||
```
|
||||
Reference in New Issue
Block a user