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

# Kotlin TCP Partial Frame Timeout Follow-up Plan

## 이 파일을 읽는 구현 에이전트에게

이 plan의 구현 완료 조건에는 대응 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성이 포함된다. 구현 후 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 채운 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret/서비스 준비, 또는 구현을 안전하게 계속할 수 없는 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 재실행으로 닫을 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

이전 구현은 Kotlin TCP read loop를 `Dispatchers.IO`에서 read 전용 dispatcher로 분리하고 `socket.soTimeout` poll을 적용해 clients>=128 parallel stress skip을 제거했다. 그러나 `DataInputStream.readInt()`/`readFully()`가 header 또는 payload 일부를 이미 소비한 뒤 `SocketTimeoutException`을 던지는 경우, 현재 catch가 outer frame loop로 돌아가면서 stream 위치가 frame 중간으로 밀린다. TCP는 chunk 경계를 보장하지 않으므로, 느린 peer나 대용량 payload chunk 사이 지연에서 frame desync가 발생할 수 있다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 결정이 필요한 경우 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 구현 에이전트는 직접 사용자 프롬프트를 만들지 않으며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 판단한다.

## 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

## 리뷰에서 발견된 Required

- `kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt:100`: `SocketTimeoutException`을 frame 경계와 무관하게 정상 poll로 처리해, header/payload 일부 소비 뒤 timeout이 난 경우 다음 `readInt()`가 frame 중간 bytes를 length로 해석한다. timeout-aware exact read가 현재 buffer offset을 보존해야 한다.

## 범위 결정 근거

- read 전용 dispatcher와 TCP parallel required skip 제거 방향은 유지한다.
- WebSocket, protocol schema, stress matrix 축 변경은 범위에서 제외한다.
- required skip을 되살리거나 clients=128/512/1024 row를 축소하는 방식은 완료로 인정하지 않는다.

## 빌드 등급

- `cloud-G07`: TCP stream framing, timeout, coroutine dispatcher, high-client stress가 얽힌 concurrency/protocol 작업이고 verification이 terminal stress record에 의존한다.

## 구현 체크리스트

- [ ] [REVIEW_PERF-1] `TcpClient` read loop가 `SocketTimeoutException` 후에도 현재 frame header/payload offset을 보존하도록 timeout-aware exact read로 수정한다.
- [ ] [REVIEW_PERF-2] payload 또는 header chunk 사이가 `READ_POLL_TIMEOUT_MS`보다 길어도 frame이 desync되지 않는 Kotlin TCP 회귀 테스트를 추가한다.
- [ ] Kotlin unit test와 Kotlin TCP full parallel stress를 실행해 unit PASS, required skip 0, timeout/nonce/type/FIFO/pending leak 0을 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_PERF-1] partial frame timeout 보존

문제: `TcpClient.readLoop()`에서 `readInt()`나 `readFully()`가 부분 frame을 소비한 뒤 timeout을 던지면 catch가 outer loop로 돌아가 frame 경계를 잃는다.

해결 방법:

```kotlin
// shape
private fun readExact(input: DataInputStream, target: ByteArray): Boolean {
    var offset = 0
    while (offset < target.size && communicator.isAlive()) {
        try {
            val read = input.read(target, offset, target.size - offset)
            if (read < 0) return false
            offset += read
        } catch (_: SocketTimeoutException) {
            continue
        }
    }
    return offset == target.size
}
```

- 4-byte header도 byte buffer로 읽고 offset을 보존한 뒤 `ByteBuffer.wrap(header).int`로 해석한다.
- payload도 같은 방식으로 현재 offset을 보존한다.
- timeout은 같은 frame read 안에서만 poll로 처리한다. EOF, invalid length, parse/frame 오류는 기존처럼 disconnect한다.
- close/disconnect semantics와 read dispatcher 분리는 유지한다.

수정 파일 및 체크리스트:

- [ ] `kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt`: `readInt/readFully` 직접 호출을 offset-preserving read helper로 교체.
- [ ] `kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt`: timeout, EOF, invalid length, frame decode 오류의 disconnect semantics를 명확히 유지.

중간 검증:

```bash
cd kotlin && ./gradlew --offline test
```

예상 결과: exit 0.

### [REVIEW_PERF-2] delayed partial frame 회귀 테스트

문제: 기존 `testTcpCloseCompletesWhileReadParked`와 full stress는 idle poll 및 fast local loopback을 검증하지만, frame bytes 사이 지연이 poll timeout을 넘는 케이스를 검증하지 않는다.

해결 방법:

- `kotlin/src/test/kotlin/com/tokilabs/proto_socket/TcpTest.kt`에 deterministic regression test를 추가한다.
- 서버 측 raw socket이 `PacketBase` frame을 직접 만들고 header 또는 payload를 둘 이상 chunk로 나누어 쓴다.
- 첫 chunk write 후 `kotlinx.coroutines.delay(1_200)` 이상 대기한 뒤 나머지 chunk를 flush한다.
- client listener가 `TestData`를 정상 수신하고 `communicator.isAlive()`가 유지되는지 확인한다.
- 가능하면 payload chunk split을 우선 검증하고, header split도 같은 helper로 추가 검증할 수 있다.

수정 파일 및 체크리스트:

- [ ] `kotlin/src/test/kotlin/com/tokilabs/proto_socket/TcpTest.kt`: delayed partial frame test 추가.
- [ ] test가 현재 bug를 재현할 수 있도록 chunk 사이 지연이 read poll timeout보다 길게 설정됐는지 확인.

중간 검증:

```bash
cd kotlin && ./gradlew --offline test
```

예상 결과: exit 0, 신규 delayed partial frame test PASS.

## 최종 검증

```bash
cd kotlin && ./gradlew --offline test
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang kotlin --transport tcp --profile parallel
```

예상 결과: 모든 명령 exit 0. full stress 결과에서 Kotlin TCP parallel clients=16/128/512/1024가 required SKIP 없이 측정되고 timeout/nonce/type/FIFO/pending leak 모두 0이다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
