feat: large-payload-followup-optimization task completion

- Kotlin: TCP parallel test, WebSocket latency fix, stress test updates
- TypeScript: TCP client, WebSocket client/server fixes, stress test, WS test updates
- Archive completed subtasks (03_kotlin_tcp_parallel, 04_ws_fixed_latency)
- Update roadmap milestone
This commit is contained in:
toki 2026-06-14 20:07:07 +09:00
parent d2cee0ab9b
commit 71e709d30d
20 changed files with 1052 additions and 265 deletions

View file

@ -44,9 +44,9 @@
후보별로 구현 위험과 검증 방법을 분리한다.
- [ ] [typescript-ws-1mb-path] TypeScript WS 1MB 병목이 야간에도 유지되면 mask/protobuf/parse queue 경로를 우선 분석한다. 구현 계획: `agent-task/m-large-payload-followup-optimization/01_typescript_ws_1mb/PLAN-local-G06.md`
- [ ] [typescript-tcp-1024-tail-path] TypeScript TCP `parallel clients=1024` timeout tail을 focused stress로 재현하고, write queue 대기, socket write/drain, read parse queue, request timeout 시작점 중 어느 경로가 15초 hard gate를 넘기는지 계측한 뒤 transport/communicator 수정 계획을 작성한다. 구현 계획: `agent-task/m-large-payload-followup-optimization/02_typescript_tcp_tail/PLAN-cloud-G07.md`. 검증: `run_stress.sh --full --lang typescript --transport tcp --profile parallel`에서 1024-client row timeout 0을 확인하고, `run_performance.sh --quick`과 다음 야간 `run_performance.sh --full`에서 same-language hard gate 0을 재확인한다.
- [ ] [kotlin-tcp-parallel-unskip-path] Kotlin TCP `parallel clients>=128` required skip을 해제하기 위해 blocking read thread-per-connection 병목과 bounded read timeout 부재를 분리 분석하고, 128 -> 512 -> 1024 순서로 측정 가능하게 만드는 수정 계획을 작성한다. 구현 계획: `agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/PLAN-cloud-G07.md`. 검증: `run_stress.sh --full --lang kotlin --transport tcp --profile parallel`에서 required skip 없이 128/512/1024 rows가 기록되고 stability hard gate 0을 만족한다.
- [x] [typescript-ws-1mb-path] TypeScript WS 1MB 병목이 야간에도 유지되면 mask/protobuf/parse queue 경로를 우선 분석한다. 완료 근거: `agent-task/archive/2026/06/m-large-payload-followup-optimization/01_typescript_ws_1mb/complete.log`; 검증: `npm run check`, `npm test -- test/ws.test.ts test/communicator.test.ts`, `run_stress.sh --full --lang typescript --transport ws --profile payload`, `agent-test/runs/20260613-220314-proto-socket-stress-full.md`.
- [x] [typescript-tcp-1024-tail-path] TypeScript TCP `parallel clients=1024` timeout tail을 focused stress로 재현하고, write queue 대기, socket write/drain, read parse queue, request timeout 시작점 중 어느 경로가 15초 hard gate를 넘기는지 계측한 뒤 transport/communicator 수정 계획을 작성한다. 완료 근거: `agent-task/archive/2026/06/m-large-payload-followup-optimization/02_typescript_tcp_tail/complete.log`; 검증: `agent-test/runs/20260614-072352-proto-socket-stress-full.md`, `agent-test/runs/20260614-072436-proto-socket-performance-quick.md`.
- [x] [kotlin-tcp-parallel-unskip-path] Kotlin TCP `parallel clients>=128` required skip을 해제하기 위해 blocking read thread-per-connection 병목과 bounded read timeout 부재를 분리 분석하고, 128 -> 512 -> 1024 순서로 측정 가능하게 만드는 수정 계획을 작성한다. 완료 근거: `agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/complete.log`; 검증: `cd kotlin && ./gradlew --offline test`, `run_stress.sh --full --lang kotlin --transport tcp --profile parallel`, `agent-test/runs/20260614-082714-proto-socket-stress-full.md`.
- [ ] [ws-fixed-latency-path] WS 계열 fixed latency가 야간에도 유지되면 transport별 공통 지연 경로와 언어별 worker/gateway path를 분리해 분석한다. 구현 계획: `agent-task/m-large-payload-followup-optimization/04_ws_fixed_latency/PLAN-cloud-G07.md`
- [x] [kotlin-ws-guard] Kotlin WS 1MB는 야간 record에서 개선 유지 여부를 확인하고, 회귀가 보이면 별도 구현 대상으로 승격한다. 검증: `agent-test/runs/20260610-124006-proto-socket-stress-full.md`, `agent-test/runs/20260610-131132-proto-socket-stress-full.md`, `agent-test/runs/20260613-141506-proto-socket-stress-full.md`의 Kotlin WS 1MB rows가 모두 stability PASS이고 queue/backlog leak 없이 유지되어 이번 후보 창에서는 별도 구현 대상으로 승격하지 않는다.
- [ ] [dart-large-packet-guard] Dart TCP/WS 1MB는 기존 개선이 유지되는지 감시하고, 회귀가 보이면 guard 또는 path hardening 대상으로 승격한다. 구현 계획: `agent-task/m-large-payload-followup-optimization/05_dart_large_packet_guard/PLAN-local-G05.md`

View file

@ -0,0 +1,181 @@
<!-- 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`를 작성한다.

View file

@ -0,0 +1,202 @@
<!-- task=m-large-payload-followup-optimization/03_kotlin_tcp_parallel plan=1 tag=REVIEW_PERF -->
# Code Review Reference - REVIEW_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; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-14
task=m-large-payload-followup-optimization/03_kotlin_tcp_parallel, plan=1, tag=REVIEW_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]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_PERF-1] partial frame timeout 보존 | [x] |
| [REVIEW_PERF-2] delayed partial frame 회귀 테스트 | [x] |
## 구현 체크리스트
- [x] [REVIEW_PERF-1] `TcpClient` read loop가 `SocketTimeoutException` 후에도 현재 frame header/payload offset을 보존하도록 timeout-aware exact read로 수정한다.
- [x] [REVIEW_PERF-2] payload 또는 header chunk 사이가 `READ_POLL_TIMEOUT_MS`보다 길어도 frame이 desync되지 않는 Kotlin TCP 회귀 테스트를 추가한다.
- [x] Kotlin unit test와 Kotlin TCP full parallel stress를 실행해 unit PASS, 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이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/`를 `agent-task/archive/YYYY/MM/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-large-payload-followup-optimization`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-large-payload-followup-optimization/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
- 계획 shape의 `readExact`는 `communicator.isAlive()`를 while 조건에 두었다. 실제 구현은 `while (offset < target.size)` 루프 안 첫 줄에서 `if (!communicator.isAlive()) return false`로 체크한다. 동작은 동일하며, "다 읽었으면 true, close/EOF면 false" 반환 계약을 명확히 하기 위한 구조 차이일 뿐이다.
- read 루프에서 close(`isAlive=false`)로 인한 `readExact` false와 EOF(`read<0`)로 인한 false를 호출부에서 `if (communicator.isAlive()) onDisconnected()`로 구분한다. close면 disconnect를 다시 호출하지 않고 조용히 종료하고, EOF면 disconnect한다. 이로써 기존 close 멱등성을 유지한다.
- 검증 명령은 계획과 동일하게 실행했다(대체 없음). stress 결과 record는 `agent-test/runs/20260614-081249-proto-socket-stress-full.md`.
## 주요 설계 결정
- **offset-preserving read**: `DataInputStream.readInt()`/`readFully()`는 `SocketTimeoutException` 발생 시 이미 소비한 byte를 호출자가 복구할 수 없어 frame 경계가 깨진다. 단일 `readExact(input, target)` helper로 header(4-byte buffer)와 payload를 모두 읽고, timeout은 **같은 read 안에서만** `continue`로 흡수해 `offset`을 보존한다. header는 `ByteBuffer.wrap(header).int`로 length를 파싱한다. 이것이 리뷰가 지적한 partial-frame desync의 근본 수정이다.
- **disconnect semantics 유지**: timeout=같은 frame 내 poll, EOF(read<0)=disconnect, invalid length(`<0` 또는 `>MAX_PACKET_SIZE`)=disconnect, frame decode 오류(`onReceivedFrame != null`)=disconnect, close(`isAlive=false`)=조용히 종료. PERF-1에서 도입한 read 전용 dispatcher 분리와 soTimeout poll 방향은 그대로 유지한다.
- **deterministic 회귀 테스트**: `testTcpDelayedPartialFrameDoesNotDesync`는 raw `ServerSocket`이 `4-byte length + PacketBase(TestData)` frame을 직접 만들어, (1) payload를 마지막 3 byte 앞에서 자르고 (2) header(length prefix)를 2 byte에서 자른 뒤, 각 chunk 사이에 1_200ms(>READ_POLL_TIMEOUT_MS=1000ms) 지연을 둔다. offset 보존이 없으면 chunk 사이 timeout 후 desync로 frame을 못 받아 test가 timeout/실패한다. 두 frame 모두 정상 수신되고 `isAlive()`가 유지됨을 확인한다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- timeout-aware exact read가 header/payload offset을 보존하고, timeout 뒤 outer frame loop로 restart하지 않는지 확인한다.
- delayed partial frame test가 `READ_POLL_TIMEOUT_MS`보다 긴 chunk gap을 만들어 기존 desync bug를 재현 가능한 방식으로 막는지 확인한다.
- 기존 Kotlin TCP full parallel stress clients=128/512/1024 row가 required SKIP 없이 계속 측정되는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### REVIEW_PERF-1 중간 검증
```text
$ cd kotlin && ./gradlew --offline compileKotlin
> Task :compileKotlin
BUILD SUCCESSFUL in 7s
4 actionable tasks: 1 executed, 3 up-to-date
EXIT=0
```
offset-preserving `readExact` 적용 후 compile PASS.
### REVIEW_PERF-2 중간 검증
```text
$ cd kotlin && ./gradlew --offline test --tests "com.tokilabs.proto_socket.TcpTest"
> Task :compileTestKotlin
> Task :test
BUILD SUCCESSFUL in 10s
11 actionable tasks: 2 executed, 9 up-to-date
EXIT=0
```
신규 `testTcpDelayedPartialFrameDoesNotDesync`(payload split + header split, chunk 사이 1_200ms 지연) 포함 TcpTest 전 테스트 PASS.
### 최종 검증
```text
$ cd kotlin && ./gradlew --offline test
> Task :test
BUILD SUCCESSFUL in 14s
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
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-081249-proto-socket-stress-full.md`
EXIT=0
```
측정 결과(record `agent-test/runs/20260614-081249-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=16/128/512/1024 row가 required SKIP 없이 측정되고 모든 stability counter 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?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| 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를 붙인다. |
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가
- Correctness: Pass
- Completeness: Pass
- Test coverage: Pass
- API contract: Pass
- Code quality: Pass
- Plan deviation: Pass
- Verification trust: Pass
- 발견된 문제: 없음
- 다음 단계
- PASS: `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다.

View file

@ -0,0 +1,44 @@
# Complete - m-large-payload-followup-optimization/03_kotlin_tcp_parallel
## 완료 일시
2026-06-14T08:28:02Z
## 요약
Kotlin TCP read loop starvation 및 partial-frame timeout desync를 수정하고 TCP full parallel required skip 해제를 검증했다. loop count=2, final verdict=PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | `SocketTimeoutException` 후 partial frame offset을 잃는 Required issue 발견 |
| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | PASS | offset-preserving read와 delayed partial frame regression test로 Required issue 해소 |
## 구현/정리 내용
- Kotlin `TcpClient` TCP read loop를 read 전용 dispatcher로 분리하고 `socket.soTimeout` 기반 poll을 적용했다.
- header/payload read를 offset-preserving `readExact`로 바꿔 timeout이 frame 중간에서 발생해도 desync되지 않게 했다.
- TCP parallel clients>=128 required skip을 제거하고 clients=16/128/512/1024 full parallel row를 실제 측정했다.
- `TcpTest`에 close-while-read-parked 및 delayed partial frame(header split, payload split) 회귀 테스트를 추가했다.
## 최종 검증
- `cd kotlin && ./gradlew --offline test` - PASS; `BUILD SUCCESSFUL in 2s`, 11 actionable tasks 중 1 executed, 10 up-to-date.
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang kotlin --transport tcp --profile parallel` - PASS; `DONE kotlin: PASS (rows=4 skips=0 required_skips=0 violations=0)`, result record=`agent-test/runs/20260614-082714-proto-socket-stress-full.md`.
- `git diff --check` - PASS; whitespace/error output 없음.
## Roadmap Completion
- Milestone: `agent-roadmap/milestones/large-payload-followup-optimization.md`
- Completed task ids:
- `kotlin-tcp-parallel-unskip-path`: PASS; evidence=`agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_1.log`; verification=`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`, `agent-test/runs/20260614-082714-proto-socket-stress-full.md`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,120 @@
<!-- 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,231 @@
<!-- task=m-large-payload-followup-optimization/04_ws_fixed_latency 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`.
> 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/04_ws_fixed_latency, plan=0, tag=PERF
## Roadmap Targets
- Milestone: `agent-roadmap/milestones/large-payload-followup-optimization.md`
- Task ids:
- `ws-fixed-latency-path`: WS 계열 fixed latency가 지속되면 common/worker/gateway 경로 분석
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [PERF-1] low-latency 설정 및 phase 계측 | [x] |
| [PERF-2] fixed latency row 수집 | [x] |
## 구현 체크리스트
- [x] [PERF-1] TypeScript/Kotlin WS/TCP socket 경로의 low-latency 설정과 phase 계측을 추가해 gateway off/on과 transport latency를 분리한다.
- [x] [PERF-2] fixed latency 재현 stress를 실행하고 개선 여부와 남은 병목 후보를 review stub에 수치로 기록한다.
- [x] TypeScript gateway/WS roundtrip 및 Kotlin WS quick stress를 실행해 correctness와 stability hard gate가 유지되는지 확인한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [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을 확인한다.
- [x] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
## 계획 대비 변경 사항
- `typescript/src/tcp_client.ts`: TLS 경로 별도 분기 없이 `TcpClient` 생성자에서 `socket.setNoDelay(true)`를 한 번 호출하도록 했다. `net.Socket`과 `tls.TLSSocket` 모두 `setNoDelay`를 제공하므로 `connectTcp`/`connectTcpTls` 양쪽에 동일하게 적용된다.
- Kotlin no-delay는 plan의 `NoDelaySocketFactory` shape 그대로 구현했다. plan이 우려한 TLS 충돌은 발생하지 않는다. OkHttp는 `sslSocketFactory`로 만든 SSL socket을 `socketFactory`가 만든 plain socket 위에 layering하므로, plain socket에 `tcpNoDelay`를 켜면 ws/wss 모두 적용된다. 별도 TLS 분기를 추가하지 않았다.
- TypeScript phase 계측은 plan대로 stdout `ROW|` schema를 건드리지 않고 `emitRow`에서 stderr `DIAG fixed-latency ...` 한 줄을 추가하는 방식으로만 구현했다. 기존 `log()`(stderr) helper를 재사용했다.
- Kotlin `kotlin/crosstest/stress.kt`에도 동일한 stderr `DIAG fixed-latency ...` 진단을 `emitRow`에 추가했다. 이 파일은 sibling task `03_kotlin_tcp_parallel`의 변경이 이미 들어와 있어, 그쪽 변경을 건드리지 않고 `emitRow` 말미에만 추가했다.
- plan에는 명시되지 않았지만 `typescript/test/ws.test.ts`에 fake socket으로 `NodeWebSocket`이 `setNoDelay(true)`를 호출하는지 검증하는 단위 테스트를 추가했다(plan의 "필요 시" 조항 활용).
## 주요 설계 결정
- **근본 원인 가설: Nagle's algorithm + delayed-ACK.** `NodeWebSocket.writeFrame`는 header와 payload를 두 번의 `socket.write`로 보낸다(`node_ws_client.ts:388-389`). no-delay가 꺼진 상태에서 작은 두 번째 write가 delayed-ACK를 기다리며 막히는 전형적 패턴이 약 40ms 고정 지연과 일치한다. 따라서 socket no-delay를 명시하는 것을 1차 완화로 선택했다.
- **단일 지점 적용.** custom WS 경로(client `connectNodeWebSocket`, server `handleUpgrade`)는 모두 `NodeWebSocket`을 생성하므로 생성자에서 `socket.setNoDelay?.(true)`를 한 번 켜면 양쪽이 커버된다. 서버 `handleUpgrade`에는 `101 Switching Protocols` 핸드셰이크 응답 write도 즉시 flush되도록 raw socket 단계에서 `setNoDelay(true)`를 추가로 명시했다.
- **검증 결과 fixed latency 해소.** baseline(`20260613-202744-...stress-full.md`)에서 TypeScript gateway transport row는 gateway off/on 모두 p50 약 41ms였다. no-delay 적용 후 quick gateway run에서 ws gateway=off p50=1.604ms, gateway=on p50=1.545ms로 떨어졌고, WS roundtrip concurrency=1 p50도 0.645ms다. 약 40ms 고정 지연이 사라졌다.
- **남은 병목 후보.** gateway `worker_threads` mode는 여전히 p99=215.558ms tail을 보인다. 이는 no-delay와 무관한 worker_threads hop 자체의 tail이며 plan 배경에서 이미 별도 issue로 언급된 항목이다. Kotlin WS는 concurrency=1에서 p50=1.303ms로 깨끗하고, concurrency≥16에서의 p50 상승(44~59ms)은 per-request 고정 지연이 아니라 동시성 포화 특성이다. 두 항목 모두 이 plan의 범위 밖이며 후속 분석 후보로 남긴다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- no-delay 설정이 TLS/secure WS 경로를 깨지 않는지 확인한다.
- ROW schema가 performance parser와 호환되게 유지됐는지 확인한다.
- fixed latency가 남으면 남은 후보와 수치가 명확히 기록됐는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### PERF-1 중간 검증
```text
$ cd typescript && npm test -- test/tcp.test.ts test/ws.test.ts
✓ test/tcp.test.ts (7 tests) 264ms
✓ test/ws.test.ts (12 tests) 1429ms
Test Files 2 passed (2)
Tests 19 passed (19)
# 신규 추가한 "NodeWebSocket enables TCP no-delay on its socket" 포함, ws.test.ts 11 -> 12 tests
$ cd kotlin && ./gradlew --offline test --tests com.tokilabs.proto_socket.WsTest
> Task :compileKotlin
> Task :test
BUILD SUCCESSFUL in 14s
11 actionable tasks: 4 executed, 7 up-to-date
# NoDelaySocketFactory 추가 후 컴파일/WsTest 통과
```
### PERF-2 중간 검증
```text
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript,kotlin --transport ws --profile roundtrip
# TypeScript
DIAG fixed-latency profile=roundtrip transport=ws gateway=off p50=0.645 p99=1.863 # concurrency=1
DIAG fixed-latency profile=roundtrip transport=ws gateway=off p50=1.685 p99=3.986 # concurrency=16
DIAG fixed-latency profile=roundtrip transport=ws gateway=off p50=5.923 p99=6.178 # concurrency=64
DIAG fixed-latency profile=roundtrip transport=ws gateway=off p50=16.921 p99=25.548 # concurrency=256
DONE typescript: PASS (rows=4 skips=0 required_skips=0 violations=0)
# Kotlin
DIAG fixed-latency profile=roundtrip transport=ws p50=1.303 p99=27.231 # concurrency=1
DIAG fixed-latency profile=roundtrip transport=ws p50=44.270 p99=46.298 # concurrency=16
DIAG fixed-latency profile=roundtrip transport=ws p50=47.065 p99=47.564 # concurrency=64
DIAG fixed-latency profile=roundtrip transport=ws p50=59.129 p99=70.263 # concurrency=256
DONE kotlin: PASS (rows=4 skips=0 required_skips=0 violations=0)
전체 결과값: PASS
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport ws --profile gateway
DIAG fixed-latency profile=gateway transport=ws gateway=off p50=1.604 p99=4.027
DIAG fixed-latency profile=gateway transport=ws gateway=on p50=1.545 p99=2.673
DIAG fixed-latency profile=gateway transport=ws gateway=worker_threads p50=2.027 p99=215.558
DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0)
전체 결과값: PASS
# baseline 약 41ms(off/on) -> off 1.604ms / on 1.545ms 로 고정 지연 해소.
# worker_threads p99 tail(215ms)은 no-delay와 무관한 별도 이슈로 잔존.
```
### 최종 검증
```text
$ cd typescript && npm run check
> tsc --noEmit
# exit 0
$ cd typescript && npm test -- test/tcp.test.ts test/ws.test.ts
Test Files 2 passed (2)
Tests 19 passed (19)
$ cd kotlin && ./gradlew --offline test --tests com.tokilabs.proto_socket.WsTest
BUILD SUCCESSFUL in 14s
$ bash .../run_stress.sh --quick --lang typescript,kotlin --transport ws --profile roundtrip
전체 결과값: PASS (typescript rows=4, kotlin rows=4, violations=0)
결과 기록: agent-test/runs/20260614-103154-proto-socket-stress-quick.md
$ bash .../run_stress.sh --quick --lang typescript --transport ws --profile gateway
전체 결과값: PASS (typescript rows=6, violations=0)
결과 기록: agent-test/runs/20260614-103211-proto-socket-stress-quick.md
```
---
> **[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를 붙인다. |
## 코드리뷰 결과
### 종합 판정
PASS
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| correctness | Pass | TypeScript TCP/custom WS와 Kotlin OkHttp WS client에 no-delay가 적용됐고, 기존 WS/TCP request/response 동작이 유지된다. |
| completeness | Pass | 계획된 low-latency 설정, stderr diagnostic, stress row 수집, 남은 worker_threads tail 후보 기록이 완료됐다. |
| test coverage | Pass | TypeScript fake socket no-delay 테스트와 TCP/WS 회귀 테스트, Kotlin WS 테스트, quick stress row가 실행됐다. 리뷰 중 TypeScript/Kotlin 전체 smoke도 추가 확인했다. |
| API contract | Pass | stdout `ROW|`/summary schema는 유지되고 diagnostic은 stderr에만 추가됐다. 공개 protocol/schema 변경은 없다. |
| code quality | Pass | 변경은 transport 설정과 deterministic diagnostic에 국한되며 debug-only dead code나 불필요한 API 확장은 보이지 않는다. |
| plan deviation | Pass | TypeScript no-delay fake socket 테스트 추가는 계획의 "필요 시" 범위 안이며, Kotlin crosstest의 sibling 변경과 충돌하지 않게 `emitRow` 말미만 확장했다. |
| verification trust | Pass | 구현자가 기록한 stress 결과 파일의 row와 stderr diagnostic을 확인했고, 리뷰 중 핵심 검증 명령을 재실행해 모두 PASS를 확인했다. |
### 발견된 문제
없음
### 리뷰 중 재실행 검증
```text
$ cd typescript && npm run check
> tsc --noEmit
exit 0
$ cd typescript && npm test -- test/tcp.test.ts test/ws.test.ts
Test Files 2 passed (2)
Tests 19 passed (19)
$ cd kotlin && ./gradlew --offline test --tests com.tokilabs.proto_socket.WsTest
BUILD SUCCESSFUL in 2s
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript,kotlin --transport ws --profile roundtrip
전체 결과값: PASS
결과 기록 파일: agent-test/runs/20260614-110207-proto-socket-stress-quick.md
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport ws --profile gateway
전체 결과값: PASS
결과 기록 파일: agent-test/runs/20260614-110215-proto-socket-stress-quick.md
$ cd typescript && npm test
Test Files 5 passed (5)
Tests 63 passed (63)
$ cd kotlin && ./gradlew test
BUILD SUCCESSFUL in 15s
```
### 다음 단계
PASS이므로 `complete.log`를 작성하고 task directory를 archive로 이동한다. Roadmap 업데이트는 code-review가 직접 수행하지 않고 런타임 완료 이벤트로 보고한다.

View file

@ -0,0 +1,48 @@
# Complete - m-large-payload-followup-optimization/04_ws_fixed_latency
## 완료 일시
2026-06-14
## 요약
WS fixed latency path 분석/완화 작업 1회 리뷰 루프를 PASS로 종료했다. TypeScript TCP/custom WS와 Kotlin OkHttp WS client에 no-delay를 명시하고, stress stderr diagnostic으로 fixed latency row를 추적 가능하게 했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | no-delay 적용 후 TypeScript WS gateway off/on p50이 약 41ms에서 1-2ms대로 내려갔고, stability hard gate가 유지됐다. |
## 구현/정리 내용
- TypeScript `TcpClient`, custom `NodeWebSocket`, `NodeWsServer` upgrade socket에 TCP no-delay 설정을 추가했다.
- Kotlin OkHttp WS client용 `NoDelaySocketFactory`를 추가해 plain WS/WSS client socket에 `tcpNoDelay`를 적용했다.
- TypeScript/Kotlin stress `emitRow`에 stdout `ROW|` 계약을 유지한 채 stderr `DIAG fixed-latency` diagnostic을 추가했다.
- TypeScript `NodeWebSocket` 생성 시 `setNoDelay(true)` 호출을 fake socket unit test로 검증했다.
- worker_threads gateway p99 tail은 no-delay와 별도인 잔여 후보로 기록했다.
## 최종 검증
- `cd typescript && npm run check` - PASS; `tsc --noEmit` exit 0.
- `cd typescript && npm test -- test/tcp.test.ts test/ws.test.ts` - PASS; 2 files, 19 tests passed.
- `cd typescript && npm test` - PASS; 5 files, 63 tests passed.
- `cd kotlin && ./gradlew --offline test --tests com.tokilabs.proto_socket.WsTest` - PASS; `BUILD SUCCESSFUL`.
- `cd kotlin && ./gradlew test` - PASS; `BUILD SUCCESSFUL`.
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript,kotlin --transport ws --profile roundtrip` - PASS; result record `agent-test/runs/20260614-110207-proto-socket-stress-quick.md`.
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport ws --profile gateway` - PASS; result record `agent-test/runs/20260614-110215-proto-socket-stress-quick.md`.
## Roadmap Completion
- Milestone: `agent-roadmap/milestones/large-payload-followup-optimization.md`
- Completed task ids:
- `ws-fixed-latency-path`: PASS; evidence=`agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/plan_cloud_G07_0.log`, `agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/code_review_cloud_G07_0.log`; verification=`agent-test/runs/20260614-110207-proto-socket-stress-quick.md`, `agent-test/runs/20260614-110215-proto-socket-stress-quick.md`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -1,121 +0,0 @@
<!-- 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 격리 | [ ] |
| [PERF-2] required skip 제거와 full stress 측정 | [ ] |
## 구현 체크리스트
- [ ] [PERF-1] Kotlin TCP read loop가 Dispatchers.IO starvation을 만들지 않도록 bounded read timeout과 격리된 read execution 전략을 적용한다.
- [ ] [PERF-2] Kotlin stress의 TCP parallel required skip을 제거하고 clients=128/512/1024 row를 실제 측정한다.
- [ ] Kotlin TCP full parallel stress를 실행해 required skip 0, timeout/nonce/type/FIFO/pending leak 0을 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block을 확인한다.
- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/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
(output)
```
### PERF-2 중간 검증
```text
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang kotlin --transport tcp --profile parallel
(output)
```
### 최종 검증
```text
$ 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
(output)
```
---
> **[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를 붙인다. |

View file

@ -1,126 +0,0 @@
<!-- task=m-large-payload-followup-optimization/04_ws_fixed_latency 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`.
> 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/04_ws_fixed_latency, plan=0, tag=PERF
## Roadmap Targets
- Milestone: `agent-roadmap/milestones/large-payload-followup-optimization.md`
- Task ids:
- `ws-fixed-latency-path`: WS 계열 fixed latency가 지속되면 common/worker/gateway 경로 분석
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [PERF-1] low-latency 설정 및 phase 계측 | [ ] |
| [PERF-2] fixed latency row 수집 | [ ] |
## 구현 체크리스트
- [ ] [PERF-1] TypeScript/Kotlin WS/TCP socket 경로의 low-latency 설정과 phase 계측을 추가해 gateway off/on과 transport latency를 분리한다.
- [ ] [PERF-2] fixed latency 재현 stress를 실행하고 개선 여부와 남은 병목 후보를 review stub에 수치로 기록한다.
- [ ] TypeScript gateway/WS roundtrip 및 Kotlin WS quick stress를 실행해 correctness와 stability hard gate가 유지되는지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block을 확인한다.
- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- no-delay 설정이 TLS/secure WS 경로를 깨지 않는지 확인한다.
- ROW schema가 performance parser와 호환되게 유지됐는지 확인한다.
- fixed latency가 남으면 남은 후보와 수치가 명확히 기록됐는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### PERF-1 중간 검증
```text
$ cd typescript && npm test -- test/tcp.test.ts test/ws.test.ts
$ cd kotlin && ./gradlew --offline test --tests com.tokilabs.proto_socket.WsTest
(output)
```
### PERF-2 중간 검증
```text
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript,kotlin --transport ws --profile roundtrip
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport ws --profile gateway
(output)
```
### 최종 검증
```text
$ cd typescript && npm run check
$ cd typescript && npm test -- test/tcp.test.ts test/ws.test.ts
$ cd kotlin && ./gradlew --offline test --tests com.tokilabs.proto_socket.WsTest
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript,kotlin --transport ws --profile roundtrip
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport ws --profile gateway
(output)
```
---
> **[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를 붙인다. |

View file

@ -181,6 +181,12 @@ private fun emitRow(
status,
)
println(fields.joinToString("|"))
// fixed-latency 진단용 stderr diagnostic. stdout ROW 계약은 그대로 두고, transport별 p50/p99를
// grep 가능한 형태로 남겨 ws fixed latency 분석에서 Kotlin WS row를 쉽게 추출한다.
log(
"DIAG fixed-latency profile=$profile transport=$transport " +
"p50=${String.format("%.3f", p50)} p99=${String.format("%.3f", p99)}",
)
}
private fun emitSkip(profile: String, axis: String, transport: String, reason: String) {
@ -195,17 +201,11 @@ private fun deferredBottleneck(transport: String, profile: String, concurrency:
return null
}
// Kotlin TcpClient uses one blocking read thread per connection without a bounded read
// timeout. At high parallel client counts the read threads starve and the TCP parallel
// load makes no forward progress (no timeout fires, the run hangs). Defer those rows as a
// known optimize-Epic bottleneck instead of deadlocking the baseline run. WS parallel and
// low TCP client counts are unaffected and still measured.
private fun parallelDeadlockBottleneck(transport: String, clientCount: Int): String? {
if (transport == "tcp" && clientCount >= 128) {
return "deferred: optimize Epic — Kotlin TcpClient blocking read-thread-per-connection without bounded read timeout deadlocks at TCP parallel clients>=128 (thread starvation, no forward progress)"
}
return null
}
// TCP parallel clients>=128 deferred skip은 제거됐다. TcpClient가 read 전용 dispatcher와 soTimeout
// 기반 read poll을 쓰도록 고쳐(read/write 스레드 풀 분리) thread starvation deadlock이 사라졌고,
// clients=128/512/1024 row를 실제로 측정한다. 남는 deferred는 deferredBottleneck(slow-mix)뿐이다.
@Suppress("UNUSED_PARAMETER")
private fun parallelDeadlockBottleneck(transport: String, clientCount: Int): String? = null
private fun summarize(
profile: String,

View file

@ -2,7 +2,9 @@ package com.tokilabs.proto_socket
import com.tokilabs.proto_socket.packets.HeartBeat
import com.tokilabs.proto_socket.packets.PacketBase
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
@ -12,7 +14,10 @@ import java.io.DataInputStream
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
import java.net.SocketTimeoutException
import java.nio.ByteBuffer
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import javax.net.ssl.SSLContext
class TcpClient private constructor(
@ -44,6 +49,7 @@ class TcpClient private constructor(
init {
socket.tcpNoDelay = true
socket.soTimeout = READ_POLL_TIMEOUT_MS
communicator.setWriteErrorHandler { onDisconnected() }
communicator.setFrameErrorHandler { onDisconnected() }
communicator.addListener(typeNameOf<HeartBeat>()) { onHeartBeat() }
@ -68,18 +74,34 @@ class TcpClient private constructor(
}
private fun readLoop() {
scope.launch(Dispatchers.IO) {
// 연결당 read는 blocking이라 Dispatchers.IO에서 돌리면 parallel clients>=128에서 read 스레드가
// IO lane을 점유해 write/heartbeat/accept를 starvation시킨다. read 전용 dispatcher로 분리해
// write 경로와 스레드 풀을 공유하지 않게 하고, soTimeout 기반 poll로 close 시 즉시 루프를 빠져나간다.
scope.launch(readDispatcher) {
val input = DataInputStream(socket.getInputStream())
val header = ByteArray(4)
while (communicator.isAlive()) {
try {
val length = input.readInt()
// header/payload 모두 offset-preserving read로 읽는다. soTimeout poll은 frame 경계와
// 무관하게 발생할 수 있으므로, readInt/readFully를 직접 쓰면 frame 일부 소비 후 timeout이
// 나면 다음 read가 frame 중간 bytes를 length로 오해해 desync된다. readExact는 같은 frame
// read 안에서만 timeout을 poll로 흡수해 offset을 보존한다.
if (!readExact(input, header)) {
// close(isAlive=false)면 정상 종료, EOF(read<0)면 disconnect.
if (communicator.isAlive()) onDisconnected()
return@launch
}
val length = ByteBuffer.wrap(header).int
if (length == 0) continue
if (length < 0 || length > MAX_PACKET_SIZE) {
onDisconnected()
return@launch
}
val bytes = ByteArray(length)
input.readFully(bytes)
if (!readExact(input, bytes)) {
if (communicator.isAlive()) onDisconnected()
return@launch
}
// raw frame을 communicator ingress로 넘긴다. gateway가 attach되어 있으면 worker pool이
// decode/reorder하고, 없으면 inline decode한다. inline decode 오류는 반환값으로,
// gateway decode 오류는 frame error handler로 동일한 disconnect semantics를 적용한다.
@ -96,6 +118,25 @@ class TcpClient private constructor(
}
}
// target을 가득 채울 때까지 offset을 보존하며 읽는다. soTimeout poll(SocketTimeoutException)은 같은
// read 안에서 흡수해 frame 경계를 잃지 않는다. close되면(isAlive=false) 즉시 false를 반환해 read 루프가
// socket close에만 의존하지 않고 빠져나간다. EOF(read<0)도 false를 반환해 disconnect로 이어진다.
// 반환: target.size만큼 다 읽으면 true, close/EOF면 false.
private fun readExact(input: DataInputStream, target: ByteArray): Boolean {
var offset = 0
while (offset < target.size) {
if (!communicator.isAlive()) return false
try {
val read = input.read(target, offset, target.size - offset)
if (read < 0) return false
offset += read
} catch (_: SocketTimeoutException) {
continue
}
}
return true
}
suspend fun send(message: com.google.protobuf.MessageLite) {
communicator.send(message)
}
@ -103,6 +144,21 @@ class TcpClient private constructor(
companion object {
const val MAX_PACKET_SIZE: Int = 64 * 1024 * 1024
// soTimeout poll 주기. blocking readInt가 이 주기마다 SocketTimeoutException으로 깨어나
// isAlive()를 재확인하므로, close 후 read 루프가 socket close에만 의존하지 않고 즉시 종료된다.
private const val READ_POLL_TIMEOUT_MS: Int = 1_000
// 모든 TcpClient read 루프가 공유하는 read 전용 dispatcher. cached thread pool이라 연결 수에
// 맞춰 read 스레드가 늘고, Dispatchers.IO(write/heartbeat/accept)와 풀을 분리해 starvation을
// 막는다. daemon 스레드라 JVM 종료를 막지 않으며, soTimeout poll로 idle 스레드는 곧 회수된다.
private val readThreadCounter = AtomicInteger(0)
private val readDispatcher: CoroutineDispatcher =
Executors.newCachedThreadPool { runnable ->
Thread(runnable, "proto-socket-tcp-read-${readThreadCounter.incrementAndGet()}").apply {
isDaemon = true
}
}.asCoroutineDispatcher()
fun fromSocket(
socket: Socket,
intervalSec: Int = 30,

View file

@ -14,6 +14,8 @@ import okio.Buffer
import okio.ByteString
import org.java_websocket.WebSocket
import java.io.IOException
import java.net.InetAddress
import java.net.Socket
import java.security.KeyStore
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@ -21,6 +23,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import javax.net.SocketFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
@ -28,6 +31,30 @@ import javax.net.ssl.X509TrustManager
internal const val MAX_STAGED_FRAMES = 1024
/**
* 생성하는 모든 plain socket에 `tcpNoDelay = true` 적용하는 [SocketFactory].
* OkHttp의 SSL socket factory는 plain socket 위에 layering되므로 ws/wss 모두 no-delay가 된다.
*/
internal object NoDelaySocketFactory : SocketFactory() {
private val delegate: SocketFactory = getDefault()
private fun configure(socket: Socket): Socket = socket.apply { tcpNoDelay = true }
override fun createSocket(): Socket = configure(delegate.createSocket())
override fun createSocket(host: String?, port: Int): Socket =
configure(delegate.createSocket(host, port))
override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int): Socket =
configure(delegate.createSocket(host, port, localHost, localPort))
override fun createSocket(host: InetAddress?, port: Int): Socket =
configure(delegate.createSocket(host, port))
override fun createSocket(host: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int): Socket =
configure(delegate.createSocket(host, port, localAddress, localPort))
}
class WsClient internal constructor(
private val connection: WsConnection,
intervalSec: Int,
@ -252,6 +279,10 @@ private fun dialWsUrl(
.protocols(listOf(Protocol.HTTP_1_1))
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
// 서버(WsServer)는 이미 tcpNoDelay를 켠다. 클라이언트 plain socket도 no-delay로 만들어
// Nagle's algorithm + delayed-ACK 조합에서 생기는 ~40ms 고정 지연을 막는다.
// SSL socket factory는 이 plain socket 위에 layering되므로 ws/wss 모두에 적용된다.
.socketFactory(NoDelaySocketFactory)
if (sslContext != null) {
val trustManager = object : javax.net.ssl.X509TrustManager {
override fun checkClientTrusted(chain: Array<out java.security.cert.X509Certificate>?, authType: String?) {}

View file

@ -145,6 +145,90 @@ class TcpTest {
}
}
@Test
fun testTcpCloseCompletesWhileReadParked() = runBlocking {
// read 루프가 soTimeout poll로 readInt에 parked된 상태에서도 close가 socket close에만
// 의존하지 않고 즉시 완료되는지 확인한다(parallel clients>=128 unskip 회귀 방지).
val port = freePort()
val server = TcpServer("127.0.0.1", port) { socket ->
TcpClient.fromSocket(socket, 0, 0, testParserMap())
}
server.start()
val client = dialTcpWithRetry("127.0.0.1", port)
waitForCondition(message = "client did not connect") { server.clients().size == 1 }
// poll 주기를 한 번 이상 지나 read가 확실히 blocking readInt에 머무는 시점에 close한다.
kotlinx.coroutines.delay(1_200)
val elapsed = kotlin.system.measureTimeMillis { client.close() }
assertFalse(client.communicator.isAlive())
kotlin.test.assertTrue(elapsed < 2_000, "close blocked for ${elapsed}ms")
server.stop()
}
@Test
fun testTcpDelayedPartialFrameDoesNotDesync() = runBlocking {
// frame header/payload를 두 chunk로 나눠 보내되, chunk 사이 지연을 read poll timeout(1000ms)보다
// 길게 둔다. offset을 보존하지 않으면 chunk 사이 SocketTimeoutException 후 frame 중간 bytes를
// length로 오해해 desync된다. header split과 payload split을 모두 검증한다.
val serverSocket = java.net.ServerSocket(0)
val accept = async(Dispatchers.IO) { serverSocket.accept() }
val socket = Socket("127.0.0.1", serverSocket.localPort)
val peer = accept.await()
val client = TcpClient.fromSocket(socket, 0, 0, testParserMap())
try {
val first = CompletableDeferred<TestData>()
val second = CompletableDeferred<TestData>()
val results = listOf(first, second)
val nextResult = java.util.concurrent.atomic.AtomicInteger(0)
addListenerTyped<TestData>(client.communicator) { td ->
val i = nextResult.getAndIncrement()
if (i < results.size) results[i].complete(td)
}
val out = peer.getOutputStream()
// 1) payload split: header + payload 앞부분을 보내고, poll timeout을 넘긴 뒤 나머지 payload flush.
val framePayloadSplit = buildTestDataFrame(7, "payload-split")
val payloadCut = framePayloadSplit.size - 3
out.write(framePayloadSplit, 0, payloadCut)
out.flush()
kotlinx.coroutines.delay(1_200)
out.write(framePayloadSplit, payloadCut, framePayloadSplit.size - payloadCut)
out.flush()
assertEquals("payload-split", first.await().message)
// 2) header split: 4-byte length prefix 자체를 둘로 쪼개 poll timeout을 넘겨도 desync되지 않는지 확인.
val frameHeaderSplit = buildTestDataFrame(9, "header-split")
out.write(frameHeaderSplit, 0, 2)
out.flush()
kotlinx.coroutines.delay(1_200)
out.write(frameHeaderSplit, 2, frameHeaderSplit.size - 2)
out.flush()
assertEquals("header-split", second.await().message)
kotlin.test.assertTrue(client.communicator.isAlive(), "client disconnected after delayed partial frames")
} finally {
client.close()
peer.close()
serverSocket.close()
}
}
// 4-byte big-endian length prefix + PacketBase(TestData) envelope. wire 포맷은 Communicator.send와
// TcpClient.writePacket이 만드는 것과 동일하다.
private fun buildTestDataFrame(index: Int, message: String): ByteArray {
val td = TestData.newBuilder().setIndex(index).setMessage(message).build()
val base = com.tokilabs.proto_socket.packets.PacketBase.newBuilder()
.setTypeName(typeNameOf<TestData>())
.setNonce(1)
.setData(td.toByteString())
.build()
val body = base.toByteArray()
val header = java.nio.ByteBuffer.allocate(4).putInt(body.size).array()
return header + body
}
@Test
fun testTcpClientCloseIdempotent() = runBlocking {
val serverSocket = java.net.ServerSocket(0)

View file

@ -181,6 +181,12 @@ function emitRow(row: Row): void {
rowStatus(row),
].join("|") + "\n",
);
// fixed-latency 진단용 stderr diagnostic. stdout ROW 계약은 그대로 두고, transport/gateway mode별
// p50/p99를 grep 가능한 형태로 남겨 ws fixed latency 분석에서 row를 쉽게 추출한다.
log(
`DIAG fixed-latency profile=${row.profile} transport=${row.transport} gateway=${row.gateway} ` +
`p50=${row.p50.toFixed(3)} p99=${row.p99.toFixed(3)}`,
);
}
function log(line: string): void {

View file

@ -170,6 +170,10 @@ export class NodeWebSocket {
) {
this.events.on("error", () => {});
// writeFrame은 header와 payload를 별도 write로 보낸다. Nagle's algorithm + delayed-ACK가
// 겹치면 두 번째 작은 write가 ~40ms 지연되므로 no-delay를 명시해 즉시 flush한다.
socket.setNoDelay?.(true);
socket.on("data", (chunk) => {
this.onData(chunk);
});

View file

@ -89,6 +89,9 @@ export class NodeWsServer {
}
private handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
// upgrade socket도 client와 동일하게 Nagle 지연을 피하도록 no-delay를 켠다.
socket.setNoDelay(true);
const key = this.validateUpgrade(req);
if (key === null) {
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");

View file

@ -19,6 +19,9 @@ export class TcpClient extends BaseClient {
}
});
this.socket = socket;
// Nagle's algorithm은 작은 write를 모아 보내며 delayed-ACK와 만나면 ~40ms 고정 지연을 만든다.
// roundtrip 패턴에서는 즉시 flush가 유리하므로 no-delay를 명시한다.
this.socket.setNoDelay(true);
this.initBase(parserMap);
socket.on("data", (chunk) => {

View file

@ -14,7 +14,7 @@ import {
} from "../src/communicator.js";
import { create, TestDataSchema, type TestData, toBinary, PacketBaseSchema } from "../src/packets/message_common_pb.js";
import { connectNodeWs, connectNodeWss, NodeWsClient } from "../src/node_ws_client.js";
import { connectNodeWs, connectNodeWss, NodeWebSocket, NodeWsClient } from "../src/node_ws_client.js";
import { NodeWsServer } from "../src/node_ws_server.js";
const certsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "certs");
@ -485,6 +485,27 @@ describe("WS", () => {
expect(pongData[1]).toBe(0x00);
}, 10000);
test("NodeWebSocket enables TCP no-delay on its socket", () => {
// Nagle's algorithm + delayed-ACK 조합으로 인한 ~40ms 고정 지연을 막기 위해
// NodeWebSocket이 생성 시 socket no-delay를 켜는지 fake socket으로 검증한다.
let noDelay: boolean | undefined;
const fakeSocket = {
setNoDelay(value: boolean) {
noDelay = value;
},
on() {
return this;
},
once() {
return this;
},
destroyed: false,
};
new NodeWebSocket(fakeSocket as unknown as net.Socket, true);
expect(noDelay).toBe(true);
});
test("WS masking tail bytes equivalence unit test for various lengths", async () => {
const lengths = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 1023, 1024, 1025];
const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap()));