231 lines
27 KiB
Markdown
231 lines
27 KiB
Markdown
# Redis 연결을 여섯 lane으로 나눈 이유: Pool과 RuntimeOwner 생명주기
|
|
|
|
> **Redis 코드 상세 시리즈 06/20** · [전체 지도](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-backend-policy-boundary.md) · 이전: [하나의 설정에서 세 topology로: RedisTopologyClientFactory 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-topology-client-factory.md) · 다음: [YAML 한 줄이 Redis 명령을 거절하기까지: Policy Loader·Catalog·Guard](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-command-policy-admission.md)
|
|
|
|
## 이 글이 답하는 코드 질문
|
|
|
|
Redis connection은 thread-safe하다는 설명만 보면 하나를 공유해도 될 것처럼 보입니다. 하지만 blocking command, transaction, script, Pub/Sub, admin은 connection 상태와 권한이 다릅니다. 이 글은 여섯 `RedisConnectionKind`가 어떻게 account와 pool ceiling을 고르고, `RedisRuntimeOwner`가 borrow·return·invalidate·drain·close를 어떤 순서로 처리하는지 설명합니다.
|
|
|
|
## 먼저 보는 클래스 지도
|
|
|
|
| 클래스 | 입력 | 출력 | 다음 호출 |
|
|
|---|---|---|---|
|
|
| [`RedisConnectionKind`](/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/RedisConnectionKind.java:6) | command descriptor 또는 explicit lane | lane과 credential role | runtime client role router |
|
|
| [`RedisRuntimeOwner`](/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/RedisRuntimeOwner.java:21) | runtime client, lane limits, drain timeout | typed `RedisLease` | gateway 또는 return |
|
|
| [`RedisLease`](/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/RedisLease.java:5) | borrowed lane connection | gateway, invalidate, close | owner.release |
|
|
| [`RedisRuntimeClient`](/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/RedisRuntimeClient.java:7) | kind + optional routing key | 새 driver lane connection | owner idle pool |
|
|
| [`RedisConnectionRegistry`](/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/RedisConnectionRegistry.java:13) | generic factory + limit | untyped legacy lease | 현재 production에서 호출되지 않음 |
|
|
| [`RedisSdkAutoConfiguration.redisRuntimeOwner()`](/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/config/RedisSdkAutoConfiguration.java:263) | settings + runtime client | Spring destroy method를 가진 owner bean | request-time borrow |
|
|
|
|
## 여섯 lane과 격리하는 실패 모드
|
|
|
|
[`RedisConnectionKind`](/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/RedisConnectionKind.java:21)는 정확히 여섯 값을 가집니다.
|
|
|
|
| lane | connection 성격 | credential role | 공유했을 때의 문제 |
|
|
|---|---|---|---|
|
|
| `REGULAR` | 일반 non-blocking command | APPLICATION | 다른 특수 traffic이 일반 요청을 막을 수 있음 |
|
|
| `BLOCKING` | block 시간 동안 connection 점유 | APPLICATION | BLPOP/XREAD BLOCK이 일반 명령을 stall시킴 |
|
|
| `TRANSACTION` | MULTI~EXEC window 독점 | APPLICATION | 다음 caller command가 열린 transaction에 섞일 수 있음 |
|
|
| `SCRIPT` | registered script 실행 | ADVANCED | 일반 request path에 SCRIPT/EVALSHA grant가 퍼짐 |
|
|
| `PUBSUB` | subscribe lifecycle 전용 | PUBSUB | subscribed connection은 일반 command 용도로 쓸 수 없음 |
|
|
| `ADMIN` | read-only diagnostics | ADMIN | 운영 권한이 application connection에 섞임 |
|
|
|
|
`forCommand()`는 descriptor가 blocking이면 `BLOCKING`을 먼저 선택하고, `ADMIN_READONLY` access이면 `ADMIN`, application/advanced/raw/extension access이면 `REGULAR`을 반환합니다. SCRIPT, TRANSACTION, PUBSUB은 일반 command descriptor만으로 결정하지 않고 해당 고수준 surface가 explicit하게 borrow합니다.
|
|
|
|
이 지점에는 오해하기 쉬운 차이가 있습니다. descriptor의 `APPLICATION_ADVANCED`가 자동으로 `SCRIPT` lane을 뜻하지 않습니다. registered script runner가 SCRIPT lane을 선택해야 account isolation이 적용됩니다. aggregate production DI가 확인되지 않으므로 모든 command가 이 경로를 탄다고 확대할 수 없습니다.
|
|
|
|
## Spring이 계산하는 lane ceiling
|
|
|
|
[`redisRuntimeOwner()`](/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/config/RedisSdkAutoConfiguration.java:273)은 settings에서 limit map을 만듭니다.
|
|
|
|
| lane | ceiling source | 기본값 |
|
|
|---|---|---:|
|
|
| REGULAR | `capacity.maximumInFlightCommands` | 64 |
|
|
| BLOCKING | `blocking.maxConnections` | 32 |
|
|
| TRANSACTION | `transaction.maxConnections` | 16 |
|
|
| SCRIPT | `capacity.maximumInFlightCommands` | 64 |
|
|
| PUBSUB | `max(1, pubsub.bufferCapacity / 64)` | 16 |
|
|
| ADMIN | admin enabled면 2, 아니면 1 | 1 |
|
|
|
|
각 값은 physical idle connection 수의 선할당이 아닙니다. owner constructor는 lane별 빈 `ArrayDeque`와 outstanding counter를 만들 뿐 connection을 열지 않습니다. limit은 동시에 대여된 lease 수의 ceiling입니다.
|
|
|
|
PUBSUB connection ceiling이 buffer capacity에서 파생되는 이유는 source에서 별도 설명되지 않습니다. 공식은 분명하지만 `64`의 운영 근거는 코드·테스트만으로 확인되지 않습니다. admin disabled 상태에도 ceiling 1과 pool은 존재하지만 admin surface production 조립은 확인되지 않습니다.
|
|
|
|
## borrow 호출 순서
|
|
|
|
[`borrow(kind, routingKey)`](/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/RedisRuntimeOwner.java:123)는 admission과 connection acquisition을 나눕니다.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant C as Caller
|
|
participant O as RedisRuntimeOwner
|
|
participant P as Idle deque
|
|
participant R as RedisRuntimeClient
|
|
C->>O: borrow(kind, routingKey)
|
|
O->>O: state == OPEN 확인
|
|
O->>O: outstanding < limit 확인 후 +1
|
|
alt routingKey 없음
|
|
O->>P: poll idle connection
|
|
P-->>O: connection 또는 null
|
|
end
|
|
alt idle 없음/죽음/routed lease
|
|
O->>R: openLane(kind, routingKey)
|
|
R-->>O: lane connection
|
|
end
|
|
O-->>C: RedisLease
|
|
```
|
|
|
|
monitor lock 안에서 먼저 state와 ceiling을 확인합니다. `OPEN`이 아니면 새 work를 거절합니다. outstanding이 limit에 도달했어도 기다리지 않고 즉시 `RedisCommandRejectedException`을 던집니다. failure metadata는 `notSent("CONNECTION", NONE, false, mode)`입니다. connection을 얻기 전에 거절했으므로 command는 전송되지 않았습니다.
|
|
|
|
admission을 통과하면 outstanding을 1 올립니다. routing key가 없을 때만 idle deque에서 connection을 꺼냅니다. idle connection의 `open()`이 false면 닫고 새로 엽니다. connection factory가 실패하면 counter를 되돌리고 예외를 그대로 던집니다.
|
|
|
|
`routingKey`가 있으면 pooled connection을 쓰지 않습니다. Cluster transaction connection은 이전 caller의 slot owner에 고정되어 있을 수 있기 때문입니다. Standalone/Sentinel은 routing key를 무시할 수 있지만 owner는 topology와 상관없이 routed lease를 non-reusable로 다루는 보수적인 정책을 사용합니다.
|
|
|
|
## return과 invalidate
|
|
|
|
owner가 반환하는 내부 [`Lease`](/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/RedisRuntimeOwner.java:269)는 `kind`, connection, `reusable`, `closed`를 가집니다.
|
|
|
|
- `gateway()`는 close 전까지만 접근할 수 있습니다.
|
|
- `invalidate()`는 `reusable=false`로 바꿉니다.
|
|
- `close()`는 synchronized이며 한 번만 `release()`를 호출합니다.
|
|
|
|
[`release()`](/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/RedisRuntimeOwner.java:168)는 outstanding을 1 줄입니다. reusable이고 owner가 여전히 OPEN이며 connection도 open이면 idle deque 뒤에 넣습니다. 그 외에는 connection을 닫습니다.
|
|
|
|
invalidate가 필요한 대표 사례는 transaction cleanup 실패입니다. DISCARD가 server에 도달하지 않았다면 connection에 MULTI window가 남아 있을 수 있습니다. 이를 pool에 돌려보내면 다음 caller command가 이전 transaction에 queue됩니다. Pub/Sub unsubscribe cleanup 실패도 같은 종류입니다.
|
|
|
|
close를 두 번 호출해도 counter는 한 번만 줄어듭니다. 이미 반환한 lease에서 gateway를 요청하면 `IllegalStateException`입니다. lease 누락은 hard ceiling의 한 자리를 영구 점유하므로 모든 사용자는 try-with-resources 또는 동등한 종료 경로를 가져야 합니다.
|
|
|
|
## pool의 실제 모양과 queue behavior
|
|
|
|
`RedisRuntimeOwner`의 pool은 lane별 `ArrayDeque<RedisLaneConnection>`입니다. background replenishment, min-idle, idle eviction, fairness queue는 없습니다.
|
|
|
|
- 첫 borrow가 connection을 엽니다.
|
|
- 정상 return이 idle deque에 connection을 보관합니다.
|
|
- 다음 borrow가 FIFO `poll()`로 재사용합니다.
|
|
- 죽은 idle connection은 borrow 시 발견해 교체합니다.
|
|
- limit 도달 시 대기 queue를 만들지 않습니다.
|
|
|
|
`app.redis.lifecycle.acquire-timeout`은 settings에 있고 양수 검증도 되지만 owner는 사용하지 않습니다. 현재 queue behavior는 “acquire timeout까지 기다림”이 아니라 즉시 rejection입니다.
|
|
|
|
`app.redis.limits.offline-queue-commands`도 binding되고 [`Limits.validate()`](/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/config/RedisSdkSettings.java:253)에서 양수 여부를 검사합니다. 그러나 production main source에는 [`getOfflineQueueCommands()`](/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/config/RedisSdkSettings.java:344)의 호출자가 없습니다. 따라서 이 값을 바꿔도 현행 driver request queue의 runtime ceiling은 바뀌지 않습니다.
|
|
|
|
Lettuce client 내부의 실제 `requestQueueSize`는 [`capacity.maximumInFlightCommands`](/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/RedisTopologyClientFactory.java:307)로 설정됩니다. connection lease ceiling과 driver command queue는 다른 층입니다. owner limit을 통과했다고 해서 driver queue가 반드시 여유 있다는 뜻은 아닙니다.
|
|
|
|
## lifecycle 상태 전이
|
|
|
|
owner state는 `OPEN`, `DRAINING`, `CLOSED` 세 개입니다.
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> OPEN
|
|
OPEN --> DRAINING: close() CAS 성공 / admission 중지
|
|
DRAINING --> DRAINING: outstanding lease bounded wait
|
|
DRAINING --> CLOSED: drain 완료 또는 timeout / idle close / client close
|
|
CLOSED --> CLOSED: 두 번째 close는 no-op
|
|
```
|
|
|
|
[`close()`](/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/RedisRuntimeOwner.java:197)의 순서는 다음과 같습니다.
|
|
|
|
1. atomic CAS로 `OPEN -> DRAINING`을 수행합니다. 실패하면 이미 닫는 중이거나 닫혔으므로 return합니다.
|
|
2. 새 borrow는 즉시 거절됩니다.
|
|
3. outstanding 합계가 0이 될 때까지 `drainTimeout` 안에서 monitor wait합니다.
|
|
4. deadline이 지나면 outstanding 수를 warning으로 남기고 계속 종료합니다.
|
|
5. 모든 idle deque를 비우고 pooled connection을 닫습니다.
|
|
6. 마지막에 runtime client를 닫습니다.
|
|
7. client close 성공 여부와 관계없이 state를 `CLOSED`로 설정합니다.
|
|
|
|
client가 마지막인 이유는 event loop가 in-flight command completion을 수행하기 때문입니다. 먼저 client를 닫으면 drain이 기다리던 작업 자체를 끊습니다.
|
|
|
|
outstanding lease가 drain timeout을 넘으면 owner는 해당 lease의 connection을 직접 목록으로 추적해 닫지 않습니다. client shutdown이 최종적으로 underlying connection/resource를 정리하지만 caller가 나중에 lease를 close할 때 owner counter가 CLOSED 상태에서 감소합니다. 상태와 counter는 diagnostic용이며 close 후 재사용은 허용되지 않습니다.
|
|
|
|
### Spring context에는 client close 경로가 하나 더 있습니다
|
|
|
|
위 상태 전이는 `RedisRuntimeOwner.close()` 자체에 idempotence가 있음을 보여 줍니다. 그러나 Spring production bean graph 전체에서 `client.close()`가 정확히 한 번만 호출된다는 뜻은 아닙니다.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant S as Spring context
|
|
participant O as RedisRuntimeOwner bean
|
|
participant C as RedisRuntimeClient bean
|
|
S->>O: explicit destroyMethod close()
|
|
O->>C: client.close()
|
|
O-->>S: owner CLOSED
|
|
S->>C: inferred destroy close()
|
|
```
|
|
|
|
[`redisRuntimeOwner()`](/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/config/RedisSdkAutoConfiguration.java:273)은 client bean에 의존하고 explicit `destroyMethod="close"`를 가집니다. 따라서 context는 owner를 먼저 destroy하고, owner는 내부에서 [`client.close()`](/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/RedisRuntimeOwner.java:227)를 호출합니다. 한편 [`redisRuntimeClient()`](/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/config/RedisSdkAutoConfiguration.java:226)는 destroy method inference를 끄지 않은 일반 `@Bean`입니다. 반환 type인 [`RedisRuntimeClient`](/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/RedisRuntimeClient.java:19)는 public no-arg `close()`를 가진 `AutoCloseable`입니다. Spring이 이어서 client bean의 inferred destroy method를 실행하면 같은 runtime client에 두 번째 `close()`가 들어갈 수 있습니다.
|
|
|
|
owner의 `CLOSED -> CLOSED` no-op은 두 번째 `owner.close()`만 막습니다. client bean을 직접 닫는 두 번째 경로에는 적용되지 않습니다. [`StandaloneRuntimeClient.close()`](/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/RedisTopologyClientFactory.java:395)와 [`ClusterRuntimeClient.close()`](/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/RedisTopologyClientFactory.java:470)에는 별도 closed guard가 없습니다. role router도 [`close()`가 호출될 때마다](/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/RedisTopologyClientFactory.java:515) 하위 client를 닫습니다. 현재 Lettuce가 반복 shutdown을 받아들일 수 있더라도, 이 구조만으로 lifecycle ownership이 exactly-once라고 말할 수는 없습니다.
|
|
|
|
## `RedisConnectionRegistry`와 현행 owner를 구분합니다
|
|
|
|
[`RedisConnectionRegistry`](/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/RedisConnectionRegistry.java:13)는 문서 주석에서 “five connection lanes”라고 쓰지만 enum은 현재 여섯 개입니다. constructor는 enum 전체에 positive limit을 요구하므로 실행 의미는 여섯 lane입니다. 주석이 drift했습니다.
|
|
|
|
이 class는 counter를 atomic increment하고 limit 초과 시 즉시 거절하지만 connection을 `Object`로 반환하고 close 시 counter만 0으로 만듭니다. production source에서 `new RedisConnectionRegistry(...)` 호출은 확인되지 않았고 단위 테스트만 생성합니다.
|
|
|
|
현행 production bean은 `RedisRuntimeOwner`입니다. typed gateway, idle connection 실제 close, invalidate, lifecycle state, bounded drain, client shutdown을 가진 쪽도 owner입니다. `RedisConnectionRegistryTest`의 계약을 production lifecycle 증거로 직접 쓰면 안 됩니다.
|
|
|
|
## 정상·실패·degraded 분기
|
|
|
|
### 정상
|
|
|
|
- OPEN + ceiling 미만: idle connection 재사용 또는 새 connection open
|
|
- lease close + healthy reusable connection: 같은 lane idle deque로 return
|
|
- routed/invalidate/dead connection: close하고 counter만 반환
|
|
- close + 빠른 lease return: drain 완료 후 pool과 client shutdown
|
|
|
|
### admission 거절
|
|
|
|
- DRAINING/CLOSED에서 borrow
|
|
- 해당 lane outstanding이 ceiling 이상
|
|
|
|
둘 다 Redis에 command를 보내기 전 `RedisCommandRejectedException`입니다. 다른 lane counter는 소비하지 않으므로 blocking saturation이 regular lane을 직접 줄이지 않습니다.
|
|
|
|
### connection open 실패
|
|
|
|
endpoint, authentication, TLS handshake가 실패하면 outstanding을 되돌리고 예외를 전달합니다. command 실행 이전일 수 있지만 driver failure 번역은 이 owner가 하지 않습니다.
|
|
|
|
### shutdown timeout
|
|
|
|
drain timeout은 startup/runtime availability 상태를 failure로 바꾸지 않고 warning을 남긴 뒤 close를 계속합니다. 종료 과정의 degraded branch이며 요청 결과의 execution certainty를 판정하지 않습니다.
|
|
|
|
## 테스트가 고정하는 계약
|
|
|
|
[`RedisRuntimeOwnerTest`](/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/RedisRuntimeOwnerTest.java:20)는 server 없이 lifecycle을 직접 검사합니다.
|
|
|
|
- [`aLeaseIsReturnedAndPooled()`](/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/RedisRuntimeOwnerTest.java:44): close once, double-close no-op, pool reuse
|
|
- [`anInvalidatedConnectionIsNotReused()`](/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/RedisRuntimeOwnerTest.java:76): invalidated transaction connection close
|
|
- [`anExhaustedLaneRefuses()`](/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/RedisRuntimeOwnerTest.java:92): queue 대신 즉시 rejection
|
|
- [`closingStopsAdmissionFirst()`](/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/RedisRuntimeOwnerTest.java:106): DRAINING에서 새 lease 거절
|
|
- [`theClientShutsDownLast()`](/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/RedisRuntimeOwnerTest.java:136): connection close 뒤 client shutdown
|
|
- [`aDeadPooledConnectionIsReplaced()`](/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/RedisRuntimeOwnerTest.java:163): idle-dead replacement
|
|
|
|
[`RedisConnectionRegistryTest`](/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/RedisConnectionRegistryTest.java:18)는 lane routing과 counter 격리를 고정하지만 legacy/non-production class의 단위 계약입니다.
|
|
|
|
[`LiveRedisCompositionTest.aLeaseReachesTheServer()`](/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/config/LiveRedisCompositionTest.java:104)는 PING 뒤 outstanding이 0인지 확인합니다. [`closingTheContextTearsEverythingDown()`](/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/config/LiveRedisCompositionTest.java:135)는 context 종료 뒤 Lettuce thread 수가 원래 수준으로 돌아오는지 확인합니다. 이들은 opt-in real-server lane이며 이번 문서 작업에서는 실행하지 않았습니다.
|
|
|
|
직접 owner test의 [`theClientShutsDownLast()`](/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/RedisRuntimeOwnerTest.java:136)는 fake client가 connection 뒤에 닫히는 순서를, [`closingTwiceIsIdempotent()`](/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/RedisRuntimeOwnerTest.java:152)는 `owner.close()`를 두 번 불러도 fake client shutdown이 한 번임을 고정합니다. 둘 다 Spring이 client bean을 별도로 destroy하는 경로는 포함하지 않습니다. auto-configuration test의 [`theRuntimeOwnerFollowsTheContext()`](/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/config/RedisSdkAutoConfigurationTest.java:418)는 owner state만, live test는 남은 Lettuce thread만 확인합니다. Spring context에서 runtime client의 `close()` 호출 횟수를 세는 테스트는 없어 exactly-once ownership은 검증되지 않았습니다.
|
|
|
|
## 현재 구현 공백과 다음 source 순서
|
|
|
|
- `RedisConnectionRegistry`는 production 미사용이며 주석의 five-lane 표기도 enum과 drift했습니다.
|
|
- acquire timeout, min-idle, fairness queue, idle eviction은 구현되지 않았습니다.
|
|
- `limits.offlineQueueCommands`는 binding·validation만 되고 production queue 구성에는 쓰이지 않습니다. 실제 Lettuce `requestQueueSize`는 `capacity.maximumInFlightCommands`를 사용합니다.
|
|
- connection limit은 concurrent lease 수이고 command in-flight byte/reply byte ceiling enforcement와 같지 않습니다.
|
|
- aggregate command executor production DI가 없어 모든 typed operation이 owner admission과 observation path를 일관되게 거친다고 확인할 수 없습니다.
|
|
- PUBSUB ceiling의 `/64` 근거와 admin disabled 상태의 limit 1 이유는 source에서 설명되지 않습니다.
|
|
- drain timeout을 넘긴 outstanding command의 실행 결과는 owner가 판정하지 않습니다.
|
|
- Spring context에는 owner를 통한 close와 client bean inferred destroy가 겹치는 경로가 있습니다. 별도 source 변경에서 lifecycle authority를 owner 하나로 모으려면 client bean에 `@Bean(destroyMethod = "")`를 명시하되 생성 실패 cleanup을 보존해야 합니다. 두 경로를 유지한다면 runtime client close를 idempotent하게 만들어 반복 shutdown을 안전하게 처리할 수 있습니다. 어느 선택이든 context-level close-count test가 필요하며 현행 구현에는 없습니다.
|
|
|
|
다음에는 [`RedisConnectionKind`](/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/RedisConnectionKind.java:21), [`RedisRuntimeOwner.borrow()`](/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/RedisRuntimeOwner.java:123), [`release()`](/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/RedisRuntimeOwner.java:168), [`close()`](/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/RedisRuntimeOwner.java:197) 순서로 읽으면 됩니다.
|
|
|
|
관련 시리즈 주제는 executor timeout과 execution certainty입니다.
|
|
|
|
## 시리즈에서 이어 읽기
|
|
|
|
- 이전 글: [하나의 설정에서 세 topology로: RedisTopologyClientFactory 코드 읽기](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-topology-client-factory.md)
|
|
- 다음 글: [YAML 한 줄이 Redis 명령을 거절하기까지: Policy Loader·Catalog·Guard](/home/donghyeon/workspace/ai-tool/document-haness/.run/redis/redis-command-policy-admission.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)
|
|
|