refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -0,0 +1,241 @@
# Timeout 뒤 쓰였는지 모를 때: Executor와 실행 확실성 모델
> **Redis 코드 상세 시리즈 12/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-advanced-surfaces.md) · 다음: [Redis 캐시 한 요청의 전 생애: Generation·Envelope·Soft/Hard TTL](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-cache-code-walkthrough.md)
## 이 글이 답하는 코드 질문
Redis write가 timeout 또는 connection loss로 실패했을 때 “실행되지 않았다”고 말할 수 있습니까? sync, reactive, transaction queue는 같은 admission과 failure metadata를 어떻게 사용합니까?
현행 translator의 핵심 규칙은 다음과 같습니다.
- server가 거절했다는 reply가 있으면 confirmed failure로 다룹니다.
- read timeout/connection failure는 policy가 retry-safe인 경우 retryable metadata를 가질 수 있습니다.
- 실행됐을 수 있는 write timeout/connection loss는 `RedisAmbiguousExecutionException`입니다.
- ambiguous failure는 `retryable=false`입니다.
executor 자체에는 자동 retry loop가 없습니다. metadata와 `ExecutionCertainty`는 caller가 retry·reconciliation·compensation을 결정할 근거이지, 현재 production pipeline이 자동 재전송한다는 증거가 아닙니다.
## 먼저 보는 클래스 지도
| 클래스 | 입력 | 출력 | 다음 호출 |
|---|---|---|---|
| [CommandRequest](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandRequest.java:34) | command/key/size/permit/budget/deferred invocation | 실행 전 요청 | guard |
| [CommandAdmission](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandAdmission.java:17) | descriptor/lane/slot/timeout | 실행 결정 | executor |
| [SyncRedisCommandExecutor](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/SyncRedisCommandExecutor.java:23) | request | blocking result 또는 typed failure | translator·observation |
| [ReactiveRedisCommandExecutor](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/ReactiveRedisCommandExecutor.java:19) | request | `Mono<R>` | translator·observation |
| [QueueingRedisCommandExecutor](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/QueueingRedisCommandExecutor.java:28) | transaction command/stage | unresolved stage, explicit await | `EXEC` |
| [LettuceExceptionTranslator](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java:41) | Throwable와 execution context | stable SDK exception | caller |
| [RedisFailureMetadata](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisFailureMetadata.java:17) | payload-free failure facts | retry/ambiguity 판단 값 | caller·telemetry |
| [ExecutionCertainty](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/ExecutionCertainty.java:15) | descriptor와 certainty state | 자동 retry 허용 여부 | failover model |
| [SentinelFailoverObserver](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserver.java:37) | promotion/reconnect/in-flight 분류 | counters와 certainty | operator/caller |
## Admission과 wire send의 경계
`CommandRequest`는 invocation을 `Supplier<CompletionStage<R>>`로 보관합니다. guard가 실패하면 supplier를 평가하지 않으므로 명령은 전송되지 않습니다.
```mermaid
flowchart TD
A[CommandRequest] --> B[guard.validate]
B -->|거절| C[not-sent typed exception]
B -->|admit| D[invocation.get]
D --> E{reply/driver outcome}
E -->|success| F[result + success observation]
E -->|server error| G[confirmed typed failure]
E -->|timeout/connection loss| H{read인가, ambiguous write인가}
H -->|retry-safe read| I[retryable non-ambiguous failure]
H -->|write may have applied| J[ambiguous non-retryable failure]
```
admission failure와 invocation 이후 failure는 evidence가 다릅니다. namespace·permit·budget·capability 거절은 not sent입니다. invocation을 시작한 뒤 reply를 못 받은 write는 server에 도달하지 않았다고 증명할 수 없습니다.
## Effective timeout은 어디서 옵니까
기본 timeout은 [TimeoutProfile](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/TimeoutProfile.java:11)에 있습니다.
| profile | default |
|---|---:|
| `FAST` | 500ms |
| `COLLECTION` | 2s |
| `SCRIPT` | 1s |
| `BATCH` | 2s |
| `ADMIN` | 3s |
| `BLOCKING` | 2s default, 실제 block에는 margin 적용 |
R2 request가 `OperationBudget`을 가지면 non-blocking path에서는 budget의 timeout이 effective timeout입니다. server block을 선언하지 않은 optional-blocking path도 같은 분기입니다. [OperationBudget](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/command/OperationBudget.java:12)은 element, request bytes, reply bytes, timeout을 모두 양수로 요구합니다.
blocking command가 bounded server block을 선언하면 budget timeout은 사용하지 않습니다. 0·음수·configured maximum 초과를 거절한 뒤 `serverBlock + BLOCKING_MARGIN(2s)`를 client-side timeout으로 씁니다. optional-block command에 block이 없을 때만 budget 또는 profile default를 사용합니다. 이 분기는 [CommandPolicyGuard.effectiveTimeout](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java:231)에 그대로 드러납니다.
## Sync executor의 호출 순서
[SyncRedisCommandExecutor.execute](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/SyncRedisCommandExecutor.java:58)는 다음 순서로 동작합니다.
1. guard가 `CommandAdmission`을 만듭니다.
2. descriptor, lane, topology, slot으로 observation을 시작합니다.
3. `invocation.get()`으로 driver call을 시작합니다.
4. returned stage를 effective timeout까지 기다립니다.
5. success면 observation을 기록하고 결과를 반환합니다.
6. runtime failure면 elapsed를 넣은 context로 translate합니다.
7. translated metadata의 ambiguity를 failure observation에 기록한 뒤 throw합니다.
`CompletableFuture.get` timeout은 Lettuce `RedisCommandTimeoutException`으로 감싸 translator에 보냅니다. Java `InterruptedException`은 [interrupt flag를 복원한 뒤](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/SyncRedisCommandExecutor.java:81) `CompletionException`으로 감쌉니다. 두 failure는 translator에서 같은 branch를 타지 않습니다.
observation sink는 `NoThrowObservationSink`으로 감쌉니다. [success 기록의 경계](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/SyncRedisCommandExecutor.java:65)는 meter failure를 Redis write failure로 오인하지 않게 driver try/catch 밖에서 success observation을 기록합니다.
## Reactive executor의 호출 순서
[ReactiveRedisCommandExecutor.execute](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/ReactiveRedisCommandExecutor.java:56)는 `Mono.defer` 안에서 admission을 실행합니다.
이 위치 때문에 다음이 성립합니다.
- publisher assembly 때는 Redis 호출과 guard validation이 시작되지 않습니다.
- subscribe 때 namespace/permit/budget failure가 error signal로 발생합니다.
- caller는 `onErrorResume` 같은 reactive recovery를 사용할 수 있습니다.
- 같은 publisher를 여러 번 subscribe하면 deferred request가 다시 실행될 수 있습니다.
admission 후 `Mono.fromCompletionStage``.timeout(admission.timeout())`을 적용합니다. error는 translator를 거쳐 stable SDK exception이 되고 observation에 ambiguity가 기록됩니다.
sync와 reactive는 같은 guard와 descriptor semantics를 사용하지만 timeout 구현 자체는 `Future.get`과 Reactor operator로 다릅니다.
## Queueing executor는 왜 기다리지 않습니까
transaction의 queued command는 `MULTI` 안에서 `+QUEUED`만 받습니다. 실제 reply는 `EXEC`가 실행될 때까지 존재하지 않습니다. 여기서 일반 sync executor처럼 wait하면 transaction이 자기 reply를 만들 `EXEC`에 도달하지 못해 deadlock합니다.
[QueueingRedisCommandExecutor.queue](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/QueueingRedisCommandExecutor.java:63)는 admission과 invocation 시작까지만 하고 stage를 반환합니다.
success observation도 queue 시점이 아니라 stage completion에 붙입니다. watch conflict로 `EXEC`가 실행하지 않은 command를 성공으로 세지 않기 위해서입니다.
transaction 자체가 소유한 `WATCH`, `MULTI`, `EXEC`, cleanup stage는 [await](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/QueueingRedisCommandExecutor.java:105)로 기다립니다. 특히 `EXEC` reply timeout은 transaction 전체가 실행됐을 수도 있으므로 write context로 번역되어 ambiguous입니다. Java interrupt도 flag를 복원한 뒤 `EXEC` write context로 translator에 보내므로 unclassified ambiguous failure가 됩니다.
## Translator의 분류 순서
[translate](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java:63)는 `CompletionException``ExecutionException`을 먼저 벗깁니다. 이미 `RedisOperationException`이면 그대로 반환합니다.
그다음 구체적인 driver type을 분류합니다.
| 입력 | SDK failure | retry/ambiguity |
|---|---|---|
| `RedisCommandTimeoutException` 또는 Java `TimeoutException` | read: `RedisTimeoutException`; ambiguous write: `RedisAmbiguousExecutionException` | read policy에 따라 retryable; write ambiguous |
| Lettuce `RedisCommandInterruptedException` | timeout과 같은 hierarchy | read policy에 따라 retryable; write ambiguous |
| executor의 Java `InterruptedException` | unclassified read: generic `RedisOperationException`; ambiguous write: `RedisAmbiguousExecutionException` | retry-safe read만 retryable; write ambiguous |
| connection failure | read: `RedisConnectionException`; ambiguous write: `RedisAmbiguousExecutionException` | 같은 규칙 |
| loading/busy | `RedisBusyException` | read 여부 또는 false |
| Lettuce NOSCRIPT | `RedisNoScriptException` | false/false |
| read-only replica/partition | `RedisRedirectionException` | false/false |
| server execution error | leading error code로 세분화 | server reply가 있으므로 non-ambiguous |
| unclassified failure | retry-safe read: generic retryable failure; ambiguous write: ambiguous failure | descriptor에서 결정 |
unclassified write가 plain non-applied failure로 떨어지지 않는 것이 중요합니다. [unclassified fallback](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java:126)은 server reply가 없고 write가 ambiguous할 수 있으면 안전한 기본값으로 ambiguity를 선택합니다.
이 구분은 class 이름이 비슷해서 놓치기 쉽습니다. translator가 timeout으로 직접 분류하는 interrupted type은 [Lettuce의 `RedisCommandInterruptedException`](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java:70)뿐입니다. `Future.get`이 던지는 `java.lang.InterruptedException`은 그 type이 아니므로 unwrap 뒤 unclassified fallback으로 갑니다.
## Server error code와 정보 노출 제한
`RedisCommandExecutionException`은 message의 첫 uppercase error code만 읽습니다.
- `WRONGTYPE``RedisDataTypeMismatchException`
- `CROSSSLOT``RedisCrossSlotException`
- `NOPERM`, `NOAUTH`, `WRONGPASS`, `NOUSER`, `UNAUTHORIZED``RedisAccessDeniedException`
- `MOVED`, `ASK`, `TRYAGAIN`, `CLUSTERDOWN`, `MASTERDOWN`, `REDIRECT``RedisRedirectionException`
- `BUSY`, `LOADING`, `BUSYGROUP`, `BUSYKEY``RedisBusyException`
- `NOSCRIPT``RedisNoScriptException`
- `OOM`, `MISCONF`, `NOREPLICAS`, `EXECABORT`, `READONLY``RedisCommandRejectedException`
[serverError](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslator.java:166)는 raw server message를 SDK message에 복사하지 않습니다. Redis error에 들어갈 수 있는 key와 argument fragment가 exception/telemetry로 노출되지 않게 합니다.
## `RedisFailureMetadata`가 보존하는 것
[RedisFailureMetadata](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisFailureMetadata.java:17)는 다음 field만 가집니다.
- low-cardinality `commandCategory`
- `CommandAccess`
- read 여부
- retryable 여부
- ambiguous execution 여부
- optional server version
- deployment mode
- optional Cluster slot
- elapsed duration
key, value, credential, raw server message는 없습니다. constructor는 retryable과 ambiguous가 동시에 true인 상태를 금지하며 slot을 0..16383으로 제한합니다.
`notSent` factory는 read rejection만 retryable로 표시하고 ambiguity는 false로 둡니다. stored data corruption은 read여도 retryable이 아니므로 별도 [storedDataCorruption](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/error/RedisFailureMetadata.java:73) factory를 사용합니다.
## 실행 확실성 네 상태
[ExecutionCertainty](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/ExecutionCertainty.java:15)는 상태를 네 개로 이름 붙입니다.
```mermaid
stateDiagram-v2
[*] --> CONFIRMED_SUCCESS: server success reply
[*] --> CONFIRMED_FAILURE: server refusal reply
[*] --> SAFE_TO_RETRY_FAILURE: server 미도달 증명
[*] --> AMBIGUOUS_FAILURE: 도달/적용 여부 불명
```
`allowsAutomaticRetry`는 confirmed outcome에는 false, safe-to-retry failure에는 true를 반환합니다. ambiguous failure는 descriptor가 retry-safe일 때만 true입니다.
그러나 exception metadata의 invariant는 ambiguous와 retryable을 동시에 허용하지 않습니다. 따라서 `ExecutionCertainty.AMBIGUOUS_FAILURE`가 retry-safe read에 대해 자동 retry를 허용하는 모델과 translator가 생성하는 metadata는 서로 다른 표현 계층입니다. 현재 executor가 `ExecutionCertainty`를 사용해 retry하는 코드는 없습니다.
## Sentinel reconnect queue와 in-flight 분류
[SentinelFailoverObserver](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserver.java:10)는 promotion 시 다음을 기록하도록 설계됐습니다.
- promotion count
- ambiguous non-idempotent write count
- reconnect queue가 차서 거절한 count
- longest reconnect duration
`offerWhileReconnecting`은 atomic counter가 configured maximum을 넘으면 즉시 false를 반환하고 refusal을 셉니다. unbounded backlog를 만들지 않습니다.
`classify(descriptor, reachedServer)`는 server에 도달하지 않았으면 `SAFE_TO_RETRY_FAILURE`, 도달했으면 `AMBIGUOUS_FAILURE`를 반환합니다. 후자의 descriptor가 retry-safe가 아니면 ambiguous write counter를 올립니다.
이 observer의 class comment에 있는 2,086과 1 수치는 historical Sentinel 실험 설명입니다. client가 성공 reply를 받은 뒤 old primary의 write가 유실되는 경우는 observer가 볼 수 없으며, server-side `min-replicas-to-write`와 bounded `min-replicas-max-lag`가 필요하다고 설명합니다. 이 수치를 현행 runtime test 결과로 표현하면 안 됩니다.
`WAIT`로 이 공백을 해결한다고 읽어도 안 됩니다. 현행 command policy에 `WAIT`가 없어 default-deny이며 typed/semantic surface도 없습니다.
## 테스트가 고정하는 계약
translator 테스트는 failure별 시작 행을 따로 가집니다.
- [write timeout의 ambiguous·non-retryable metadata](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:22)
- [read timeout의 retryable·non-ambiguous metadata](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:33)
- [write 주변 connection loss의 ambiguity](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:44)
- [async completion wrapper 제거](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:55)
- [server code의 stable exception hierarchy 변환](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:65)
- [server message detail 비노출](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:85)
- [이미 번역한 failure의 동일 instance 통과](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:94)
- [unrecognized write failure의 ambiguity](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:105)
- [unrecognized read failure의 retryable metadata](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/LettuceExceptionTranslatorTest.java:122)
Sentinel observer는 [server 미도달](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserverTest.java:26), [non-idempotent in-flight write](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserverTest.java:37), [idempotent read](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserverTest.java:47), [confirmed outcome](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserverTest.java:57), [bounded queue](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserverTest.java:64), [longest reconnect](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/connection/SentinelFailoverObserverTest.java:78)를 각각 단위 테스트합니다.
Transaction 쪽은 [queued command의 commit 전 미적용](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisTransactionContractTest.java:169), [`QueuedReply` 조기 접근 금지](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisTransactionContractTest.java:201), [watch conflict에서 미실행](/home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/operations/RedisTransactionContractTest.java:217)을 별도 테스트가 고정합니다.
이 테스트는 이번 문서 작업에서 실행하지 않았습니다. real-server standalone/Sentinel/Cluster/TLS lane도 실행하지 않았습니다.
## 현재 구현 공백과 잘못 읽기 쉬운 지점
1. sync/reactive/queueing executor, translator, guard의 production bean 조립은 확인되지 않습니다.
2. aggregate facade와 application bridge가 미조립이므로 이 failure model이 모든 production Redis call에 적용된다고 단정할 수 없습니다.
3. executor에는 automatic retry loop가 없습니다. `retryable`은 재전송이 일어났다는 뜻이 아닙니다.
4. `ExecutionCertainty``SentinelFailoverObserver`는 production source에서 서로 외의 사용처나 runtime wiring을 찾지 못했습니다.
5. `CommandExecutionContext.of`는 server version을 `Optional.empty()`로 만들며 executors는 `withServerVersion`을 호출하지 않습니다. translator가 만든 failure metadata의 server version은 현재 비어 있습니다. guard rejection metadata에는 probed version이 들어가는 것과 다릅니다.
6. `QueueingRedisCommandExecutor.queue`의 asynchronously failed stage는 translator로 observation을 만들지만 returned stage 자체를 translated failure로 교체하지 않습니다. transaction caller가 받는 exception shape는 별도 검증이 필요합니다.
7. Java `InterruptedException`은 Lettuce `RedisCommandInterruptedException`과 달리 timeout hierarchy로 번역되지 않습니다. sync read는 generic unclassified failure가 될 수 있고, write와 transaction `EXEC`는 ambiguous가 됩니다. 이 차이를 직접 고정하는 executor contract test는 확인되지 않았습니다.
8. Sentinel observer의 reconnect queue counter는 실제 driver queue를 소유하는 자료구조가 아니라 admission 판단과 metric 모델입니다. production 연결도 확인되지 않았습니다.
9. success reply 뒤 promotion으로 유실된 write는 client ambiguity model이 탐지할 수 없습니다.
10. `WAIT`는 현재 default-deny입니다.
다음에 source를 열 때는 guard와 admission, 세 executor, execution context, translator, metadata, certainty enum, Sentinel observer, tests 순으로 보면 됩니다.
## 시리즈의 관련 문서
관련 범위는 command admission, connection lifecycle, typed operations, advanced surfaces입니다.
## 시리즈에서 이어 읽기
- 이전 글: [Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-advanced-surfaces.md)
- 다음 글: [Redis 캐시 한 요청의 전 생애: Generation·Envelope·Soft/Hard TTL](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-cache-code-walkthrough.md)
- 전체 흐름: [Redis를 범용 클라이언트가 아니라 정책 경계로 다루기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md)
- 운영 흐름: [Redis를 켠다는 말의 운영적 의미: 단일 활성화 스위치에서 Sentinel 쓰기 손실 검증까지](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-platform-sre-operations.md)