refactor: 문서 개선 중
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user