Files
tech-log-backend/src/adapter/outbound/cache-redis/README.md
T

26 KiB
Raw Blame History

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.jsonadapter-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 장애는 redisOptionalstate=DEGRADED detail로만 나타나고 readiness를 내리지 않는다. COORDINATION/SESSION 장애는 redisRequiredDOWN으로 만들며, 어떤 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-contractEdgeRateLimitPort 뒤에서 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-coreCacheRegionPort<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-coreCacheAsideExecutor는 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를 만든다.

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.jsonredis/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=redisca-skeleton.providers.redis.roles.cache를 함께 요구한다. 전자는 semantic policy를, 후자는 topology/TLS/ACL credential을 소유한다. Canonical region은 legacy app.cache.redis.host, password, raw HMAC 값을 읽지 않고 CACHE role router와 RedisCredentialMaterialProvidersecret:// 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로 제한한다. RedisStringCacheRegionCacheRegionPort<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 세 개다.

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이 났다.)

./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> 을 추가로 요구한다.