<!-- task=m-large-payload-followup-optimization/03_kotlin_tcp_parallel plan=0 tag=PERF -->

# Code Review Reference - PERF

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.

## 개요

date=2026-06-14
task=m-large-payload-followup-optimization/03_kotlin_tcp_parallel, plan=0, tag=PERF

## Roadmap Targets

- Milestone: `agent-roadmap/milestones/large-payload-followup-optimization.md`
- Task ids:
  - `kotlin-tcp-parallel-unskip-path`: Kotlin TCP parallel clients>=128 required skip 해제
- Completion mode: check-on-pass

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [PERF-1] TCP blocking read loop 격리 | [x] |
| [PERF-2] required skip 제거와 full stress 측정 | [x] |

## 구현 체크리스트

- [x] [PERF-1] Kotlin TCP read loop가 Dispatchers.IO starvation을 만들지 않도록 bounded read timeout과 격리된 read execution 전략을 적용한다.
- [x] [PERF-2] Kotlin stress의 TCP parallel required skip을 제거하고 clients=128/512/1024 row를 실제 측정한다.
- [x] Kotlin TCP full parallel stress를 실행해 required skip 0, timeout/nonce/type/FIFO/pending leak 0을 확인한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block을 확인한다.
- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
- [x] FAIL이고 user-review gate가 트리거되지 않았으므로 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

- read dispatcher 구현 방식: 계획은 `tcpReadDispatcher`라는 이름만 제시했다. 실제로는 모든 `TcpClient`가 공유하는 companion-level `readDispatcher`를 `Executors.newCachedThreadPool` + daemon thread factory(`proto-socket-tcp-read-N`)로 구현했다. 이유: cached pool은 연결 수에 맞춰 read 스레드가 늘고 disconnect 후 회수되며, daemon이라 JVM 종료를 막지 않는다. per-client dispatcher를 만들면 client 수만큼 pool이 생겨 자원 낭비라 process-wide 단일 풀로 두었다.
- `BaseClient.kt` 변경 없음: 계획 체크리스트는 "필요 시 dispatcher close/lifecycle hook 추가"였다. read dispatcher가 `Dispatchers.IO`처럼 process-wide daemon 풀이고 read 코루틴은 `scope`(BaseClient 소유)에서 launch되어 `scope.cancel()`로 종료되므로, per-client lifecycle hook이 불필요했다. read 스레드는 코루틴 취소 후 `return@launch`로 풀에 반환된다. 따라서 `BaseClient.kt`는 수정하지 않았다.
- 테스트: 계획의 "regression test 권장"에 따라 `testTcpCloseCompletesWhileReadParked`를 추가했다. read가 soTimeout poll로 parked된 상태(>1 poll 주기 경과)에서 close가 2초 내 완료되는지 검증한다. 기존 `testTcpConcurrentRequests`가 concurrent request 회귀를 이미 커버하므로 별도 추가는 하지 않았다.

## 주요 설계 결정

- **starvation 근본 원인 분리**: deadlock의 원인은 connection당 blocking read가 write/heartbeat/accept와 같은 `Dispatchers.IO` 스레드 풀을 두고 경쟁한 것이다. read 전용 dispatcher로 풀을 물리적으로 분리해, read 스레드가 아무리 늘어도 write 경로 스레드를 굶기지 않게 했다. 이것이 clients>=128 unskip의 핵심이다.
- **soTimeout poll(1000ms)**: `socket.soTimeout`을 설정하고 `SocketTimeoutException`을 정상 흐름(`continue`)으로 처리한다. 이로써 (1) blocking read가 영구 park되지 않고 주기적으로 `isAlive()`를 재확인하므로 close가 socket close 도착에만 의존하지 않고 즉시 루프를 빠져나가고, (2) read 스레드가 무한 점유 상태로 고착되지 않는다. EOF/IOException 등 다른 예외는 기존 disconnect semantics(`onDisconnected()` 후 종료)를 그대로 유지한다.
- **disconnect/close semantics 불변**: `SocketTimeoutException`만 정상 poll로 분기하고, 그 외 모든 예외와 frame 오류, length 범위 위반은 기존과 동일하게 `onDisconnected()` → `return@launch`로 처리한다. close 멱등성(`testTcpClientCloseIdempotent`)과 server stop 시 disconnect(`testTcpServerStopDisconnectsClients`)도 회귀 통과했다.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- read loop 변경이 close/disconnect semantics와 resource lifecycle을 깨지 않았는지 확인한다.
- `parallelDeadlockBottleneck` required skip이 runtime fix 후 제거됐는지 확인한다.
- full stress에 clients=128/512/1024 rows가 실제로 출력됐는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

### PERF-1 중간 검증
```text
$ cd kotlin && ./gradlew --offline test
> Task :compileKotlin
> Task :compileJava
> Task :classes
> Task :compileTestKotlin
> Task :test

BUILD SUCCESSFUL in 17s
11 actionable tasks: 3 executed, 8 up-to-date
EXIT=0
```
신규 `testTcpCloseCompletesWhileReadParked` 포함 TcpTest 전 테스트 PASS.

### PERF-2 중간 검증
```text
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang kotlin --transport tcp --profile parallel
RUN stress lang=kotlin mode=full transports=tcp profiles=parallel
INFO stress harness language=Kotlin mode=full transports=tcp profiles=parallel typeName=TestData
[parallel] transport=tcp mode=full clients=[16, 128, 512, 1024] perClient=500
[parallel] transport=tcp clients=16 done violations=0
[parallel] transport=tcp clients=128 done violations=0
[parallel] transport=tcp clients=512 done violations=0
[parallel] transport=tcp clients=1024 done violations=0
DONE kotlin: PASS (rows=4 skips=0 required_skips=0 violations=0)

전체 결과값: PASS
결과 기록 파일: `agent-test/runs/20260614-074923-proto-socket-stress-full.md`
EXIT=0
```

측정 결과(record `agent-test/runs/20260614-074923-proto-socket-stress-full.md`):

| Profile | Axis | Clients | Requests | Timeout | NonceMis | TypeMis | FIFOViol | PendLeak | 결과 |
|---|---|---:|---:|---:|---:|---:|---:|---:|---|
| parallel | clients=16 | 16 | 8000 | 0 | 0 | 0 | 0 | 0 | PASS |
| parallel | clients=128 | 128 | 64000 | 0 | 0 | 0 | 0 | 0 | PASS |
| parallel | clients=512 | 512 | 256000 | 0 | 0 | 0 | 0 | 0 | PASS |
| parallel | clients=1024 | 1024 | 512000 | 0 | 0 | 0 | 0 | 0 | PASS |

clients=128/512/1024 row가 SKIP 없이 실제 측정됨. required skip 0, 모든 stability counter 0. 이전 milestone에서 clients=128 측정 시 3시간+ 멈추던 deadlock이 해소됨.

### 최종 검증
```text
$ cd kotlin && ./gradlew --offline test
BUILD SUCCESSFUL in 4s
11 actionable tasks: 1 executed, 10 up-to-date
EXIT=0

$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang kotlin --transport tcp --profile parallel
DONE kotlin: PASS (rows=4 skips=0 required_skips=0 violations=0)
전체 결과값: PASS
EXIT=0
```
두 명령 모두 exit 0. full stress에서 Kotlin TCP parallel clients=16/128/512/1024가 required SKIP 없이 측정되고 timeout/nonce/type/FIFO/pending leak 모두 0.

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**

## 섹션 소유권

| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | 구현 에이전트는 수정하지 않는다. |
| Roadmap Targets | Fixed at stub creation from plan | PASS 시 code-review가 `complete.log`의 Roadmap Completion 근거로 복사한다. |
| 구현 항목별 완료 여부 | Implementing agent | 구현 에이전트가 `[ ]`를 `[x]`로 갱신한다. |
| 구현 체크리스트 | Implementing agent | final checkbox까지 반드시 채운다. |
| 코드리뷰 전용 체크리스트 | Review agent only | 구현 에이전트는 수정하거나 체크하지 않는다. |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | 실제 구현 결과로 placeholder를 교체한다. |
| 사용자 리뷰 요청 | Implementing agent | 사용자 결정이 필요할 때만 `상태: 없음`을 교체한다. |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | 리뷰 초점이다. |
| 검증 결과 | Implementing agent | 실제 stdout/stderr를 붙인다. |

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가
  - Correctness: Fail
  - Completeness: Fail
  - Test coverage: Fail
  - API contract: Fail
  - Code quality: Pass
  - Plan deviation: Pass
  - Verification trust: Pass
- 발견된 문제
  - Required: `kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt:100`에서 모든 `SocketTimeoutException`을 정상 poll로 처리하고 frame read를 처음부터 다시 시작합니다. `DataInputStream.readInt()`/`readFully()`는 header 또는 payload 일부를 이미 소비한 뒤 다음 underlying read에서 timeout이 나면 그 부분 소비 상태를 되돌리지 않습니다. 이 경우 느린 TCP peer가 header 일부, header 전체 뒤 payload 일부, 또는 대용량 payload chunk 사이를 `READ_POLL_TIMEOUT_MS`보다 길게 지연하면 stream 위치가 frame 중간으로 밀린 채 다음 `readInt()`가 payload bytes를 length로 해석해 연결을 끊거나 subsequent frame을 오염시킵니다. TCP framing 계약을 깨는 correctness/API 문제이므로, timeout-aware `readExact`가 header/payload offset을 보존하면서 timeout마다 `isAlive()`만 재확인하도록 고치고, payload chunk 사이 timeout을 재현하는 회귀 테스트를 추가해야 합니다.
- 다음 단계
  - WARN/FAIL 후속: 부분 frame timeout 보존 로직과 회귀 테스트를 구현하는 다음 `PLAN-cloud-G07.md` / `CODE_REVIEW-cloud-G07.md`를 작성한다.
