diff --git a/agent-roadmap/milestones/large-payload-followup-optimization.md b/agent-roadmap/milestones/large-payload-followup-optimization.md index 2a33887..3489bd7 100644 --- a/agent-roadmap/milestones/large-payload-followup-optimization.md +++ b/agent-roadmap/milestones/large-payload-followup-optimization.md @@ -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` diff --git a/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_0.log b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_0.log new file mode 100644 index 0000000..5a3f6b1 --- /dev/null +++ b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_0.log @@ -0,0 +1,181 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고하고 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`를 작성한다. diff --git a/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_1.log b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_1.log new file mode 100644 index 0000000..d331efa --- /dev/null +++ b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/code_review_cloud_G07_1.log @@ -0,0 +1,202 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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로 이동한다. diff --git a/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/complete.log b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/complete.log new file mode 100644 index 0000000..bc0f4e2 --- /dev/null +++ b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/complete.log @@ -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 + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/PLAN-cloud-G07.md b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/plan_cloud_G07_0.log similarity index 100% rename from agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/PLAN-cloud-G07.md rename to agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/plan_cloud_G07_0.log diff --git a/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/plan_cloud_G07_1.log b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/plan_cloud_G07_1.log new file mode 100644 index 0000000..2577a87 --- /dev/null +++ b/agent-task/archive/2026/06/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/plan_cloud_G07_1.log @@ -0,0 +1,120 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/code_review_cloud_G07_0.log b/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/code_review_cloud_G07_0.log new file mode 100644 index 0000000..9292020 --- /dev/null +++ b/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/code_review_cloud_G07_0.log @@ -0,0 +1,231 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고하고 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가 직접 수행하지 않고 런타임 완료 이벤트로 보고한다. diff --git a/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/complete.log b/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/complete.log new file mode 100644 index 0000000..9193168 --- /dev/null +++ b/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/complete.log @@ -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 + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-large-payload-followup-optimization/04_ws_fixed_latency/PLAN-cloud-G07.md b/agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/plan_cloud_G07_0.log similarity index 100% rename from agent-task/m-large-payload-followup-optimization/04_ws_fixed_latency/PLAN-cloud-G07.md rename to agent-task/archive/2026/06/m-large-payload-followup-optimization/04_ws_fixed_latency/plan_cloud_G07_0.log diff --git a/agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/CODE_REVIEW-cloud-G07.md b/agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/CODE_REVIEW-cloud-G07.md deleted file mode 100644 index c3eb89d..0000000 --- a/agent-task/m-large-payload-followup-optimization/03_kotlin_tcp_parallel/CODE_REVIEW-cloud-G07.md +++ /dev/null @@ -1,121 +0,0 @@ - - -# 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-`이면 완료 이벤트 메타데이터를 보고하고 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를 붙인다. | diff --git a/agent-task/m-large-payload-followup-optimization/04_ws_fixed_latency/CODE_REVIEW-cloud-G07.md b/agent-task/m-large-payload-followup-optimization/04_ws_fixed_latency/CODE_REVIEW-cloud-G07.md deleted file mode 100644 index d954897..0000000 --- a/agent-task/m-large-payload-followup-optimization/04_ws_fixed_latency/CODE_REVIEW-cloud-G07.md +++ /dev/null @@ -1,126 +0,0 @@ - - -# 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-`이면 완료 이벤트 메타데이터를 보고하고 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를 붙인다. | diff --git a/kotlin/crosstest/stress.kt b/kotlin/crosstest/stress.kt index 1a73b10..e6a71c0 100644 --- a/kotlin/crosstest/stress.kt +++ b/kotlin/crosstest/stress.kt @@ -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, diff --git a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt index 2729593..c6e026e 100644 --- a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt +++ b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt @@ -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()) { 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, diff --git a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/WsClient.kt b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/WsClient.kt index bc6b0b6..20fc8dc 100644 --- a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/WsClient.kt +++ b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/WsClient.kt @@ -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?, authType: String?) {} diff --git a/kotlin/src/test/kotlin/com/tokilabs/proto_socket/TcpTest.kt b/kotlin/src/test/kotlin/com/tokilabs/proto_socket/TcpTest.kt index c1a7e1c..28f4e62 100644 --- a/kotlin/src/test/kotlin/com/tokilabs/proto_socket/TcpTest.kt +++ b/kotlin/src/test/kotlin/com/tokilabs/proto_socket/TcpTest.kt @@ -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() + val second = CompletableDeferred() + val results = listOf(first, second) + val nextResult = java.util.concurrent.atomic.AtomicInteger(0) + addListenerTyped(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()) + .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) diff --git a/typescript/bench/stress.ts b/typescript/bench/stress.ts index 95dd46d..e9e96f9 100644 --- a/typescript/bench/stress.ts +++ b/typescript/bench/stress.ts @@ -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 { diff --git a/typescript/src/node_ws_client.ts b/typescript/src/node_ws_client.ts index 4905292..b307bbe 100644 --- a/typescript/src/node_ws_client.ts +++ b/typescript/src/node_ws_client.ts @@ -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); }); diff --git a/typescript/src/node_ws_server.ts b/typescript/src/node_ws_server.ts index d35e73c..74ff528 100644 --- a/typescript/src/node_ws_server.ts +++ b/typescript/src/node_ws_server.ts @@ -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"); diff --git a/typescript/src/tcp_client.ts b/typescript/src/tcp_client.ts index 81335a2..5acfe1b 100644 --- a/typescript/src/tcp_client.ts +++ b/typescript/src/tcp_client.ts @@ -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) => { diff --git a/typescript/test/ws.test.ts b/typescript/test/ws.test.ts index 63ec33b..c5a0a11 100644 --- a/typescript/test/ws.test.ts +++ b/typescript/test/ws.test.ts @@ -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()));