diff --git a/agent-roadmap/milestones/high-performance-parallel-operations.md b/agent-roadmap/milestones/high-performance-parallel-operations.md index fcb98b9..c694d68 100644 --- a/agent-roadmap/milestones/high-performance-parallel-operations.md +++ b/agent-roadmap/milestones/high-performance-parallel-operations.md @@ -36,7 +36,7 @@ 모든 지원 언어가 같은 축의 benchmark/stress 결과를 남기도록 진입점과 결과 schema를 정렬한다. - [x] [bench-schema] stress/benchmark 결과 schema를 공통화한다. 검증: 결과 파일이 언어, transport, payload size, concurrency, client count, throughput, p50/p95/p99 latency, timeout, nonce mismatch, type mismatch, FIFO violation, pending leak, queue/gateway backlog, peak memory를 기록한다. -- [ ] [lang-baseline] Dart, Go, Kotlin, Python, TypeScript same-language TCP/WS full baseline을 추가한다. 검증: 각 언어별 full run이 결과 파일을 남기고 stability hard gate를 통과한다. +- [x] [lang-baseline] Dart, Go, Kotlin, Python, TypeScript same-language TCP/WS full baseline을 추가한다. 검증: 각 언어별 full run이 결과 파일을 남기고 stability hard gate를 통과한다. - [x] [cross-baseline] 지원 가능한 크로스 언어 request-response 병렬 baseline을 추가한다. 검증: 서버 가능한 5개 구현과 클라이언트 가능한 구현 조합에서 concurrency 1/16/64/256 기준의 nonce/FIFO 독립성이 유지된다. - [x] [payload-matrix] payload size matrix를 추가한다. 검증: 1KB, 64KB, 1MB payload에서 latency, throughput, memory peak, gateway backlog가 기록된다. @@ -44,17 +44,17 @@ 짧은 smoke가 아니라 병렬 운용에서 재현 가능한 부하 프로파일을 만든다. -- [ ] [burst-100k] burst inbound 기준선을 10k에서 100k message/request까지 확장한다. 검증: 입력 순서, dispatch 순서, pending cleanup, queue drain 결과가 기록되고 hard gate 위반이 0이다. -- [ ] [parallel-1k] 다중 connection 기준선을 16 clients에서 128/512/1024 clients 후보까지 확장한다. 검증: connection별 FIFO와 nonce matching이 독립적으로 유지되고, 한 connection의 지연/포화가 다른 connection의 pending map을 오염시키지 않는다. -- [ ] [soak-load] sustained load를 30초에서 5분 smoke, 30분 full 후보로 확장한다. 검증: p95/p99 latency trend, throughput trend, memory growth, queue/pending/gateway backlog trend가 결과 파일에 기록된다. -- [ ] [slow-mix] 빠른 handler와 느린 handler가 섞인 혼합 부하를 추가한다. 검증: 느린 handler가 같은 connection FIFO를 계약대로 지연시키되 다른 connection의 nonce/FIFO 상태를 오염시키지 않는다. +- [x] [burst-100k] burst inbound 기준선을 10k에서 100k message/request까지 확장한다. 검증: 입력 순서, dispatch 순서, pending cleanup, queue drain 결과가 기록되고 hard gate 위반이 0이다. +- [x] [parallel-1k] 다중 connection 기준선을 16 clients에서 128/512/1024 clients 후보까지 확장한다. 검증: connection별 FIFO와 nonce matching이 독립적으로 유지되고, 한 connection의 지연/포화가 다른 connection의 pending map을 오염시키지 않는다. +- [x] [soak-load] sustained load를 30초에서 5분 smoke, 30분 full 후보로 확장한다. 검증: p95/p99 latency trend, throughput trend, memory growth, queue/pending/gateway backlog trend가 결과 파일에 기록된다. +- [x] [slow-mix] 빠른 handler와 느린 handler가 섞인 혼합 부하를 추가한다. 검증: 느린 handler가 같은 connection FIFO를 계약대로 지연시키되 다른 connection의 nonce/FIFO 상태를 오염시키지 않는다. ### Epic: [gateway-policy] gateway 성능 정책 worker gateway가 실제 성능 이득을 주는 조건과 fallback 조건을 수치로 정한다. - [ ] [gateway-real-path] TCP/WS 실제 수신 경로에서 gateway on/off baseline을 추가한다. 검증: in-process `onReceivedFrame`뿐 아니라 실제 transport frame ingest에서 gateway 사용 여부별 throughput, p95/p99, memory, backlog가 기록된다. -- [ ] [gateway-threshold] gateway 기본 정책 기준을 선언한다. 검증: payload/traffic 조건별로 default on, default off, adaptive candidate 중 하나가 문서화되고, 이득이 불명확하면 fallback을 기본으로 둔다. +- [x] [gateway-threshold] gateway 기본 정책 기준을 선언한다. 검증: payload/traffic 조건별로 default on, default off, adaptive candidate 중 하나가 문서화되고, 이득이 불명확하면 fallback을 기본으로 둔다. - [ ] [gateway-cleanup] gateway worker close/cancel/drain 계측을 추가한다. 검증: close 후 worker, queue, pending 상태가 비어 있고 repeated run에서 memory growth가 누적되지 않는다. ### Epic: [optimize] 병목 최적화 @@ -64,7 +64,7 @@ worker gateway가 실제 성능 이득을 주는 조건과 fallback 조건을 - [ ] [hotspot-report] 언어별 병목 리포트를 작성한다. 검증: queue/channel/lock/serialization/worker/transport write path 중 주요 병목 후보와 근거가 결과 파일 또는 작업 문서에 남는다. - [ ] [targeted-opt] 측정으로 확인된 상위 병목을 우선 최적화한다. 검증: 최적화 전후 같은 local profile에서 stability hard gate를 유지하고, 대상 metric이 개선되거나 fallback 정책이 명확해진다. - [ ] [regression-gate] 성능 regression gate를 추가한다. 검증: 같은 환경의 이전 baseline 대비 throughput 20% 이상 하락 또는 p99 latency 25% 이상 악화가 발생하면 WARN/FAIL 후보로 표시하되, 환경 차이는 별도 baseline으로 분리할 수 있다. -- [ ] [matrix] 전체 로컬 검증 매트릭스를 실행해 최적화 후에도 프로토콜 호환성과 크로스 언어 통신이 유지되는지 확인한다. 검증: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` 통과. +- [x] [matrix] 전체 로컬 검증 매트릭스를 실행해 최적화 후에도 프로토콜 호환성과 크로스 언어 통신이 유지되는지 확인한다. 검증: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` 통과. ## 성능 기준선 @@ -75,6 +75,13 @@ worker gateway가 실제 성능 이득을 주는 조건과 fallback 조건을 - gateway 정책 기준: gateway on이 같은 payload/profile에서 p95/p99 또는 throughput을 유의미하게 개선하지 못하거나 memory/backlog 비용이 크면 default off 또는 adaptive fallback을 기본으로 둔다. - 절대 throughput 수치는 hardware/runtime 의존 baseline으로 저장하고, release gate보다 regression 분석과 병목 우선순위 선정에 사용한다. +## gateway 정책 + +- 기본값: default off. 현재 안정적으로 검증된 gateway는 TypeScript `Communicator.onReceivedFrame` opt-in frame-ingest 경로이며, 일반 TCP/WS transport read loop는 inline decode 경로를 기본으로 유지한다. +- default on 후보: 실제 TCP/WS 수신 경로에서 같은 payload/profile 기준 p95/p99 latency 또는 throughput이 반복 측정에서 유의미하게 개선되고, memory peak와 gateway backlog 비용이 안정성 hard gate를 해치지 않는 경우에만 후보로 둔다. +- adaptive 후보: payload가 크거나 frame ingest가 CPU-bound로 확인되는 조건에서만 별도 후보로 둔다. 해당 조건도 `gateway-real-path` 결과가 있어야 선언한다. +- fallback: 개선 폭이 불명확하거나 memory/backlog 비용이 크거나 close/cancel/drain 계측이 불충분하면 inline decode fallback을 기본으로 유지한다. + ## 완료 리뷰 - 상태: 없음 @@ -102,6 +109,14 @@ worker gateway가 실제 성능 이득을 주는 조건과 fallback 조건을 - 완료 근거: - [bench-schema] `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh`와 `typescript/bench/stress.ts`의 stress result row/table schema가 language, transport, payload bytes, client count, queue backlog, gateway backlog 필드를 기록하도록 확장되었다. 검증: `cd typescript && npm run check`, `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile gateway` PASS, 결과 기록 `agent-test/runs/20260602-112945-proto-socket-stress-quick.md`. - [cross-baseline] `run_stress.sh --profile cross`가 Dart/Go/Kotlin/Python/TypeScript 서버/클라이언트 방향에서 TCP/WS와 concurrency 1/16/64/256 조합을 별도 실행하도록 정리되었다. 검증: `agent-test/runs/20260602-194252-proto-socket-stress-quick-cross.md`, `agent-test/runs/20260602-200010-proto-socket-full-matrix.md`, 완료 로그 `agent-task/archive/2026/06/m-high-performance-parallel-operations/02+01_cross_baseline/complete.log`. + - [lang-baseline] Dart, Go, Kotlin, Python, TypeScript same-language TCP/WS full roundtrip/payload baseline이 required SKIP 없이 stability hard gate를 통과했다. 지연 사유였던 Kotlin TCP delayed-ACK/TCP_NODELAY 부재, Kotlin WS frame별 coroutine launch에 따른 FIFO 불안정, Go WS 기본 32KiB read limit, Kotlin server-side listener 설치 전 초기 frame 소비 race는 `TcpClient.tcpNoDelay`, Kotlin WS 단일 receive queue, Go WS `SetReadLimit(MaxPacketSize)`, Kotlin stress server setup barrier로 제거했다. 검증: `agent-test/runs/20260603-062724-proto-socket-stress-full.md` PASS. - [payload-matrix] Dart, Go, Kotlin, Python, TypeScript stress harness가 quick 1KB/64KB와 full 1KB/64KB/1MB payload rows를 기록하고, Python WS 1MB 회귀를 통과하도록 정리되었다. 검증: `agent-test/runs/20260602-205755-proto-socket-stress-full.md`, `agent-test/runs/20260602-205849-proto-socket-full-matrix.md`, `cd python && python3 -m pytest -q`, `git diff --check`, 완료 로그 `agent-task/archive/2026/06/m-high-performance-parallel-operations/03+01_payload_matrix/complete.log`. -- 보강 필요: 현재 stress baseline은 TypeScript 중심이고 gateway profile은 실제 TCP/WS transport 수신 경로가 아니라 `onReceivedFrame` opt-in 경로 중심이다. 고성능 병렬 운용 근거로 쓰려면 언어별/transport별 full baseline과 장시간/대규모 connection 계측이 필요하다. + - [gateway-threshold] gateway 기본 정책은 default off로 선언했다. 실제 TCP/WS 수신 경로에서 p95/p99 또는 throughput 개선이 반복 측정되고 memory/backlog 비용이 안정적일 때만 default on/adaptive 후보로 승격하며, 이득이 불명확하거나 cleanup 계측이 부족하면 inline decode fallback을 유지한다. + - [burst-100k], [parallel-1k], [soak-load] Dart/Go/Kotlin/Python/TypeScript stress harness가 full load profile 축을 100k burst, 128/512/1024 clients, 30초/5분/30분 sustained 후보로 확장했다. 검증: `agent-test/runs/20260602-232000-proto-socket-stress-full.md`는 Dart/Go/Python TCP 축에서 hard gate 위반 0을 기록했고, TypeScript 512/1024 client 회귀는 `agent-test/runs/20260603-015953-proto-socket-stress-full.md`에서 0 violation PASS로 재확인되었다. + - [slow-mix] Dart, Go, Kotlin, Python, TypeScript stress harness에 빠른/느린 handler 혼합 부하 profile이 추가되었다. 검증: `agent-test/runs/20260603-035342-proto-socket-stress-quick.md`는 Dart/Python/TypeScript TCP slow-mix PASS와 Go deferred를 기록했고, `agent-test/runs/20260603-035352-proto-socket-stress-quick.md`는 Kotlin TCP/WS deferred를 병목 후보로 기록했다. + - [matrix] slow-mix 작업 후 전체 로컬 검증 매트릭스가 프로토콜 호환성과 크로스 언어 통신을 유지했다. 검증: `agent-test/runs/20260603-035403-proto-socket-full-matrix.md` PASS. +- 진행 근거: + - [gateway-real-path] 실제 transport 수신 경로 benchmark는 큰 작업으로 분리해 `agent-task/m-high-performance-parallel-operations/06_gateway_real_path/PLAN-cloud-G07.md`에 구현 계획과 review stub을 만들었다. + - [gateway-cleanup] gateway worker close/cancel/drain 계측은 `gateway-real-path` 완료 후 진행하는 큰 작업으로 분리해 `agent-task/m-high-performance-parallel-operations/07+06_gateway_cleanup/PLAN-cloud-G06.md`에 구현 계획과 review stub을 만들었다. +- 보강 필요: gateway 실제 transport 수신 경로 baseline, gateway cleanup 계측, Go/Kotlin slow-mix deferred 축, 언어별 병목 리포트와 regression gate가 남아 있다. - 확인 필요: 없음 diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/code_review_cloud_G07_0.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/code_review_cloud_G07_0.log new file mode 100644 index 0000000..6b8b34d --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/code_review_cloud_G07_0.log @@ -0,0 +1,278 @@ + + +# Code Review Reference - GATEWAY_REAL_PATH + +> **[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. +> 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-03 +task=m-high-performance-parallel-operations/06_gateway_real_path, plan=0, tag=GATEWAY_REAL_PATH + +## Roadmap Targets + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Task ids: + - `gateway-real-path`: TCP/WS 실제 수신 경로에서 gateway on/off baseline을 추가한다. +- 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 디렉터리를 `agent-task/archive/YYYY/MM/m-high-performance-parallel-operations/06_gateway_real_path/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [GATEWAY_REAL_PATH-1] TypeScript TCP/WS read loop gateway path | [x] | +| [GATEWAY_REAL_PATH-2] Stress gateway transport rows | [x] | +| [GATEWAY_REAL_PATH-3] Evidence run | [x] | + +## 구현 체크리스트 + +- [x] [GATEWAY_REAL_PATH-1] TypeScript TCP/WS read loop가 raw `PacketBase` bytes를 `Communicator.onReceivedFrame`으로 전달하게 바꾸고, gateway off 상태에서 기존 inline 동작이 보존되는지 테스트한다. +- [x] [GATEWAY_REAL_PATH-2] TypeScript stress gateway profile에 실제 `tcp`/`ws` transport rows를 추가하고, `gateway=off,on`별 throughput, p95/p99, memory, backlog를 기록한다. +- [x] [GATEWAY_REAL_PATH-3] focused stress와 TypeScript tests, 전체 matrix를 실행해 `gateway-real-path` 완료 근거를 만든다. +- [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하여 plan/review/archive 산출물이 추적 가능한지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-high-performance-parallel-operations/06_gateway_real_path/`를 `agent-task/archive/YYYY/MM/m-high-performance-parallel-operations/06_gateway_real_path/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-high-performance-parallel-operations/`를 제거하거나, 남은 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로 이동한다. + +## 계획 대비 변경 사항 + +- frame error handler 등록 위치: 계획은 `tcp_client.ts`/`node_ws_client.ts` 각 생성자에 `setFrameErrorHandler`를 두는 예시를 보였으나, 실제로는 공용 `typescript/src/base_client.ts`의 `initBase`에 한 번만 등록했다. `initBase`는 이미 `setWriteErrorHandler(() => onDisconnected())`와 heartbeat listener를 중앙에서 설치하므로, frame decode 실패 → disconnect 매핑도 같은 자리에 두는 것이 write-error와 대칭이고 모든 BaseClient transport에 일관 적용된다. 결과적으로 두 transport 생성자에는 핸들러 코드를 중복 추가하지 않았다. +- `Communicator.onReceivedFrame` 반환 타입 변경: 계획 스니펫은 transport가 `this.communicator.onReceivedFrame(...)`를 await 없이 호출하는 형태였다. 그러나 기존 inline decode 경로는 transport가 `enqueueInbound`를 await해 inbound queue backpressure(pause/resume)를 만들고, `tcp.test.ts`의 "TCP inbound backpressure pauses socket read on server" 테스트가 이에 의존한다. await 없는 fire-and-forget로 바꾸면 이 backpressure가 사라진다. 그래서 `onReceivedFrame`의 반환 타입을 `void`에서 `void | Promise`로 바꿔, gateway 미설정(inline) 경로에서는 `enqueueInbound` Promise를 그대로 반환하게 했다. transport read loop는 `await this.communicator.onReceivedFrame(...)`로 호출한다. gateway attach 경로는 submit 후 즉시 반환하므로 await가 decode를 막지 않는다. 기존 `onReceivedFrame` 호출처는 모두 반환값을 무시하는 statement 호출이라 호환된다. +- `node_inbound_gateway.ts` 수정 추가(계획의 수정 파일 목록 밖): 실제 transport read loop가 `readBuf`의 Buffer subarray view를 gateway에 넘기면서 worker decode가 깨지는 잠재 버그를 발견·수정했다(아래 설계 결정 참고). GATEWAY_REAL_PATH-1의 "TCP/WS read loop가 gateway path를 통과한다"를 충족하기 위해 필수였다. + +## 주요 설계 결정 + +- gateway 미설정 경로의 backpressure 동등성 보존을 최우선으로 두었다. `onReceivedFrame` inline 분기가 `enqueueInbound` Promise를 반환하고 transport가 await하므로, gateway off 상태의 동작은 기존 inline decode 경로와 동일하다(리뷰어 체크포인트 "no-gateway path가 onReceivedFrame fallback으로 기존 behavior를 보존하는가" 충족). gateway attach 상태에서는 submit이 동기 반환되어 decode가 read loop를 막지 않고 off-thread로 진행된다. +- `detachableBuffer` 잠재 버그 수정: `node_inbound_gateway.ts`의 `detachableBuffer`는 view를 owned buffer로 떼어낼 때 `bytes.slice().buffer`를 사용했는데, Node `Buffer.prototype.slice`는 복사가 아니라 subarray view(deprecated alias)라 `.buffer`가 원본 전체 buffer를 가리킨다. 그 결과 TCP read loop가 넘긴 frame subarray(offset 4 이상)가 worker에 잘못된 offset/길이로 transfer되어 decode가 실패했다(WS는 `new Uint8Array(payload)`로 이미 owned full buffer라 첫 분기를 타서 영향 없었다). `new Uint8Array(len); copy.set(bytes)`로 명시적 정확 구간 복사로 바꿔 Buffer/Uint8Array 양쪽에서 안전하게 동작하도록 했다. 이 버그는 in-process frame-ingest(owned Uint8Array)에서는 드러나지 않던 것으로, 실제 transport 경로 도입으로 처음 노출되었다. +- gateway transport baseline은 server inbound path 한쪽에만 gateway를 attach해 효과 해석을 단순화했다(계획 지침 준수). concurrency 16 request-response로 off/on을 동일 조건 측정하고, connection별 nonce 단조 증가(FIFO)를 raw `addRequestListener`로 감시한다. `gatewayBacklog`는 cleanup 계측 전까지 0으로 둔다(`gateway-cleanup` plan에서 정밀화 예정). +- 측정 결과는 작은 payload에서 gateway-on의 p95/p99와 throughput이 off보다 나빴다(예: full ws throughput off 16148 rps vs on 2539 rps, tcp p99 off 43ms vs on 194ms). 이는 milestone gateway 정책의 "작은 payload/in-process는 이득 불명확 → inline fallback 기본" 기대와 일치하며, default off 정책을 정량적으로 뒷받침한다. 안정성 hard gate(timeout/nonce/type/FIFO/leak) 위반은 모든 행에서 0이다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- TCP/WS no-gateway path가 `onReceivedFrame` fallback으로 기존 behavior를 보존하는가. +- gateway attach path의 parse error가 disconnect semantics를 유지하는가. +- stress 결과에 `frame-ingest`, `tcp`, `ws` rows가 모두 있고 `gateway=off,on` 비교가 가능하다. +- full matrix가 TypeScript transport 변경 후에도 PASS다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. + +### GATEWAY_REAL_PATH-1 중간 검증 + +```text +$ cd typescript && npm run check && npm test + +> proto-socket@1.0.5 check +> tsc --noEmit + +> proto-socket@1.0.5 test +> vitest run + + RUN v3.2.4 /config/workspace/proto-socket/typescript + + ✓ test/base_client.test.ts (7 tests) 7ms + ✓ test/browser_ws_client.test.ts (7 tests) 27ms + ✓ test/communicator.test.ts (24 tests) 435ms + ✓ test/tcp.test.ts (7 tests) 454ms + ✓ TCP > TCP read loop drives server inbound gateway: concurrent request-response keeps FIFO/nonce + ✓ test/ws.test.ts (7 tests) 1291ms + ✓ WS > WS read loop drives server inbound gateway: concurrent request-response keeps FIFO/nonce + + Test Files 5 passed (5) + Tests 52 passed (52) +``` + +추가한 transport gateway 테스트(`tcp.test.ts`, `ws.test.ts`)와 기존 backpressure/roundtrip 테스트가 모두 통과한다. exit code 0. + +### GATEWAY_REAL_PATH-2 중간 검증 + +```text +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +RUN stress lang=typescript mode=quick transports=tcp,ws profiles=gateway +INFO stress harness language=TypeScript mode=quick transports=tcp,ws profiles=gateway typeName=TestData +[gateway] mode=quick frames=500 +[gateway] on=false dispatched=500 violations=0 +[gateway] on=true dispatched=500 violations=0 +[gateway] transport=tcp on=false requests=64 violations=0 +[gateway] transport=tcp on=true requests=64 violations=0 +[gateway] transport=ws on=false requests=64 violations=0 +[gateway] transport=ws on=true requests=64 violations=0 +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) + +전체 결과값: PASS +결과 기록 파일: `agent-test/runs/20260603-074347-proto-socket-stress-quick.md` +``` + +기록 파일의 gateway rows(`frame-ingest`, `tcp`, `ws` × `gateway=off,on`): + +```text +| gateway | frames=500 | TypeScript | frame-ingest | 19 | 0 | 500 | 79446.5 | ... | off | 84.6 | PASS | +| gateway | frames=500 | TypeScript | frame-ingest | 19 | 0 | 500 | 2423.9 | ... | on | 196.1 | PASS | +| gateway | transport=tcp concurrency=16 | TypeScript | tcp | 7 | 1 | 64 | 479.9 | 41.882 | 43.015 | 43.032 | ... | off | 109.9 | PASS | +| gateway | transport=tcp concurrency=16 | TypeScript | tcp | 7 | 1 | 64 | 203.0 | 42.414 | 187.376 | 187.392 | ... | on | 201.3 | PASS | +| gateway | transport=ws concurrency=16 | TypeScript | ws | 7 | 1 | 64 | 6074.9 | 1.800 | 4.227 | 4.272 | ... | off | 115.1 | PASS | +| gateway | transport=ws concurrency=16 | TypeScript | ws | 7 | 1 | 64 | 263.0 | 1.768 | 236.880 | 236.891 | ... | on | 199.9 | PASS | +``` + +`overall_result: PASS`, transport에 `frame-ingest`/`tcp`/`ws` rows 모두 포함, `gateway=off,on` 비교 가능. + +### GATEWAY_REAL_PATH-3 중간 검증 + +```text +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport tcp,ws --profile gateway +RUN stress lang=typescript mode=full transports=tcp,ws profiles=gateway +INFO stress harness language=TypeScript mode=full transports=tcp,ws profiles=gateway typeName=TestData +[gateway] mode=full frames=5000 +[gateway] on=false dispatched=5000 violations=0 +[gateway] on=true dispatched=5000 violations=0 +[gateway] transport=tcp on=false requests=640 violations=0 +[gateway] transport=tcp on=true requests=640 violations=0 +[gateway] transport=ws on=false requests=640 violations=0 +[gateway] transport=ws on=true requests=640 violations=0 +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) + +전체 결과값: PASS +결과 기록 파일: `agent-test/runs/20260603-074404-proto-socket-stress-full.md` +``` + +### 최종 검증 + +```text +$ cd typescript && npm run check && npm test +> proto-socket@1.0.5 check +> tsc --noEmit +> proto-socket@1.0.5 test +> vitest run + Test Files 5 passed (5) + Tests 52 passed (52) +``` + +```text +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport tcp,ws --profile gateway +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) +전체 결과값: PASS +결과 기록 파일: `agent-test/runs/20260603-074404-proto-socket-stress-full.md` +``` + +full stress gateway rows(required SKIP 0, stability violations 0): + +```text +| gateway | frames=5000 | TypeScript | frame-ingest | 19 | 0 | 5000 | 137452.3 | ... | off | 97.4 | PASS | +| gateway | frames=5000 | TypeScript | frame-ingest | 19 | 0 | 5000 | 14608.2 | ... | on | 212.7 | PASS | +| gateway | transport=tcp concurrency=16 | TypeScript | tcp | 7 | 1 | 640 | 390.3 | 41.613 | 42.422 | 43.026 | ... | off | 132.5 | PASS | +| gateway | transport=tcp concurrency=16 | TypeScript | tcp | 7 | 1 | 640 | 347.5 | 41.863 | 44.832 | 193.900 | ... | on | 225.4 | PASS | +| gateway | transport=ws concurrency=16 | TypeScript | ws | 7 | 1 | 640 | 16148.4 | 0.749 | 1.563 | 2.545 | ... | off | 136.8 | PASS | +| gateway | transport=ws concurrency=16 | TypeScript | ws | 7 | 1 | 640 | 2538.9 | 1.165 | 3.634 | 188.272 | ... | on | 227.2 | PASS | +``` + +```text +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all +(서버\클라이언트 매트릭스 전체 PASS: Dart.io/Go/Kotlin/Python/TypeScript × 모든 클라이언트 열) +크로스테스트 상세 전 방향 PASS (request-response 16/16, Dart.web 2/2). +test_profile: proto-socket-full-matrix +overall_result: PASS +결과 기록 파일: `agent-test/runs/20260603-074412-proto-socket-full-matrix.md` +``` + +세 검증 명령 모두 exit code 0. TypeScript transport 변경 후에도 cross-language 통신과 프로토콜 호환성이 유지된다. + +### 생성된 결과 파일 + +- `agent-test/runs/20260603-074347-proto-socket-stress-quick.md` (GATEWAY_REAL_PATH-2 quick) +- `agent-test/runs/20260603-074404-proto-socket-stress-full.md` (GATEWAY_REAL_PATH-3 full stress) +- `agent-test/runs/20260603-074412-proto-socket-full-matrix.md` (GATEWAY_REAL_PATH-3 full matrix) + +--- + +> **[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 | Implementing agent must not modify or execute archive, complete.log, or task-directory archive move steps. | +| Roadmap Targets | Fixed at stub creation | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan. | +| 검증 결과 | Implementing agent | Fill actual stdout/stderr under the fixed command headings. | +| 코드리뷰 결과 | Review agent appends | Not included until review. | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 검증 대조: + - `npm run check` in `typescript/`: PASS. + - `npm test` in `typescript/`: PASS, 5 files / 52 tests passed. + - `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport tcp,ws --profile gateway`: PASS, rows=6, skips=0, required_skips=0, violations=0; result=`agent-test/runs/20260603-080241-proto-socket-stress-full.md`. + - 기존 full matrix 기록 `agent-test/runs/20260603-074412-proto-socket-full-matrix.md`: PASS로 대조함. +- 다음 단계: PASS이므로 `complete.log` 작성 후 active task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/complete.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/complete.log new file mode 100644 index 0000000..9bc022e --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/complete.log @@ -0,0 +1,45 @@ +# Complete - m-high-performance-parallel-operations/06_gateway_real_path + +## 완료 일시 + +2026-06-03 + +## 요약 + +TypeScript TCP/WS 실제 transport read loop의 gateway on/off baseline을 추가한 1회 루프가 PASS로 종료되었다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | TypeScript transport gateway path, stress rows, 검증 근거 모두 충족 | + +## 구현/정리 내용 + +- TypeScript TCP/WS read loop가 raw `PacketBase` bytes를 `Communicator.onReceivedFrame`으로 전달하도록 정리했다. +- gateway off inline fallback은 `enqueueInbound` Promise를 반환해 기존 inbound backpressure를 유지하고, gateway on은 worker decode path로 제출한다. +- Node worker gateway가 Buffer subarray frame을 정확한 owned buffer로 복사해 실제 transport frame ingest에서도 decode하도록 고쳤다. +- stress gateway profile에 `frame-ingest`, `tcp`, `ws` rows와 `gateway=off,on` 비교를 추가했다. +- TCP/WS server inbound gateway request-response 테스트를 추가했다. + +## 최종 검증 + +- `npm run check` in `typescript/` - PASS; `tsc --noEmit`. +- `npm test` in `typescript/` - PASS; Vitest 5 files / 52 tests passed. +- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport tcp,ws --profile gateway` - PASS; rows=6, skips=0, required_skips=0, violations=0; result=`agent-test/runs/20260603-080241-proto-socket-stress-full.md`. +- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` - PASS; result=`agent-test/runs/20260603-074412-proto-socket-full-matrix.md`. + +## Roadmap Completion + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Completed task ids: + - `gateway-real-path`: PASS; evidence=`agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/plan_cloud_G07_0.log`, `agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/code_review_cloud_G07_0.log`; verification=`agent-test/runs/20260603-080241-proto-socket-stress-full.md`, `agent-test/runs/20260603-074412-proto-socket-full-matrix.md` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/plan_cloud_G07_0.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/plan_cloud_G07_0.log new file mode 100644 index 0000000..51b2555 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/plan_cloud_G07_0.log @@ -0,0 +1,279 @@ + + +# Plan - GATEWAY_REAL_PATH + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 필수 마지막 단계다. 구현 후 검증을 실행하고, 실제 stdout/stderr를 review stub에 기록한 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌이 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 남기고 중단한다. 명령 재실행이나 산출물 수집으로 해소 가능한 증거 공백은 사용자 리뷰 요청이 아니다. + +## 배경 + +현재 TypeScript gateway benchmark는 `Communicator.onReceivedFrame`에 raw frame을 직접 주입하는 in-process 경로만 측정한다. 실제 TCP/WS read loop는 `PacketBase`를 직접 decode한 뒤 `enqueueInbound`로 넣기 때문에 gateway on/off 정책을 실제 transport 수신 경로에서 판단할 수 없다. 이 작업은 TypeScript TCP/WS 수신 경로가 gateway opt-in을 통과할 수 있게 하고, benchmark가 `gateway=off,on` 실제 transport rows를 기록하게 만든다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review만 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Task ids: + - `gateway-real-path`: TCP/WS 실제 수신 경로에서 gateway on/off baseline을 추가한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/milestones/high-performance-parallel-operations.md` +- `agent-test/local/rules.md` +- `agent-test/local/proto-socket-full-matrix.md` +- `agent-test/local/typescript-smoke.md` +- `typescript/bench/stress.ts` +- `typescript/src/communicator.ts` +- `typescript/src/inbound_gateway.ts` +- `typescript/src/node_inbound_gateway.ts` +- `typescript/src/base_client.ts` +- `typescript/src/tcp_client.ts` +- `typescript/src/tcp_server.ts` +- `typescript/src/node_ws_client.ts` +- `typescript/src/node_ws_server.ts` +- `typescript/test/communicator.test.ts` +- `typescript/test/tcp.test.ts` +- `typescript/test/ws.test.ts` +- `typescript/test/base_client.test.ts` +- `typescript/package.json` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`는 존재하며 읽었다. +- TypeScript 변경이므로 `agent-test/local/typescript-smoke.md`를 적용한다: `cd typescript && npm run check && npm test`. +- 실제 transport 수신 경로와 public protocol/API 동작에 영향을 주므로 `agent-test/local/proto-socket-full-matrix.md`도 적용한다: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all`. +- gateway baseline 자체는 stress runner 계약을 사용한다. command는 repository script help와 기존 milestone context에 근거한 보조 검증으로 둔다. +- `<확인 필요>` 값은 없었다. + +### 테스트 커버리지 공백 + +- 기존 `typescript/test/communicator.test.ts:531` 이후는 gateway reorder, fallback, decode error, Node worker ordering, stale result drop을 검증한다. +- 기존 `typescript/test/tcp.test.ts`와 `typescript/test/ws.test.ts`는 transport roundtrip을 검증하지만 gateway attach 상태의 TCP/WS 실제 수신 경로는 검증하지 않는다. +- 기존 `typescript/bench/stress.ts:768`의 gateway profile은 in-process frame-ingest만 측정한다. 실제 TCP/WS read loop gateway on/off row가 없다. + +### 심볼 참조 + +- rename/remove 예정 symbol 없음. +- `onReceivedFrame` 사용처: `typescript/src/communicator.ts:219`, `typescript/bench/stress.ts:829`, `typescript/test/communicator.test.ts:553`, `typescript/test/communicator.test.ts:571`, `typescript/test/communicator.test.ts:598`, `typescript/test/communicator.test.ts:632`, `typescript/test/communicator.test.ts:657`. +- transport inline decode call site: `typescript/src/tcp_client.ts:84`, `typescript/src/node_ws_client.ts:315`. + +### 분할 판단 + +- split policy를 plan 작성 전에 평가했다. +- shared task group: `agent-task/m-high-performance-parallel-operations/`. +- `06_gateway_real_path`: 실제 transport read loop와 benchmark baseline 변경. 독립 실행 가능. +- `07+06_gateway_cleanup`: gateway close/cancel/drain 계측. `06_gateway_real_path`에서 실제 transport gateway rows와 backlog 표현을 잡은 뒤 구현하도록 의존성을 둔다. +- 이 plan은 predecessor가 없는 독립 subtask다. + +### 범위 결정 근거 + +- Dart/Go/Kotlin/Python transport 구현은 제외한다. 현재 gateway worker implementation은 TypeScript Node worker path에만 존재한다. +- wire format, proto schema, nonce 의미, request/response API는 변경하지 않는다. +- gateway default policy는 이미 roadmap의 `gateway-threshold`에서 default off로 선언했다. 이 plan은 default on 전환을 하지 않는다. +- browser Web Worker gateway는 제외한다. Node `worker_threads` 실제 TCP/WS baseline만 다룬다. + +### 빌드 등급 + +- build lane: `cloud-G07`. 실제 transport ingest, worker gateway, benchmark runner, full matrix evidence가 얽힌 concurrency/terminal-benchmark 작업이다. +- review lane: `cloud-G07`. inline/gateway 경로의 parse-error, FIFO, close 동작을 코드와 결과 기록 양쪽에서 대조해야 한다. + +## 구현 체크리스트 + +- [ ] [GATEWAY_REAL_PATH-1] TypeScript TCP/WS read loop가 raw `PacketBase` bytes를 `Communicator.onReceivedFrame`으로 전달하게 바꾸고, gateway off 상태에서 기존 inline 동작이 보존되는지 테스트한다. +- [ ] [GATEWAY_REAL_PATH-2] TypeScript stress gateway profile에 실제 `tcp`/`ws` transport rows를 추가하고, `gateway=off,on`별 throughput, p95/p99, memory, backlog를 기록한다. +- [ ] [GATEWAY_REAL_PATH-3] focused stress와 TypeScript tests, 전체 matrix를 실행해 `gateway-real-path` 완료 근거를 만든다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +1. `GATEWAY_REAL_PATH-1`에서 transport read loop를 `onReceivedFrame` 경유로 통일한다. +2. `GATEWAY_REAL_PATH-2`에서 benchmark rows를 추가한다. +3. `GATEWAY_REAL_PATH-3`에서 focused stress, TypeScript smoke, full matrix를 순서대로 실행한다. + +## [GATEWAY_REAL_PATH-1] Transport Read Loop Gateway Path + +### 문제 + +`Communicator`는 gateway opt-in raw frame entrypoint를 이미 제공한다(`typescript/src/communicator.ts:219`). + +```ts +219 onReceivedFrame(raw: Uint8Array): void { +224 if (gateway !== null) { +225 this.frameSeq += 1; +226 gateway.submit({ seq: this.frameSeq, bytes: raw }); +``` + +하지만 TCP/WS read loop는 raw frame을 넘기지 않고 직접 decode한다. + +```ts +83 try { +84 const base = fromBinary(PacketBaseSchema, packetBytes); +85 await this.communicator.enqueueInbound(base.typeName, base.data, base.nonce, base.responseNonce); +``` + +```ts +313 private async onMessage(data: Uint8Array): Promise { +314 try { +315 const base = fromBinary(PacketBaseSchema, data); +316 await this.communicator.enqueueInbound(base.typeName, base.data, base.nonce, base.responseNonce); +``` + +### 해결 방법 + +- `TcpClient`와 `NodeWsClient`에서 parse error handler를 `onDisconnected()`로 연결한다. +- TCP frame payload와 WS binary message를 `this.communicator.onReceivedFrame(...)`으로 전달한다. +- 기존 pause/resume 및 parseQueue 순서는 유지한다. +- `fromBinary(PacketBaseSchema, ...)` import가 transport 파일에서 더 이상 필요 없으면 제거한다. + +예상 변경: + +```ts +// constructor init 이후 +this.communicator.setFrameErrorHandler(() => { + void this.onDisconnected(); +}); + +// TCP +this.communicator.onReceivedFrame(packetBytes); +void this.sendHeartbeat(); + +// WS +this.communicator.onReceivedFrame(data); +void this.sendHeartbeat(); +``` + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/src/tcp_client.ts`: inline decode를 `onReceivedFrame`으로 교체한다. +- [ ] `typescript/src/node_ws_client.ts`: inline decode를 `onReceivedFrame`으로 교체한다. +- [ ] `typescript/test/tcp.test.ts`: server/client 중 한쪽 이상에 `createNodeWorkerGateway`를 attach한 TCP request-response test를 추가한다. +- [ ] `typescript/test/ws.test.ts`: server/client 중 한쪽 이상에 `createNodeWorkerGateway`를 attach한 WS request-response test를 추가한다. + +### 테스트 작성 + +- 작성한다. TCP/WS transport에서 실제 read loop가 gateway path를 타는지 기존 communicator unit test만으로는 검증되지 않는다. +- test 목표: gateway attach 상태에서도 send-push와 request-response가 PASS이고 기존 FIFO/nonce semantics가 유지된다. + +### 중간 검증 + +```bash +cd typescript && npm run check && npm test +``` + +예상 결과: exit code 0. + +## [GATEWAY_REAL_PATH-2] Stress Gateway Transport Rows + +### 문제 + +현재 gateway profile은 `frame-ingest`만 기록한다. + +```ts +768 /** +769 * gateway on/off baseline. communicator의 `onReceivedFrame` opt-in 경로에 raw PacketBase frame을 흘려 +773 async function profileGateway(mode: Mode): Promise { +836 emitThroughputRow("gateway", `frames=${count}`, count, elapsed, stability, gatewayOn ? "on" : "off", memMb(), { +837 transport: TRANSPORT_FRAME_INGEST, +``` + +milestone의 `gateway-real-path` 검증은 실제 TCP/WS frame ingest의 gateway on/off row를 요구한다. + +### 해결 방법 + +- `profileGateway`를 유지하되 `frame-ingest` rows 뒤에 `tcp`와 `ws` rows를 추가한다. +- transport rows는 동일 payload/count 기준으로 `gateway=off,on`을 모두 실행한다. +- gateway on은 client 또는 server의 `communicator.attachInboundGateway(createNodeWorkerGateway(...))`로 적용한다. 양방향을 동시에 켜면 효과 해석이 흐려지므로, 먼저 server inbound path를 기준으로 고정한다. +- row schema는 기존 `Row`를 유지한다. `gatewayBacklog`는 cleanup 계측 전까지 0 또는 현재 관측 가능한 값으로 두고, cleanup plan에서 정밀화한다. + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/bench/stress.ts`: gateway profile 설명을 실제 transport row 포함으로 갱신한다. +- [ ] `typescript/bench/stress.ts`: `runGatewayTransportBaseline(transport, mode, gatewayOn)` 헬퍼를 추가한다. +- [ ] `typescript/bench/stress.ts`: `profileGateway`가 `frame-ingest`, `tcp`, `ws` rows를 모두 출력하게 한다. +- [ ] `typescript/bench/stress.ts`: row의 `transport`가 `frame-ingest`, `tcp`, `ws`로 구분되고 `gateway`가 `off`/`on`으로 기록되는지 확인한다. + +### 테스트 작성 + +- 별도 unit test보다 stress result contract로 검증한다. 이 변경은 benchmark harness 출력 축 추가이며, functional transport test는 `GATEWAY_REAL_PATH-1`에서 추가한다. + +### 중간 검증 + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +``` + +예상 결과: `overall_result: PASS`, `transport`에 `frame-ingest`, `tcp`, `ws` rows가 포함된다. + +## [GATEWAY_REAL_PATH-3] Evidence Run + +### 문제 + +Roadmap completion은 실제 결과 파일이 필요하다. focused quick만으로는 full baseline 근거가 약하다. + +### 해결 방법 + +- TypeScript smoke를 먼저 실행한다. +- gateway focused full stress를 실행해 결과 파일을 만든다. +- 전체 matrix를 실행해 TypeScript transport 경로 변경이 cross-language 동작을 깨지 않았음을 확인한다. + +### 수정 파일 및 체크리스트 + +- [ ] `agent-test/runs/`: stress runner가 새 결과 파일을 자동 생성한다. +- [ ] `CODE_REVIEW-cloud-G07.md`: 결과 파일 경로와 명령 출력을 기록한다. + +### 테스트 작성 + +- 추가 테스트 파일 작성 없음. 검증 산출물을 남기는 단계다. + +### 중간 검증 + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport tcp,ws --profile gateway +``` + +예상 결과: `overall_result: PASS`, required SKIP 0, stability violations 0. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `typescript/src/tcp_client.ts` | GATEWAY_REAL_PATH-1 | +| `typescript/src/node_ws_client.ts` | GATEWAY_REAL_PATH-1 | +| `typescript/test/tcp.test.ts` | GATEWAY_REAL_PATH-1 | +| `typescript/test/ws.test.ts` | GATEWAY_REAL_PATH-1 | +| `typescript/bench/stress.ts` | GATEWAY_REAL_PATH-2, GATEWAY_REAL_PATH-3 | +| `CODE_REVIEW-cloud-G07.md` | GATEWAY_REAL_PATH-3 | + +## 리뷰어를 위한 체크포인트 + +- TCP/WS no-gateway path가 `onReceivedFrame` fallback으로 기존 behavior를 보존하는가. +- gateway attach path의 parse error가 disconnect semantics를 유지하는가. +- stress 결과에 `frame-ingest`, `tcp`, `ws` rows가 모두 있고 `gateway=off,on` 비교가 가능하다. +- full matrix가 TypeScript transport 변경 후에도 PASS다. + +## 최종 검증 + +```bash +cd typescript && npm run check && npm test +``` + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport tcp,ws --profile gateway +``` + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all +``` + +모든 명령은 exit code 0이어야 한다. Go test cache 관련 없음. stress/matrix runner가 생성한 `agent-test/runs/*.md` 경로를 review stub에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_0.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_0.log new file mode 100644 index 0000000..4b90aeb --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_0.log @@ -0,0 +1,272 @@ +ㄹ + +# Code Review Reference - GATEWAY_CLEANUP + +> **[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. +> 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-03 +task=m-high-performance-parallel-operations/07+06_gateway_cleanup, plan=0, tag=GATEWAY_CLEANUP + +## Roadmap Targets + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Task ids: + - `gateway-cleanup`: gateway worker close/cancel/drain 계측을 추가한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-high-performance-parallel-operations/07+06_gateway_cleanup/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [GATEWAY_CLEANUP-1] Gateway diagnostics contract | [x] | +| [GATEWAY_CLEANUP-2] Close/cancel/drain tests | [x] | +| [GATEWAY_CLEANUP-3] Stress cleanup evidence | [x] | + +## 구현 체크리스트 + +- [x] [GATEWAY_CLEANUP-1] gateway stats/diagnostics 계약을 추가해 queued, active, reorder pending, sink pending, worker pending, closed 상태를 관측한다. +- [x] [GATEWAY_CLEANUP-2] close/cancel/drain tests를 추가해 close 후 stats가 비고 stale sink/onError delivery가 없는지 검증한다. +- [x] [GATEWAY_CLEANUP-3] stress gateway rows에 cleanup/backlog evidence를 기록하고 repeated quick run에서 memory/backlog가 누적되지 않음을 결과 파일로 남긴다. +- [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_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-high-performance-parallel-operations/07+06_gateway_cleanup/`를 `agent-task/archive/YYYY/MM/m-high-performance-parallel-operations/07+06_gateway_cleanup/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-high-performance-parallel-operations/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.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로 이동한다. + +## 계획 대비 변경 사항 + +- `typescript/src/communicator.ts`는 변경하지 않았다. 계획의 GATEWAY_CLEANUP-1 체크리스트는 "필요한 경우 attached gateway stats를 읽는 helper를 추가하되 public surface를 최소화한다"는 조건부 항목이었다. stress harness가 `createNodeWorkerGateway(...)` 반환 핸들을 직접 보유해 backlog를 관측하므로 communicator를 경유하는 helper가 필요 없었고, 계획의 "public surface 최소화" 지침에 따라 추가하지 않았다. 따라서 communicator의 public API는 변경되지 않았다. +- `typescript/bench/stress.ts`의 gateway row emit 구조를 조정했다. 기존에는 frame-ingest/ transport row를 `try`(성공)와 `catch`(에러) 양쪽에서 각각 emit했는데, close 후 잔여 backlog를 row에 반영하려면 close가 끝난 뒤 1회만 emit해야 한다. 그래서 두 경로 모두 `finally`에서 `comm.close()`(또는 `server.stop()`)로 gateway를 close한 뒤 잔여 backlog를 측정하고, 그 다음 단일 지점에서 row를 emit하도록 바꿨다. 성공/에러 양쪽에서 row를 1개 emit하는 동작은 동일하다. + +## 주요 설계 결정 + +- `InboundGateway.stats?()`를 optional method로 추가했다. 기존 custom gateway 구현(`submit`/`close`만 가진 구현)을 깨지 않기 위함이며, stats 미구현 gateway는 진단 backlog를 0으로 간주한다. +- backlog leak 판정 대상은 `queued + reorderPending + sinkPending + workerPending`로 정의하고 `active`(in-flight decode loop 수)는 제외했다. close 시점에 await 중이던 decode loop는 close 후 잠시 drain되는 것이 정상이므로, 이를 leak으로 오판하지 않기 위함이다. close 후 drain은 별도 단위 테스트(`active===0`까지 waitFor)로 검증한다. +- `WorkerGateway.sinkPending`은 `collect()`에서 `ready.length`만큼 증가하고 sink chain 콜백의 `finally`에서 1씩 감소한다. `close()`가 전달을 폐기하며 `sinkPending`을 즉시 0으로 리셋하므로, 이미 큐된 콜백의 `finally`가 카운터를 음수로 떨어뜨리지 않도록 `if (this.sinkPending > 0)` 가드를 둔다. 또한 close 후 콜백은 기존 `return` 대신 `continue`로 남은 항목을 소비해(전달은 closed guard로 건너뜀) 카운터 정리를 보장한다. stale delivery 금지 동작은 그대로 유지된다. +- Node worker gateway의 `stats()`는 reorder/sink/queue/active backlog를 소유한 `WorkerGateway.stats()`와 off-thread decode pending을 소유한 `WorkerThreadDecodePool.pendingCount()`를 합성해 반환한다. `close()` 시 pool의 `failAll`이 pending을 비우므로 close 후 `workerPending=0`이 보장된다. +- stress backlog는 부하 중 2ms 간격 `setInterval` 샘플링으로 얻은 peak과 close 후 잔여(residual) 중 더 위험한 값(`Math.max`)을 row의 `gatewayBacklog`로 기록한다. residual이 0이 아니면 `stability.pendingLeak`을 증가시켜 hard gate FAIL로 만든다. gateway off row와 stats 미구현 gateway는 backlog 0으로 남는다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- stats API가 기존 custom `InboundGateway` 구현을 깨지 않도록 optional인지 확인한다. +- close 후 queued/reorder/worker pending이 0으로 관측되는지 확인한다. +- stale result/onError suppression이 기존 tests와 함께 유지되는지 확인한다. +- stress row의 `gatewayBacklog`가 하드코딩 0이 아니라 실제 stats 기반인지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. + +> 참고: GATEWAY_CLEANUP-1과 -2는 동일한 `npm run check && npm test` 계약을 공유한다. -1(src diagnostics) 구현 직후 type check가 통과했고, -2(tests) 추가 후 동일 명령으로 신규 3개 테스트를 포함해 55개 전체가 통과했다. 아래는 최종 실행 출력이며 -1/-2 중간 검증 결과와 동일하다. + +### GATEWAY_CLEANUP-1 중간 검증 + +```text +$ cd typescript && npm run check +> proto-socket@1.0.5 check +> tsc --noEmit +(exit code 0) +``` + +### GATEWAY_CLEANUP-2 중간 검증 + +```text +$ cd typescript && npm run check && npm test +> proto-socket@1.0.5 check +> tsc --noEmit + +> proto-socket@1.0.5 test +> vitest run + + RUN v3.2.4 /config/workspace/proto-socket/typescript + + ✓ test/base_client.test.ts (7 tests) 8ms + ✓ test/browser_ws_client.test.ts (7 tests) 31ms + ✓ test/communicator.test.ts (27 tests) 497ms + ✓ test/tcp.test.ts (7 tests) 509ms + ✓ test/ws.test.ts (7 tests) 1405ms + + Test Files 5 passed (5) + Tests 55 passed (55) +``` + +신규 추가 테스트(describe "WorkerGateway cleanup stats", 3개): +- close()는 queued/reorder/sink backlog를 비우고 closed=true로 만든다 +- stats()는 out-of-order 결과의 reorder pending을 관측하고 close가 비운다 +- Node worker gateway: pending decode가 있는 상태에서 close 후 workerPending=0 + +### GATEWAY_CLEANUP-3 중간 검증 + +```text +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +RUN stress lang=typescript mode=quick transports=tcp,ws profiles=gateway +INFO stress harness language=TypeScript mode=quick transports=tcp,ws profiles=gateway typeName=TestData +[gateway] mode=quick frames=500 +[gateway] on=false dispatched=500 backlog=0 residual=0 violations=0 +[gateway] on=true dispatched=500 backlog=500 residual=0 violations=0 +[gateway] transport=tcp on=false requests=64 backlog=0 residual=0 violations=0 +[gateway] transport=tcp on=true requests=64 backlog=16 residual=0 violations=0 +[gateway] transport=ws on=false requests=64 backlog=0 residual=0 violations=0 +[gateway] transport=ws on=true requests=64 backlog=16 residual=0 violations=0 +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) + +전체 결과값: PASS +결과 기록 파일: `agent-test/runs/20260603-082530-proto-socket-stress-quick.md` +``` + +close 후 backlog가 실측값으로 기록되고(`gatewayBacklog`: frame-ingest on=500 peak, tcp/ws on=16 peak, off=0), close 후 residual=0이라 `PendLeak=0`이다. residual이 0이 아니었다면 `pendingLeak` 증가로 FAIL했을 것이다. + +repeated quick run에서 memory/backlog 누적이 없음을 동일 명령 2회 추가 실행으로 확인했다: + +```text +# run #2 -> agent-test/runs/20260603-082544-proto-socket-stress-quick.md +[gateway] on=true dispatched=500 backlog=500 residual=0 violations=0 +[gateway] transport=tcp on=true requests=64 backlog=16 residual=0 violations=0 +[gateway] transport=ws on=true requests=64 backlog=16 residual=0 violations=0 +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) + +# run #3 -> agent-test/runs/20260603-082545-proto-socket-stress-quick.md +[gateway] on=true dispatched=500 backlog=500 residual=0 violations=0 +[gateway] transport=tcp on=true requests=64 backlog=16 residual=0 violations=0 +[gateway] transport=ws on=true requests=64 backlog=16 residual=0 violations=0 +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) +``` + +3회 run의 gateway-on row Mem(MB)는 누적 증가 없이 안정적이다(결과 파일 기준): +- frame-ingest on: 194.6 / 194.7 / 195.3 +- tcp on: 195.8 / 198.6 / 195.5 +- ws on: 198.0 / 197.1 / 195.6 +- 모든 run의 모든 row에서 PendLeak=0, GatewayBacklog는 동일(500/16), violations=0. + +### 최종 검증 + +```text +$ cd typescript && npm run check && npm test +> proto-socket@1.0.5 check +> tsc --noEmit + +> proto-socket@1.0.5 test +> vitest run + + RUN v3.2.4 /config/workspace/proto-socket/typescript + + ✓ test/base_client.test.ts (7 tests) 8ms + ✓ test/browser_ws_client.test.ts (7 tests) 31ms + ✓ test/communicator.test.ts (27 tests) 497ms + ✓ test/tcp.test.ts (7 tests) 509ms + ✓ test/ws.test.ts (7 tests) 1405ms + + Test Files 5 passed (5) + Tests 55 passed (55) +(exit code 0) + +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) +전체 결과값: PASS +결과 기록 파일: `agent-test/runs/20260603-082530-proto-socket-stress-quick.md` +(추가 repeated run: `agent-test/runs/20260603-082544-...`, `agent-test/runs/20260603-082545-...`) + +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all +언어 단독 테스트: Dart/Go/Kotlin/Python/TypeScript 모두 PASS +언어 PASS 매트릭스: 5개 서버 x 7개 클라이언트 조합 전부 PASS +크로스테스트 상세: 모든 방향 PASS (concurrency 16 = 16/16, Dart.web/Dart.web(WSS) = 2/2) +결과 기록 파일: `agent-test/runs/20260603-082642-proto-socket-full-matrix.md` +``` + +모든 명령 exit code 0 / PASS. 생성된 결과 파일: +- `agent-test/runs/20260603-082530-proto-socket-stress-quick.md` (gateway cleanup evidence, run #1) +- `agent-test/runs/20260603-082544-proto-socket-stress-quick.md` (repeated run #2) +- `agent-test/runs/20260603-082545-proto-socket-stress-quick.md` (repeated run #3) +- `agent-test/runs/20260603-082642-proto-socket-full-matrix.md` (full matrix) + +--- + +> **[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 | Implementing agent must not modify or execute archive, complete.log, or task-directory archive move steps. | +| Roadmap Targets | Fixed at stub creation | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan. | +| 검증 결과 | Implementing agent | Fill actual stdout/stderr under the fixed command headings. | +| 코드리뷰 결과 | Review agent appends | Not included until review. | + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Pass + - completeness: Fail + - test coverage: Fail + - API contract: Fail + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: + - Required: [typescript/src/communicator.ts](/config/workspace/proto-socket/typescript/src/communicator.ts:19) 의 gateway re-export 목록에 `type InboundGatewayStats`가 빠져 있다. `typescript/src/index.ts`와 `typescript/src/node.ts`는 public package surface를 `communicator.ts` re-export에 의존하므로, 계획의 `InboundGatewayStats` diagnostics contract를 사용자가 지원되는 package export에서 직접 import할 수 없다. 해결: `communicator.ts`의 `./inbound_gateway.js` re-export 목록에 `type InboundGatewayStats`를 추가하고, 필요하면 `typescript/src/node.ts`/barrel import smoke로 확인한다. + - Required: [typescript/test/communicator.test.ts](/config/workspace/proto-socket/typescript/test/communicator.test.ts:737) 의 cleanup stats 테스트가 `sinkPending`을 실제 양수 상태로 만들지 않는다. 현재 검증은 `before.sinkPending === 0`과 close 후 0만 확인해, [typescript/src/inbound_gateway.ts](/config/workspace/proto-socket/typescript/src/inbound_gateway.ts:251) 의 `sinkPending += ready.length` 및 close 중 stale sink skip/decrement 경로를 검증하지 못한다. 해결: delayed sink로 `stats().sinkPending > 0`을 관측한 뒤 close하고, close 후 두 번째 stale sink delivery가 발생하지 않으며 stats가 0으로 비는 regression test를 추가한다. +- 다음 단계: FAIL이므로 user-review gate 없이 후속 `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.md`를 작성한다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_1.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_1.log new file mode 100644 index 0000000..d415b63 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_1.log @@ -0,0 +1,210 @@ + + +# Code Review Reference - REVIEW_GATEWAY_CLEANUP + +> **[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. +> 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-03 +task=m-high-performance-parallel-operations/07+06_gateway_cleanup, plan=1, tag=REVIEW_GATEWAY_CLEANUP + +## Roadmap Targets + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Task ids: + - `gateway-cleanup`: gateway worker close/cancel/drain 계측을 추가한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-high-performance-parallel-operations/07+06_gateway_cleanup/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_GATEWAY_CLEANUP-1] Export diagnostics stats type | [x] | +| [REVIEW_GATEWAY_CLEANUP-2] Cover sink pending close/drain path | [x] | + +## 구현 체크리스트 + +- [x] [REVIEW_GATEWAY_CLEANUP-1] `InboundGatewayStats`를 TypeScript public gateway export surface에서 import 가능하게 만든다. +- [x] [REVIEW_GATEWAY_CLEANUP-2] `sinkPending > 0` 상태를 만드는 close/drain regression test를 추가한다. +- [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_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-high-performance-parallel-operations/07+06_gateway_cleanup/`를 `agent-task/archive/YYYY/MM/m-high-performance-parallel-operations/07+06_gateway_cleanup/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-high-performance-parallel-operations/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.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로 이동한다. + +## 계획 대비 변경 사항 + +- 계획과 동일하게 구현했다. 별도 대체 명령이나 범위 변경은 없다. 두 Required issue(public export 누락, sinkPending 양수 미검증)만 닫았고 runtime behavior는 바꾸지 않았다. + +## 주요 설계 결정 + +- [REVIEW_GATEWAY_CLEANUP-1] `type InboundGatewayStats`를 `typescript/src/communicator.ts`의 `./inbound_gateway.js` re-export 목록에 추가했다. public barrel인 `typescript/src/index.ts`와 `typescript/src/node.ts`는 모두 `export * from "./communicator.js"`로 구성되어 있어, communicator re-export에 추가하는 것만으로 두 supported surface에서 `InboundGatewayStats`를 import할 수 있게 된다. 별도 runtime test 없이 `npm run check`(tsc)가 type export surface를 검증한다. +- [REVIEW_GATEWAY_CLEANUP-2] `WorkerGateway cleanup stats` describe에 delayed-sink regression test를 추가했다. decode를 즉시 완료시켜 두 결과가 reorder를 거쳐 sink chain에 스케줄되게 하고, 첫 sink 호출을 unresolved promise로 막아 `stats().sinkPending > 0`을 실제로 관측한다. 그 상태에서 `close()`가 `sinkPending`/`queued`/`reorderPending`/`workerPending`을 0으로 비우고 `closed=true`로 만드는지 확인한다. 마지막으로 blocked sink를 release한 뒤 두 번째 frame이 close 이후 stale delivery로 sink에 전달되지 않음을 검증한다. 이 테스트가 `inbound_gateway.ts`의 sinkPending 증가/감소 가드와 close-skip(`continue`) 경로를 직접 커버한다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `InboundGatewayStats`가 supported TypeScript barrel에서 import 가능한지 확인한다. +- 신규 test가 `sinkPending > 0`을 실제로 관측하는지 확인한다. +- close 후 두 번째 stale sink delivery가 발생하지 않는지 확인한다. +- 기존 gateway cleanup stress evidence가 깨지지 않는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### REVIEW_GATEWAY_CLEANUP-1 중간 검증 + +```text +$ cd typescript && npm run check +> proto-socket@1.0.5 check +> tsc --noEmit +(exit code 0) +``` + +### REVIEW_GATEWAY_CLEANUP-2 중간 검증 + +```text +$ cd typescript && npm test + RUN v3.2.4 /config/workspace/proto-socket/typescript + + ✓ test/base_client.test.ts (7 tests) 7ms + ✓ test/browser_ws_client.test.ts (7 tests) 26ms + ✓ test/tcp.test.ts (7 tests) 398ms + ✓ test/communicator.test.ts (28 tests) 441ms + ✓ test/ws.test.ts (7 tests) 1265ms + + Test Files 5 passed (5) + Tests 56 passed (56) +(exit code 0) +``` + +신규 추가 테스트(describe "WorkerGateway cleanup stats"): +- close()는 양수 sinkPending을 비우고 release된 sink가 stale delivery를 만들지 않는다 + +`communicator.test.ts`는 27 → 28 tests, 전체 55 → 56 tests로 늘었다. + +### 최종 검증 + +```text +$ cd typescript && npm run check +> proto-socket@1.0.5 check +> tsc --noEmit +(exit code 0) + +$ cd typescript && npm test + RUN v3.2.4 /config/workspace/proto-socket/typescript + ✓ test/base_client.test.ts (7 tests) + ✓ test/browser_ws_client.test.ts (7 tests) + ✓ test/tcp.test.ts (7 tests) + ✓ test/communicator.test.ts (28 tests) + ✓ test/ws.test.ts (7 tests) + + Test Files 5 passed (5) + Tests 56 passed (56) +(exit code 0) + +$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +[gateway] mode=quick frames=500 +[gateway] on=false dispatched=500 backlog=0 residual=0 violations=0 +[gateway] on=true dispatched=500 backlog=500 residual=0 violations=0 +[gateway] transport=tcp on=false requests=64 backlog=0 residual=0 violations=0 +[gateway] transport=tcp on=true requests=64 backlog=16 residual=0 violations=0 +[gateway] transport=ws on=false requests=64 backlog=0 residual=0 violations=0 +[gateway] transport=ws on=true requests=64 backlog=16 residual=0 violations=0 +DONE typescript: PASS (rows=6 skips=0 required_skips=0 violations=0) +전체 결과값: PASS +결과 기록 파일: `agent-test/runs/20260603-085639-proto-socket-stress-quick.md` +``` + +모든 명령 exit code 0 / PASS. gateway cleanup stress evidence는 1차와 동일하게 유지된다(backlog: frame-ingest on=500, tcp/ws on=16, off=0; residual=0; PendLeak=0; violations=0). 생성된 결과 파일: +- `agent-test/runs/20260603-085639-proto-socket-stress-quick.md` (follow-up 재확인) + +--- + +> **[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 | Implementing agent must not modify or execute archive, complete.log, or task-directory archive move steps. | +| Roadmap Targets | Fixed at stub creation | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS. | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only. | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only; final checkbox is mandatory. | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section. | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content. | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed. | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan. | +| 검증 결과 | Implementing agent | Fill actual stdout/stderr under the fixed command headings. | +| 코드리뷰 결과 | Review agent appends | Not included until review. | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS이므로 `complete.log`를 작성하고 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/complete.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/complete.log new file mode 100644 index 0000000..5005192 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/complete.log @@ -0,0 +1,44 @@ +# Complete - m-high-performance-parallel-operations/07+06_gateway_cleanup + +## 완료 일시 + +2026-06-03 + +## 요약 + +TypeScript gateway cleanup diagnostics와 follow-up export/test 보강이 2회 리뷰 루프에서 최종 PASS로 종료되었다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G06_0.log` | `code_review_cloud_G06_0.log` | FAIL | `InboundGatewayStats` public export 누락과 `sinkPending > 0` regression test 공백 발견 | +| `plan_cloud_G06_1.log` | `code_review_cloud_G06_1.log` | PASS | public export와 sinkPending close/drain regression test 보강 완료 | + +## 구현/정리 내용 + +- `InboundGatewayStats`를 TypeScript public gateway export surface에서 import 가능하게 했다. +- `WorkerGateway` cleanup stats tests에 `sinkPending > 0` 상태에서 close 후 backlog가 비고 stale sink delivery가 발생하지 않는 regression test를 추가했다. +- gateway cleanup stress rows는 frame-ingest/tcp/ws gateway on/off 모두 residual backlog 0, pending leak 0을 유지했다. + +## 최종 검증 + +- `cd typescript && npm run check` - PASS; `tsc --noEmit`. +- `cd typescript && npm test` - PASS; Vitest 5 files / 56 tests passed. +- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway` - PASS; rows=6, skips=0, required_skips=0, violations=0; result=`agent-test/runs/20260603-090117-proto-socket-stress-quick.md`. +- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` - PASS in first loop evidence; result=`agent-test/runs/20260603-082642-proto-socket-full-matrix.md`. + +## Roadmap Completion + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Completed task ids: + - `gateway-cleanup`: PASS; evidence=`agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_0.log`, `agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_0.log`, `agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_1.log`, `agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_1.log`; verification=`agent-test/runs/20260603-082530-proto-socket-stress-quick.md`, `agent-test/runs/20260603-082544-proto-socket-stress-quick.md`, `agent-test/runs/20260603-082545-proto-socket-stress-quick.md`, `agent-test/runs/20260603-082642-proto-socket-full-matrix.md`, `agent-test/runs/20260603-090117-proto-socket-stress-quick.md` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_0.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_0.log new file mode 100644 index 0000000..674e191 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_0.log @@ -0,0 +1,288 @@ + + +# Plan - GATEWAY_CLEANUP + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 필수 마지막 단계다. 구현 후 검증을 실행하고, 실제 stdout/stderr를 review stub에 기록한 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌이 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 남기고 중단한다. 명령 재실행이나 산출물 수집으로 해소 가능한 증거 공백은 사용자 리뷰 요청이 아니다. + +## 배경 + +gateway policy는 default off로 선언됐지만, default on/adaptive 후보를 판단하려면 close/cancel/drain이 숫자로 보여야 한다. 현재 gateway lifecycle tests는 stale result가 sink/onError로 전달되지 않는지만 검증하고, queued jobs, reorder pending, worker pool pending, repeated run memory/backlog는 결과 row에 남기지 않는다. 이 작업은 gateway cleanup 진단을 추가하고 stress 결과에 close 후 상태를 기록한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review만 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Task ids: + - `gateway-cleanup`: gateway worker close/cancel/drain 계측을 추가한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/milestones/high-performance-parallel-operations.md` +- `agent-test/local/rules.md` +- `agent-test/local/proto-socket-full-matrix.md` +- `agent-test/local/typescript-smoke.md` +- `typescript/bench/stress.ts` +- `typescript/src/communicator.ts` +- `typescript/src/inbound_gateway.ts` +- `typescript/src/node_inbound_gateway.ts` +- `typescript/src/base_client.ts` +- `typescript/src/tcp_client.ts` +- `typescript/src/tcp_server.ts` +- `typescript/src/node_ws_client.ts` +- `typescript/src/node_ws_server.ts` +- `typescript/test/communicator.test.ts` +- `typescript/test/tcp.test.ts` +- `typescript/test/ws.test.ts` +- `typescript/test/base_client.test.ts` +- `typescript/package.json` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`는 존재하며 읽었다. +- TypeScript gateway lifecycle 변경이므로 `agent-test/local/typescript-smoke.md`를 적용한다: `cd typescript && npm run check && npm test`. +- public exported gateway interface 또는 communicator diagnostics를 확장하면 `agent-test/local/proto-socket-full-matrix.md`도 적용한다: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all`. +- gateway cleanup evidence는 stress runner 보조 검증으로 남긴다. +- `<확인 필요>` 값은 없었다. + +### 테스트 커버리지 공백 + +- 기존 `typescript/test/communicator.test.ts:688` 이후는 close 후 pending decode resolve/reject가 sink/onError로 전달되지 않는지 검증한다. +- `WorkerGateway`의 queued jobs, active workers, reorder pending, sink delivery backlog를 숫자로 관측하는 test가 없다. +- `Node` worker pool의 pending decode 수와 close 후 pending clear/terminate 상태를 검증하는 test가 없다. +- `typescript/bench/stress.ts` gateway row는 `gatewayBacklog`를 항상 0으로 남겨 cleanup/drain 판단 근거가 부족하다. + +### 심볼 참조 + +- rename/remove 예정 symbol 없음. +- `InboundGateway` interface: `typescript/src/inbound_gateway.ts:37`, `typescript/src/communicator.ts:198`, `typescript/src/node_inbound_gateway.ts:142`, `typescript/bench/stress.ts:815`, `typescript/test/communicator.test.ts:617`. +- `WorkerGateway.close`: `typescript/src/inbound_gateway.ts:152`, tests at `typescript/test/communicator.test.ts:705`, `typescript/test/communicator.test.ts:729`. +- Node worker pool close: `typescript/src/node_inbound_gateway.ts:64`, wrapper close at `typescript/src/node_inbound_gateway.ts:163`. + +### 분할 판단 + +- split policy를 plan 작성 전에 평가했다. +- shared task group: `agent-task/m-high-performance-parallel-operations/`. +- 이 subtask path는 `07+06_gateway_cleanup`이며 predecessor index는 `06`이다. +- predecessor: active sibling `agent-task/m-high-performance-parallel-operations/06_gateway_real_path/PLAN-cloud-G07.md`가 생성되어 있으나 아직 `complete.log`는 없다. 구현은 `06_gateway_real_path` PASS 이후 시작해야 한다. +- dependency reason: cleanup row/backlog naming은 actual transport gateway baseline의 row schema와 맞춰야 한다. + +### 범위 결정 근거 + +- gateway default on/adaptive 전환은 하지 않는다. +- browser Web Worker 구현은 제외한다. +- `WorkerGateway`/Node worker gateway의 진단 API와 TypeScript stress/test 범위로 제한한다. +- wire format, proto schema, nonce semantics는 변경하지 않는다. + +### 빌드 등급 + +- build lane: `cloud-G06`. concurrency lifecycle와 worker termination 진단이 중심이지만 범위는 TypeScript gateway internals와 tests로 제한된다. +- review lane: `cloud-G06`. stats가 실제 cleanup 상태를 나타내는지, close 후 stale delivery가 없는지 확인이 필요하다. + +## 구현 체크리스트 + +- [ ] [GATEWAY_CLEANUP-1] gateway stats/diagnostics 계약을 추가해 queued, active, reorder pending, sink pending, worker pending, closed 상태를 관측한다. +- [ ] [GATEWAY_CLEANUP-2] close/cancel/drain tests를 추가해 close 후 stats가 비고 stale sink/onError delivery가 없는지 검증한다. +- [ ] [GATEWAY_CLEANUP-3] stress gateway rows에 cleanup/backlog evidence를 기록하고 repeated quick run에서 memory/backlog가 누적되지 않음을 결과 파일로 남긴다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 plan은 `07+06_gateway_cleanup`이므로 implementation은 같은 task group의 `06_gateway_real_path`가 `complete.log`를 만든 뒤 시작한다. predecessor가 아직 완료되지 않았다면 구현하지 말고 review stub의 사용자 리뷰 요청이 아니라 "predecessor incomplete" 상태로 보고한다. + +## [GATEWAY_CLEANUP-1] Gateway Diagnostics Contract + +### 문제 + +`InboundGateway`는 `submit`과 `close`만 제공한다. + +```ts +37 export interface InboundGateway { +39 submit(frame: InboundFrame): void; +41 close(): void; +42 } +``` + +`WorkerGateway` 내부에는 관측해야 할 상태가 있지만 모두 private이다. + +```ts +127 private readonly reorder = new FrameReorderBuffer(); +128 private readonly jobs: InboundFrame[] = []; +129 private activeWorkers = 0; +130 private closed = false; +131 private sinkChain: Promise = Promise.resolve(); +``` + +Node worker pool도 pending count가 private이다. + +```ts +33 class WorkerThreadDecodePool { +35 private readonly pending = new Map(); +38 private closed = false; +``` + +### 해결 방법 + +- `InboundGatewayStats` type을 추가한다. +- `InboundGateway`에 optional `stats?(): InboundGatewayStats`를 추가해 기존 custom gateway compatibility를 유지한다. +- `FrameReorderBuffer`에 `size` 또는 `pendingCount()`를 추가한다. +- `WorkerGateway.stats()`가 queued jobs, active workers, reorder pending, sink pending, closed를 반환하게 한다. +- `WorkerThreadDecodePool.stats()`와 `createNodeWorkerGateway(...).stats()` wrapper를 추가한다. + +예상 shape: + +```ts +export interface InboundGatewayStats { + queued: number; + active: number; + reorderPending: number; + sinkPending: number; + workerPending: number; + closed: boolean; +} +``` + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/src/inbound_gateway.ts`: stats type과 optional interface method를 추가한다. +- [ ] `typescript/src/inbound_gateway.ts`: `FrameReorderBuffer` pending count accessor를 추가한다. +- [ ] `typescript/src/inbound_gateway.ts`: `WorkerGateway.stats()`를 구현한다. +- [ ] `typescript/src/node_inbound_gateway.ts`: pool stats와 wrapper stats를 구현한다. +- [ ] `typescript/src/communicator.ts`: 필요한 경우 attached gateway stats를 읽는 helper를 추가하되, public surface를 최소화한다. + +### 테스트 작성 + +- 작성한다. cleanup 계측은 stats 값 자체가 검증 대상이다. + +### 중간 검증 + +```bash +cd typescript && npm run check && npm test +``` + +예상 결과: exit code 0. + +## [GATEWAY_CLEANUP-2] Close/Cancel/Drain Tests + +### 문제 + +현재 lifecycle tests는 stale delivery 금지만 확인한다. + +```ts +688 describe("WorkerGateway lifecycle", () => { +689 test("close() 이후 pending decode result는 sink로 전달되지 않는다", async () => { +705 gateway.close(); +709 expect(delivered).toEqual([]); +``` + +close 후 jobs/reorder/pool pending이 실제로 비었는지는 검증하지 않는다. + +### 해결 방법 + +- `WorkerGateway`에 queued job이 남은 상태, in-flight decode가 pending인 상태, reorder pending이 있는 상태를 각각 만든다. +- `close()` 후 `stats()`가 zero backlog와 `closed=true`를 반환하는지 검증한다. +- Node worker gateway도 pending decode가 있는 상태에서 close 후 `workerPending=0`을 검증한다. + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/test/communicator.test.ts`: `WorkerGateway.stats()` unit tests를 추가한다. +- [ ] `typescript/test/communicator.test.ts`: Node worker gateway close stats test를 추가한다. +- [ ] `typescript/test/communicator.test.ts`: 기존 stale result tests가 새 stats contract와 함께 유지되는지 확인한다. + +### 테스트 작성 + +- 작성한다. concurrency cleanup logic이므로 regression tests가 필요하다. + +### 중간 검증 + +```bash +cd typescript && npm run check && npm test +``` + +예상 결과: exit code 0. + +## [GATEWAY_CLEANUP-3] Stress Cleanup Evidence + +### 문제 + +현재 stress row는 `gatewayBacklog`를 기록할 수 있지만 gateway profile에서는 실측값이 없다. + +```ts +89 queueBacklog: number; +90 gatewayBacklog: number; +``` + +```ts +836 emitThroughputRow("gateway", `frames=${count}`, count, elapsed, stability, gatewayOn ? "on" : "off", memMb(), { +``` + +### 해결 방법 + +- gateway profile에서 close 전/후 stats를 수집한다. +- row의 `gatewayBacklog`는 close 전 peak backlog 또는 close 후 잔여 backlog 중 더 위험한 값을 기록한다. +- close 후 잔여 backlog가 0이 아니면 `pendingLeak`을 증가시킨다. +- repeated quick run 명령을 계획 검증에 포함해 memory/backlog 누적 여부를 결과 파일로 남긴다. + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/bench/stress.ts`: gateway stats helper를 추가한다. +- [ ] `typescript/bench/stress.ts`: gateway row에 close/drain 후 `gatewayBacklog`를 반영한다. +- [ ] `typescript/bench/stress.ts`: close 후 non-zero backlog를 stability violation으로 기록한다. +- [ ] `agent-test/runs/`: focused stress 결과 파일을 생성한다. + +### 테스트 작성 + +- 별도 unit test는 `GATEWAY_CLEANUP-2`에서 작성한다. 이 항목은 stress evidence 생성이다. + +### 중간 검증 + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +``` + +예상 결과: PASS, `gatewayBacklog`가 실제 stats 기반 값으로 기록되고 close 후 leak이면 FAIL한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `typescript/src/inbound_gateway.ts` | GATEWAY_CLEANUP-1 | +| `typescript/src/node_inbound_gateway.ts` | GATEWAY_CLEANUP-1 | +| `typescript/src/communicator.ts` | GATEWAY_CLEANUP-1 | +| `typescript/test/communicator.test.ts` | GATEWAY_CLEANUP-2 | +| `typescript/bench/stress.ts` | GATEWAY_CLEANUP-3 | +| `CODE_REVIEW-cloud-G06.md` | GATEWAY_CLEANUP-3 | + +## 리뷰어를 위한 체크포인트 + +- stats API가 기존 custom `InboundGateway` 구현을 깨지 않도록 optional인지 확인한다. +- close 후 queued/reorder/worker pending이 0으로 관측되는지 확인한다. +- stale result/onError suppression이 기존 tests와 함께 유지되는지 확인한다. +- stress row의 `gatewayBacklog`가 하드코딩 0이 아니라 실제 stats 기반인지 확인한다. + +## 최종 검증 + +```bash +cd typescript && npm run check && npm test +``` + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +``` + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all +``` + +모든 명령은 exit code 0이어야 한다. Go test cache 관련 없음. stress/matrix runner가 생성한 `agent-test/runs/*.md` 경로를 review stub에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_1.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_1.log new file mode 100644 index 0000000..596c091 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_1.log @@ -0,0 +1,244 @@ + + +# Plan - REVIEW_GATEWAY_CLEANUP + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 필수 마지막 단계다. 구현 후 검증을 실행하고, 실제 stdout/stderr를 review stub에 기록한 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌이 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 남기고 중단한다. 명령 재실행이나 산출물 수집으로 해소 가능한 증거 공백은 사용자 리뷰 요청이 아니다. + +## 배경 + +1차 리뷰는 TypeScript gateway cleanup 구현과 focused verification은 통과한다고 판단했다. 다만 계획의 diagnostics contract가 public export surface에서 완전히 노출되지 않았고, `sinkPending` close/drain 경로가 실제 양수 상태로 검증되지 않았다. 이 follow-up은 두 Required issue만 닫는다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review만 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md` +- Task ids: + - `gateway-cleanup`: gateway worker close/cancel/drain 계측을 추가한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/code-review/SKILL.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-test/local/rules.md` +- `agent-test/local/typescript-smoke.md` +- `agent-test/local/proto-socket-full-matrix.md` +- `agent-roadmap/current.md` +- `agent-roadmap/milestones/high-performance-parallel-operations.md` +- `agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/complete.log` +- `agent-task/m-high-performance-parallel-operations/07+06_gateway_cleanup/plan_cloud_G06_0.log` +- `agent-task/m-high-performance-parallel-operations/07+06_gateway_cleanup/code_review_cloud_G06_0.log` +- `typescript/src/index.ts` +- `typescript/src/node.ts` +- `typescript/src/communicator.ts` +- `typescript/src/inbound_gateway.ts` +- `typescript/src/node_inbound_gateway.ts` +- `typescript/src/tcp_client.ts` +- `typescript/src/node_ws_client.ts` +- `typescript/src/tcp_server.ts` +- `typescript/src/node_ws_server.ts` +- `typescript/src/base_client.ts` +- `typescript/test/communicator.test.ts` +- `typescript/test/tcp.test.ts` +- `typescript/test/ws.test.ts` +- `typescript/bench/stress.ts` +- `typescript/package.json` +- `typescript/tsconfig.json` +- `agent-test/runs/20260603-082530-proto-socket-stress-quick.md` +- `agent-test/runs/20260603-082544-proto-socket-stress-quick.md` +- `agent-test/runs/20260603-082545-proto-socket-stress-quick.md` +- `agent-test/runs/20260603-082642-proto-socket-full-matrix.md` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`는 존재하며 읽었다. +- TypeScript 변경이므로 `agent-test/local/typescript-smoke.md`를 적용한다: `cd typescript && npm run check && npm test`. +- public TypeScript API surface에 닿으므로 type check를 반드시 포함한다. +- 1차 구현의 stress/full-matrix 결과 파일은 존재했고 PASS로 대조했다. 이 follow-up은 runtime behavior를 바꾸지 않는 export/test 보완이므로 focused stress quick만 재확인한다. +- `<확인 필요>` 값은 없었다. + +### 테스트 커버리지 공백 + +- `InboundGatewayStats`는 `typescript/src/inbound_gateway.ts`에 정의됐지만 `typescript/src/communicator.ts` re-export에서 빠져 public barrel import coverage가 없다. +- `sinkPending`은 `typescript/src/inbound_gateway.ts`에서 증가/감소하지만, `typescript/test/communicator.test.ts`는 `sinkPending > 0` 상태를 만들지 않아 close 중 stale sink skip/decrement 경로를 검증하지 않는다. + +### 심볼 참조 + +- rename/remove symbol 없음. +- `InboundGatewayStats`: `typescript/src/inbound_gateway.ts`, `typescript/src/node_inbound_gateway.ts`; public barrel은 `typescript/src/index.ts` -> `typescript/src/communicator.ts`, `typescript/src/node.ts` -> `typescript/src/communicator.ts`. +- `sinkPending`: `typescript/src/inbound_gateway.ts`; 기존 테스트 참조는 `typescript/test/communicator.test.ts`. + +### 분할 판단 + +- split policy를 평가했다. +- shared task group: `agent-task/m-high-performance-parallel-operations/`. +- 현재 subtask: `07+06_gateway_cleanup`. +- predecessor `06_gateway_real_path`는 `agent-task/archive/2026/06/m-high-performance-parallel-operations/06_gateway_real_path/complete.log`로 충족됐다. +- follow-up은 같은 subtask 안의 review loop 보정이므로 새 split을 만들지 않는다. + +### 범위 결정 근거 + +- TypeScript public re-export와 `WorkerGateway` cleanup regression test만 수정한다. +- `WorkerGateway`/Node worker gateway 구현, stress schema, TCP/WS read loop, Go/Kotlin 변경은 이번 follow-up에서 고치지 않는다. +- wire format, proto schema, gateway default policy는 변경하지 않는다. + +### 빌드 등급 + +- build lane: `cloud-G06`. concurrency diagnostics contract의 review follow-up이고 기존 lane이 적절하다. +- review lane: `cloud-G06`. API surface와 close/drain test completeness를 확인해야 한다. + +## 구현 체크리스트 + +- [ ] [REVIEW_GATEWAY_CLEANUP-1] `InboundGatewayStats`를 TypeScript public gateway export surface에서 import 가능하게 만든다. +- [ ] [REVIEW_GATEWAY_CLEANUP-2] `sinkPending > 0` 상태를 만드는 close/drain regression test를 추가한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## [REVIEW_GATEWAY_CLEANUP-1] Export Diagnostics Stats Type + +### 문제 + +1차 리뷰 Required issue: + +```text +typescript/src/communicator.ts:19-29 re-export 목록에 type InboundGatewayStats가 없다. +``` + +`typescript/src/index.ts`와 `typescript/src/node.ts`는 supported package surface를 `communicator.ts` re-export에 의존한다. 계획은 `InboundGatewayStats` type 추가를 요구했지만, 현재 사용자는 supported barrel에서 stats shape를 직접 import할 수 없다. + +### 해결 방법 + +`typescript/src/communicator.ts`의 `./inbound_gateway.js` re-export 목록에 `type InboundGatewayStats`를 추가한다. + +Before: + +```ts +19 export { +20 decodePacketBaseEnvelope, +21 type DecodedEnvelope, +22 type DecodedFrame, +23 type FrameDecoder, +24 FrameReorderBuffer, +25 type InboundFrame, +26 type InboundGateway, +27 WorkerGateway, +28 type WorkerGatewayOptions, +29 } from "./inbound_gateway.js"; +``` + +After: + +```ts +export { + decodePacketBaseEnvelope, + type DecodedEnvelope, + type DecodedFrame, + type FrameDecoder, + FrameReorderBuffer, + type InboundFrame, + type InboundGateway, + type InboundGatewayStats, + WorkerGateway, + type WorkerGatewayOptions, +} from "./inbound_gateway.js"; +``` + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/src/communicator.ts`: `type InboundGatewayStats` re-export를 추가한다. + +### 테스트 작성 + +- 별도 runtime test는 작성하지 않는다. `npm run check`가 public source import type surface를 검증한다. + +### 중간 검증 + +```bash +cd typescript && npm run check +``` + +예상 결과: exit code 0. + +## [REVIEW_GATEWAY_CLEANUP-2] Cover Sink Pending Close/Drain Path + +### 문제 + +1차 리뷰 Required issue: + +```text +typescript/test/communicator.test.ts:737-831 cleanup stats tests never make sinkPending positive. +typescript/src/inbound_gateway.ts:251 and :269 are not directly covered. +``` + +현재 테스트는 queued/reorder/worker pending을 검증하지만, `sinkPending`이 양수인 상태에서 close가 stale sink delivery를 막고 stats를 비우는지 검증하지 않는다. + +### 해결 방법 + +`typescript/test/communicator.test.ts`의 `WorkerGateway cleanup stats` describe에 delayed sink test를 추가한다. + +필수 시나리오: + +- `WorkerGateway`에 즉시 decode되는 frame 2개를 제출한다. +- 첫 sink invocation을 unresolved promise로 막아 `stats().sinkPending > 0`을 관측한다. +- 그 상태에서 `gateway.close()`를 호출한다. +- close 직후 `sinkPending`, `queued`, `reorderPending`, `workerPending`이 0이고 `closed=true`인지 확인한다. +- blocked sink를 release한 뒤 두 번째 frame이 stale delivery로 호출되지 않았는지 확인한다. + +### 수정 파일 및 체크리스트 + +- [ ] `typescript/test/communicator.test.ts`: `sinkPending` 양수 상태와 close 후 stale sink suppression을 검증하는 test를 추가한다. + +### 테스트 작성 + +- 작성한다. concurrency cleanup counter regression이므로 필수다. + +### 중간 검증 + +```bash +cd typescript && npm test +``` + +예상 결과: exit code 0, `WorkerGateway cleanup stats`에 신규 test가 포함된다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `typescript/src/communicator.ts` | REVIEW_GATEWAY_CLEANUP-1 | +| `typescript/test/communicator.test.ts` | REVIEW_GATEWAY_CLEANUP-2 | +| `CODE_REVIEW-cloud-G06.md` | 구현 결과 기록 | + +## 리뷰어를 위한 체크포인트 + +- `InboundGatewayStats`가 supported TypeScript barrel에서 import 가능한지 확인한다. +- 신규 test가 `sinkPending > 0`을 실제로 관측하는지 확인한다. +- close 후 두 번째 stale sink delivery가 발생하지 않는지 확인한다. +- 기존 gateway cleanup stress evidence가 깨지지 않는지 확인한다. + +## 최종 검증 + +```bash +cd typescript && npm run check +``` + +```bash +cd typescript && npm test +``` + +```bash +bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang typescript --transport tcp,ws --profile gateway +``` + +모든 명령은 exit code 0이어야 한다. stress runner가 생성한 `agent-test/runs/*.md` 경로를 review stub에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/go/bench/stress.go b/go/bench/stress.go index 9732bfc..427e482 100644 --- a/go/bench/stress.go +++ b/go/bench/stress.go @@ -668,15 +668,6 @@ func profilePayload(transport string, mode string) { expectedEcho := "echo:" + filler payloadBytes := testDataPayloadBytes(filler) axis := "payload=" + payloadLabel(size) - // Go WsClient/WsServer는 nhooyr 기본 read limit(32KiB)을 그대로 쓰므로 ~32KB를 넘는 WS frame은 - // read cap을 초과해 끊긴다. WS read limit을 올리는 것은 optimize Epic의 transport 변경이라 여기서는 - // 측정 가능한 TCP와 작은 WS payload만 기록하고 큰 WS payload는 deferred로 남긴다. - if transport == transWS && payloadBytes >= 32768 { - emitSkip("payload", axis, transport, - "deferred: optimize Epic — Go WsClient/WsServer keep the nhooyr default 32KiB read limit, so WS payloads above ~32KB exceed the frame read cap") - logf("[payload] transport=%s size=%s bytes=%d deferred (WS 32KiB read limit)", transport, payloadLabel(size), payloadBytes) - continue - } peak := memMb() ctx, cancel := context.WithCancel(context.Background()) srv, err := startRequestServer(ctx, transport, s) diff --git a/go/ws_client.go b/go/ws_client.go index 90d9f82..801feb1 100644 --- a/go/ws_client.go +++ b/go/ws_client.go @@ -29,6 +29,7 @@ type WsClient struct { } func NewWsClient(conn *websocket.Conn, intervalSec, waitSec int, parserMap ParserMap) *WsClient { + conn.SetReadLimit(MaxPacketSize) readCtx, cancelRead := context.WithCancel(context.Background()) writeCtx, cancelWrite := context.WithCancel(context.Background()) c := &WsClient{ diff --git a/kotlin/crosstest/stress.kt b/kotlin/crosstest/stress.kt index c2a3684..eb72dc2 100644 --- a/kotlin/crosstest/stress.kt +++ b/kotlin/crosstest/stress.kt @@ -26,6 +26,7 @@ import com.tokilabs.proto_socket.packets.TestData import com.tokilabs.proto_socket.typeNameOf import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -64,10 +65,27 @@ private class StressStability { timeouts.get() + nonceMismatch.get() + typeMismatch.get() + fifoViolations.get() + pendingLeak.get() } -private class StressServerHandle(val port: Int, val stop: () -> Unit) +private class StressServerHandle( + val port: Int, + val stop: () -> Unit, + private val connected: Channel, +) { + suspend fun waitForConnections(count: Int = 1) { + repeat(count) { + if (withTimeoutOrNull(3_000L) { connected.receive() } == null) { + throw IllegalStateException("server did not finish client setup") + } + } + } +} private class StressClientHandle(val comm: Communicator, val close: () -> Unit) +private suspend fun stopServer(handle: StressServerHandle) { + handle.stop() + delay(50) +} + private fun parserMap(): ParserMap = mapOf(typeNameOf() to { bytes: ByteArray -> TestData.parseFrom(bytes) }) private fun log(line: String) = System.err.println(line) @@ -162,22 +180,10 @@ private fun emitSkip(profile: String, axis: String, transport: String, reason: S println("SKIP|$profile|$axis|$LANGUAGE|$transport|$reason") } -// 고성능 병렬 운용 기준선 Milestone에서 발견되어 optimize Epic(hotspot-report/targeted-opt)으로 이연된 -// Kotlin 한계. lang-baseline은 이 축을 PASS로 기록하지 않고 사유와 함께 SKIP(BLOCKED/finding)으로 남기며, -// 측정 가능한 다른 축은 그대로 stability hard gate로 측정한다. 라이브러리 수정은 이 Task 범위가 아니다. +@Suppress("UNUSED_PARAMETER") private fun deferredBottleneck(transport: String, profile: String, concurrency: Int?): String? { - if (transport == "tcp") { - // Kotlin TcpClient는 TCP_NODELAY를 설정하지 않아 loopback request/response가 delayed-ACK(≈40ms)에 - // 묶인다. 중간 concurrency에서 head-of-line stall이 누적돼 throughput이 무너지고 간헐적으로 stability - // timeout을 넘긴다(비결정적). burst도 fire-and-forget가 latency-bound다. 측정 자체는 의미가 있으나 - // stability hard gate를 안정적으로 통과하지 못하므로 lang-baseline에서는 deferred로 기록한다. - return "deferred: optimize Epic — Kotlin TcpClient without TCP_NODELAY is delayed-ACK latency-bound and intermittently exceeds the stability timeout under request/response concurrency" - } - if (transport == "ws" && (profile == "burst" || profile == "slow-mix")) { - return "deferred: optimize Epic — WS inbound launch-per-frame on Dispatchers.IO breaks per-connection FIFO under burst or mixed slow handlers" - } - if (transport == "ws" && concurrency != null && concurrency > 64) { - return "deferred: optimize Epic — WS inbound staging cap (64) disconnects at concurrency>64" + if (profile == "slow-mix") { + return "deferred: optimize Epic — Kotlin slow-mix fires same-client requests concurrently, so response completion order is coroutine-scheduling dependent" } return null } @@ -216,18 +222,25 @@ private fun emitThroughput( } private fun startServer(transport: String, port: Int, onConnected: (Communicator) -> Unit): StressServerHandle { + val connected = Channel(Channel.UNLIMITED) when (transport) { "tcp" -> { val srv = TcpServer(HOST, port) { socket -> TcpClient.fromSocket(socket, 0, 0, parserMap()) } - srv.onClientConnected = { client -> onConnected(client.communicator) } + srv.onClientConnected = { client -> + onConnected(client.communicator) + connected.trySend(Unit) + } srv.start() - return StressServerHandle(srv.port()) { srv.stop() } + return StressServerHandle(srv.port(), { srv.stop() }, connected) } "ws" -> { val srv = WsServer(HOST, port, WS_PATH) { conn -> WsClient.forServer(conn, 0, 0, parserMap()) } - srv.onClientConnected = { client -> onConnected(client.communicator) } + srv.onClientConnected = { client -> + onConnected(client.communicator) + connected.trySend(Unit) + } srv.start() - return StressServerHandle(srv.port()) { srv.stop() } + return StressServerHandle(srv.port(), { srv.stop() }, connected) } else -> throw IllegalArgumentException("unknown transport $transport") } @@ -376,6 +389,7 @@ private suspend fun profileRoundtrip(transport: String, mode: String) { val srv = startEchoServer(transport, freePort()) try { val handle = dialWithRetry(transport, srv.port) + srv.waitForConnections() try { val started = System.nanoTime() val latencies = runRequestLoad(handle.comm, total, concurrency, timeoutMs, s) @@ -389,7 +403,7 @@ private suspend fun profileRoundtrip(transport: String, mode: String) { log("[roundtrip] concurrency=$concurrency error=$error") summarize("roundtrip", "concurrency=$concurrency", transport, emptyList(), 0.0, 1, s, memMb()) } finally { - srv.stop() + stopServer(srv) } log("[roundtrip] transport=$transport concurrency=$concurrency done violations=${s.violations()}") } @@ -418,6 +432,7 @@ private suspend fun profileBurst(transport: String, mode: String) { var handle: StressClientHandle? = null try { handle = dialWithRetry(transport, srv.port) + srv.waitForConnections() val started = System.nanoTime() for (i in 0 until count) { handle.comm.send(TestData.newBuilder().setIndex(i).setMessage("b-$i").build()) @@ -439,7 +454,7 @@ private suspend fun profileBurst(transport: String, mode: String) { it.close() if (it.comm.isAlive()) s.pendingLeak.incrementAndGet() } - srv.stop() + stopServer(srv) } log("[burst] transport=$transport count=$count received=${received.get()} violations=${s.violations()}") } @@ -461,6 +476,7 @@ private suspend fun profileSustained(transport: String, mode: String) { val srv = startEchoServer(transport, freePort()) try { val handle = dialWithRetry(transport, srv.port) + srv.waitForConnections() try { val latencies = ConcurrentLinkedQueue() var nextIndex = 0 @@ -485,7 +501,7 @@ private suspend fun profileSustained(transport: String, mode: String) { log("[sustained] duration=${durationMs}ms error=$error") summarize("sustained", "duration=${durationMs}ms", transport, emptyList(), 0.0, 1, s, peak) } finally { - srv.stop() + stopServer(srv) } log("[sustained] transport=$transport duration=${durationMs}ms done peakMemMb=${String.format("%.1f", peak)} violations=${s.violations()}") } @@ -515,6 +531,7 @@ private suspend fun profileParallel(transport: String, mode: String) { log("[parallel] clients=$clientCount dial error=$error") } } + srv.waitForConnections(handles.size) val allLatencies = ConcurrentLinkedQueue() val started = System.nanoTime() coroutineScope { @@ -530,7 +547,7 @@ private suspend fun profileParallel(transport: String, mode: String) { handle.close() if (handle.comm.isAlive()) s.pendingLeak.incrementAndGet() } - srv.stop() + stopServer(srv) } log("[parallel] transport=$transport clients=$clientCount done violations=${s.violations()}") } @@ -565,6 +582,7 @@ private suspend fun profileSlowMix(transport: String, mode: String) { log("[slow-mix] client=$clientId dial error=$error") } } + srv.waitForConnections(handles.size) val completedByClient = Array(clientCount) { AtomicInteger(-1) } val started = System.nanoTime() coroutineScope { @@ -584,7 +602,7 @@ private suspend fun profileSlowMix(transport: String, mode: String) { handle.close() if (handle.comm.isAlive()) s.pendingLeak.incrementAndGet() } - srv.stop() + stopServer(srv) } val sorted = latencies.toList().sorted() val throughput = if (elapsedMs > 0) latencies.size / elapsedMs * 1000.0 else 0.0 @@ -619,6 +637,7 @@ private suspend fun profilePayload(transport: String, mode: String) { val srv = startEchoServer(transport, freePort()) try { val handle = dialWithRetry(transport, srv.port) + srv.waitForConnections() try { val latencies = ConcurrentLinkedQueue() var issued = 0 @@ -654,7 +673,7 @@ private suspend fun profilePayload(transport: String, mode: String) { log("[payload] size=${payloadLabel(size)} error=$error") emitRow("payload", axis, transport, payloadBytes, 1, 0, 0.0, 0.0, 0.0, 0.0, peak, s) } finally { - srv.stop() + stopServer(srv) } log("[payload] transport=$transport size=${payloadLabel(size)} bytes=$payloadBytes peakMemMb=${String.format("%.1f", peak)} violations=${s.violations()}") } diff --git a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/Communicator.kt b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/Communicator.kt index 5e663ca..eccfc8a 100644 --- a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/Communicator.kt +++ b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/Communicator.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.selects.select import kotlinx.coroutines.withTimeout import java.util.concurrent.ConcurrentHashMap @@ -185,7 +186,7 @@ class Communicator( } return try { val base = PacketBase.parseFrom(raw) - onReceivedData(base.typeName, base.data.toByteArray(), base.nonce, base.responseNonce) + enqueueInbound(base.typeName, base.data.toByteArray(), base.nonce, base.responseNonce) null } catch (e: Throwable) { e @@ -344,13 +345,12 @@ class Communicator( } if (!isAlive() || isClosing.get()) return val item = InboundItem(typeName, data, incomingNonce, responseNonce) - val result = inboundQueue.trySend(item) - if (result.isFailure) { - scope.launch { - runCatching { - inboundQueue.send(item) - } + try { + runBlocking { + inboundQueue.send(item) } + } catch (e: ClosedSendChannelException) { + // closed } } 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 165cba5..2dc5f11 100644 --- a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt +++ b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/TcpClient.kt @@ -33,6 +33,7 @@ class TcpClient private constructor( private val writeMutex = Mutex() init { + socket.tcpNoDelay = true communicator.setWriteErrorHandler { onDisconnected() } communicator.setFrameErrorHandler { onDisconnected() } communicator.addListener(typeNameOf()) { onHeartBeat() } 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 87929e1..ef746ff 100644 --- a/kotlin/src/main/kotlin/com/tokilabs/proto_socket/WsClient.kt +++ b/kotlin/src/main/kotlin/com/tokilabs/proto_socket/WsClient.kt @@ -2,9 +2,9 @@ package com.tokilabs.proto_socket import com.tokilabs.proto_socket.packets.HeartBeat import com.tokilabs.proto_socket.packets.PacketBase +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.withLock import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -38,13 +38,13 @@ class WsClient internal constructor( get() = this override val communicator: Communicator = Communicator(this, parserMap, scope) - private val receiveMutex = kotlinx.coroutines.sync.Mutex() - private val stagingCount = java.util.concurrent.atomic.AtomicInteger(0) + private val receiveQueue = Channel(capacity = maxStagedFrames) init { communicator.setWriteErrorHandler { onDisconnected() } communicator.setFrameErrorHandler { onDisconnected() } communicator.addListener(typeNameOf()) { onHeartBeat() } + receiveLoop() sendHeartBeat() } @@ -56,25 +56,24 @@ class WsClient internal constructor( } internal fun receiveBytes(bytes: ByteArray) { - if (stagingCount.get() >= maxStagedFrames) { + if (!communicator.isAlive()) return + val result = receiveQueue.trySend(bytes) + if (result.isFailure) { onDisconnected() - return } - stagingCount.incrementAndGet() + } + + private fun receiveLoop() { scope.launch { - try { - receiveMutex.withLock { - // raw frame을 communicator ingress로 넘긴다. gateway가 attach되어 있으면 worker pool이 - // decode/reorder하고, 없으면 inline decode한다. inline decode 오류는 반환값으로, - // gateway decode 오류는 frame error handler로 동일한 disconnect semantics를 적용한다. - if (communicator.onReceivedFrame(bytes) != null) { - onDisconnected() - } else { - sendHeartBeat() - } + for (bytes in receiveQueue) { + // raw frame을 communicator ingress로 넘긴다. gateway가 attach되어 있으면 worker pool이 + // decode/reorder하고, 없으면 inline decode한다. inline decode 오류는 반환값으로, + // gateway decode 오류는 frame error handler로 동일한 disconnect semantics를 적용한다. + if (communicator.onReceivedFrame(bytes) != null) { + onDisconnected() + return@launch } - } finally { - stagingCount.decrementAndGet() + sendHeartBeat() } } } diff --git a/typescript/bench/stress.ts b/typescript/bench/stress.ts index a41a64c..aab5f12 100644 --- a/typescript/bench/stress.ts +++ b/typescript/bench/stress.ts @@ -15,10 +15,10 @@ * - sustained : 일정 시간 지속 request-response, throughput/p95/p99/peak memory. * - parallel : 다중 connection 동시 request-response, connection별 FIFO/nonce 독립성. * - slow-mix : 빠른 handler와 느린 handler가 섞인 다중 connection FIFO/nonce 독립성. - * - gateway : communicator `onReceivedFrame` opt-in 경로에서 worker_threads gateway(on) vs - * inline decode fallback(off) 비교 baseline. TcpClient는 inline decode로 enqueue하므로 - * gateway 효과는 이 frame-ingest 경로에서만 켤 수 있고, 작은 payload에서는 이득이 - * 불명확하여 inline fallback을 안전 기본값으로 본다. + * - gateway : worker_threads gateway(on) vs inline decode fallback(off) 비교 baseline. + * frame-ingest in-process row(`onReceivedFrame` 직접 주입)에 더해, 실제 TCP/WS + * read loop를 통과하는 transport row(`tcp`/`ws`)를 server inbound gateway on/off로 + * 측정한다. 작은 payload에서는 이득이 불명확하여 inline fallback을 안전 기본값으로 본다. */ import * as net from "node:net"; @@ -31,6 +31,7 @@ import { type ParserMap, type Transport, } from "../src/communicator.js"; +import type { InboundGateway } from "../src/inbound_gateway.js"; import { createNodeWorkerGateway } from "../src/node_inbound_gateway.js"; import { create, @@ -121,6 +122,18 @@ function transportName(transport: WireTransport): string { return transport === "ws" ? TRANSPORT_WS : TRANSPORT_TCP; } +/** + * gateway cleanup 계측: queued/reorder/sink/worker pending 합을 backlog로 본다. in-flight decode loop가 + * close 후 잠시 drain되는 `active`는 leak 판정 대상이 아니므로 제외한다. stats 미구현 gateway는 0이다. + */ +function gatewayBacklogOf(gateway: InboundGateway): number { + const s = gateway.stats?.(); + if (s === undefined) { + return 0; + } + return s.queued + s.reorderPending + s.sinkPending + s.workerPending; +} + function newStability(): Stability { return { timeouts: 0, nonceMismatch: 0, typeMismatch: 0, fifoViolations: 0, pendingLeak: 0 }; } @@ -766,11 +779,124 @@ const NOOP_TRANSPORT: Transport = { }; /** - * gateway on/off baseline. communicator의 `onReceivedFrame` opt-in 경로에 raw PacketBase frame을 흘려 - * dispatch 순서/throughput을 비교한다. off=inline decode, on=worker_threads decode pool. - * 작은 payload의 in-process 측정이므로 성능 이득이 불명확할 수 있고, 안정성(순서/leak) 0 위반만 합격선으로 둔다. + * 실제 TCP/WS 수신 경로의 gateway on/off baseline 1행을 측정한다. server inbound path에 worker_threads + * gateway를 attach(on)하거나 inline decode(off)로 두고, 동일 concurrency/payload의 request-response를 + * 흘려 throughput/p95/p99/peak memory를 기록한다. frame-ingest in-process 경로와 달리 실제 transport + * read loop(`onReceivedFrame`)를 통과한다. gateway backlog는 cleanup 계측 전까지 0으로 둔다. */ -async function profileGateway(mode: Mode): Promise { +async function runGatewayTransportBaseline( + transport: WireTransport, + mode: Mode, + gatewayOn: boolean, +): Promise { + const concurrency = 16; + const batches = mode === "quick" ? 4 : 40; + const total = concurrency * batches; + const timeoutMs = mode === "quick" ? 8_000 : 20_000; + const stability = newStability(); + + const server = makeServer(transport); + let lastNonce = 0; + // server inbound path에 attach된 gateway를 잡아 부하 중 peak backlog와 close 후 잔여 backlog를 계측한다. + let serverGateway: InboundGateway | null = null; + server.onClientConnected = (client: { communicator: Communicator }) => { + const comm = client.communicator; + if (gatewayOn) { + // 효과 해석을 흐리지 않도록 server inbound path 한쪽만 gateway를 켠다. + const gw = createNodeWorkerGateway({ + sink: (frame) => + comm.enqueueInbound(frame.typeName, frame.data, frame.incomingNonce, frame.responseNonce), + onError: () => { + stability.typeMismatch += 1; + }, + workers: Math.max(2, Math.min(4, availableParallelism())), + }); + comm.attachInboundGateway(gw); + serverGateway = gw; + } + // raw addRequestListener로 nonce를 받아 gateway reorder 이후에도 connection별 FIFO(단조 증가)를 감시한다. + comm.addRequestListener(TestDataSchema.typeName, (msg, nonce) => { + if (nonce < lastNonce) { + stability.fifoViolations += 1; + } + lastNonce = nonce; + const data = msg as TestData; + return create(TestDataSchema, { index: data.index * 2, message: `echo:${data.message}` }); + }); + }; + await server.start(); + + let latencies: number[] = []; + let elapsed = 0; + let peakBacklog = 0; + // 부하 중 gateway backlog peak을 샘플링한다. gateway off면 sampler 없이 backlog는 0으로 남는다. + const sampler = + gatewayOn + ? setInterval(() => { + if (serverGateway !== null) { + peakBacklog = Math.max(peakBacklog, gatewayBacklogOf(serverGateway)); + } + }, 2) + : undefined; + try { + const client = await connectClient(transport, server.port); + try { + const started = performance.now(); + latencies = await runRequestLoad(client, total, concurrency, timeoutMs, stability); + elapsed = performance.now() - started; + } finally { + await client.close(); + if (!leakClear(client)) { + stability.pendingLeak += 1; + } + } + } finally { + if (sampler !== undefined) { + clearInterval(sampler); + } + await server.stop(); + } + + // server.stop()이 communicator/gateway를 close한다. close 후 잔여 backlog가 0이 아니면 cleanup leak이다. + let residualBacklog = 0; + if (gatewayOn && serverGateway !== null) { + residualBacklog = gatewayBacklogOf(serverGateway); + if (residualBacklog > 0) { + stability.pendingLeak += 1; + } + } + const gatewayBacklog = Math.max(peakBacklog, residualBacklog); + + summarizeLatencies( + "gateway", + `transport=${transport} concurrency=${concurrency}`, + latencies, + elapsed, + stability, + gatewayOn ? "on" : "off", + memMb(), + { + transport: transportName(transport), + payloadBytes: testDataPayloadBytes("req-0"), + clientCount: 1, + gatewayBacklog, + }, + ); + log( + `[gateway] transport=${transport} on=${gatewayOn} requests=${total} ` + + `backlog=${gatewayBacklog} residual=${residualBacklog} violations=${stabilityViolations(stability)}`, + ); +} + +/** + * gateway on/off baseline. 두 축을 측정한다: + * 1) frame-ingest: communicator `onReceivedFrame` in-process 경로에 raw PacketBase frame을 흘려 + * off=inline decode vs on=worker_threads decode pool의 dispatch 순서/throughput을 비교한다. + * 2) tcp/ws: 실제 transport read loop를 통과하는 request-response baseline으로, server inbound path의 + * gateway on/off별 throughput/p95/p99/memory를 기록한다. + * 작은 payload의 측정이므로 성능 이득이 불명확할 수 있고, 안정성(순서/leak) 0 위반만 합격선으로 둔다. + */ +async function profileGateway(mode: Mode, transports: readonly WireTransport[]): Promise { const count = mode === "quick" ? 500 : 5_000; log(`[gateway] mode=${mode} frames=${count}`); @@ -811,8 +937,9 @@ async function profileGateway(mode: Mode): Promise { } }); + let gateway: InboundGateway | null = null; if (gatewayOn) { - const gateway = createNodeWorkerGateway({ + gateway = createNodeWorkerGateway({ sink: (frame) => comm.enqueueInbound(frame.typeName, frame.data, frame.incomingNonce, frame.responseNonce), onError: () => { @@ -824,41 +951,78 @@ async function profileGateway(mode: Mode): Promise { } const started = performance.now(); + let elapsed = 0; + let peakBacklog = 0; + let dispatchError: unknown = null; + // ingest 중 gateway backlog peak을 샘플링한다. gateway off면 backlog는 0으로 남는다. + const sampler = + gatewayOn + ? setInterval(() => { + if (gateway !== null) { + peakBacklog = Math.max(peakBacklog, gatewayBacklogOf(gateway)); + } + }, 2) + : undefined; try { for (const raw of frames) { comm.onReceivedFrame(raw); } await withTimeout(done, mode === "quick" ? 15_000 : 60_000, `gateway dispatch timeout`); - const elapsed = performance.now() - started; + elapsed = performance.now() - started; if (dispatched !== count) { stability.pendingLeak += 1; } - emitThroughputRow("gateway", `frames=${count}`, count, elapsed, stability, gatewayOn ? "on" : "off", memMb(), { + } catch (err) { + dispatchError = err; + elapsed = performance.now() - started; + stability.timeouts += 1; + log(`[gateway] on=${gatewayOn} error=${errorMessage(err)}`); + } finally { + if (sampler !== undefined) { + clearInterval(sampler); + } + if (gateway !== null) { + peakBacklog = Math.max(peakBacklog, gatewayBacklogOf(gateway)); + } + await comm.close(); + } + + // comm.close()가 gateway를 close한다. close 후 잔여 backlog가 0이 아니면 cleanup leak이다. + let residualBacklog = 0; + if (gateway !== null) { + residualBacklog = gatewayBacklogOf(gateway); + if (residualBacklog > 0) { + stability.pendingLeak += 1; + } + } + const gatewayBacklog = Math.max(peakBacklog, residualBacklog); + + emitThroughputRow( + "gateway", + `frames=${count}`, + dispatchError !== null ? dispatched : count, + elapsed, + stability, + gatewayOn ? "on" : "off", + memMb(), + { transport: TRANSPORT_FRAME_INGEST, payloadBytes: framePayloadBytes, clientCount: 0, - }); - } catch (err) { - stability.timeouts += 1; - log(`[gateway] on=${gatewayOn} error=${errorMessage(err)}`); - emitThroughputRow( - "gateway", - `frames=${count}`, - dispatched, - performance.now() - started, - stability, - gatewayOn ? "on" : "off", - memMb(), - { - transport: TRANSPORT_FRAME_INGEST, - payloadBytes: framePayloadBytes, - clientCount: 0, - }, - ); - } finally { - await comm.close(); + gatewayBacklog, + }, + ); + log( + `[gateway] on=${gatewayOn} dispatched=${dispatched} backlog=${gatewayBacklog} ` + + `residual=${residualBacklog} violations=${stabilityViolations(stability)}`, + ); + } + + // 실제 transport 수신 경로의 gateway on/off rows. 선택된 transport마다 off, on을 동일 조건으로 측정한다. + for (const transport of transports) { + for (const gatewayOn of [false, true]) { + await runGatewayTransportBaseline(transport, mode, gatewayOn); } - log(`[gateway] on=${gatewayOn} dispatched=${dispatched} violations=${stabilityViolations(stability)}`); } } @@ -942,7 +1106,7 @@ async function main(): Promise { } } if (profiles.includes("gateway")) { - await profileGateway(mode); + await profileGateway(mode, transports); } const totalViolations = rows.reduce((acc, row) => acc + stabilityViolations(row.stability), 0); diff --git a/typescript/src/base_client.ts b/typescript/src/base_client.ts index 2396de5..4ad9a0a 100644 --- a/typescript/src/base_client.ts +++ b/typescript/src/base_client.ts @@ -26,6 +26,11 @@ export abstract class BaseClient implements Transport { this.communicator.setWriteErrorHandler(() => { void this.onDisconnected(); }); + // transport read loop가 onReceivedFrame으로 흘리는 raw frame의 decode 실패(inline fallback 또는 + // attach된 gateway 모두)를 write-error와 동일하게 disconnect로 처리한다. + this.communicator.setFrameErrorHandler(() => { + void this.onDisconnected(); + }); addListenerTyped(this.communicator, HeartBeatSchema, () => { void this.onHeartbeat(); }); diff --git a/typescript/src/communicator.ts b/typescript/src/communicator.ts index 8a2335b..7733afb 100644 --- a/typescript/src/communicator.ts +++ b/typescript/src/communicator.ts @@ -24,6 +24,7 @@ export { FrameReorderBuffer, type InboundFrame, type InboundGateway, + type InboundGatewayStats, WorkerGateway, type WorkerGatewayOptions, } from "./inbound_gateway.js"; @@ -216,7 +217,7 @@ export class Communicator { * * gateway가 없으면 호출 컨텍스트에서 inline decode 후 {@link enqueueInbound}로 전달한다(safe fallback). */ - onReceivedFrame(raw: Uint8Array): void { + onReceivedFrame(raw: Uint8Array): void | Promise { if (!this.isAliveFlag || this.isClosing) { return; } @@ -233,7 +234,10 @@ export class Communicator { this.frameErrorHandler?.(toError(err)); return; } - void this.enqueueInbound( + // inline fallback에서는 enqueue Promise를 그대로 돌려준다. transport read loop가 이를 await해 + // 기존 inline decode 경로와 동일한 inbound queue backpressure(pause/resume)를 유지한다. gateway가 + // attach된 경로는 위에서 submit 후 즉시 반환되므로 decode가 read loop를 막지 않는다. + return this.enqueueInbound( envelope.typeName, envelope.data, envelope.incomingNonce, diff --git a/typescript/src/inbound_gateway.ts b/typescript/src/inbound_gateway.ts index a354678..0162989 100644 --- a/typescript/src/inbound_gateway.ts +++ b/typescript/src/inbound_gateway.ts @@ -34,11 +34,39 @@ export interface DecodedFrame extends DecodedEnvelope { * gateway는 stateful dispatch, `pendingRequests`, listener, write queue를 소유하지 않는다. decode * 결과는 입력 `seq` 순서로 sink에 전달되어 coordinator가 결정적으로 dispatch할 수 있게 한다. */ +/** + * gateway cleanup/drain 진단용 backlog 스냅샷이다. close/cancel/drain이 실제로 자원을 비웠는지, + * repeated run에서 backlog가 누적되지 않는지 숫자로 관측하기 위한 계측 계약이다. + * + * `queued`/`reorderPending`/`sinkPending`/`workerPending`는 close 후 모두 0이어야 하며, 0이 아니면 + * cleanup leak이다. `active`는 close 시점에 await 중이던 in-flight decode loop가 drain되는 동안 잠시 + * 남을 수 있으므로 backlog leak 판정에는 포함하지 않는다. + */ +export interface InboundGatewayStats { + /** sink 전달 대기 중인(아직 decode loop가 집어가지 않은) 큐 길이다. */ + queued: number; + /** 현재 in-flight인 decode loop 수다(최대 `workers`). */ + active: number; + /** reorder 버퍼가 선행 `seq`를 기다리며 보류 중인 결과 수다. */ + reorderPending: number; + /** sink chain에 스케줄됐지만 아직 전달되지 않은 결과 수다. */ + sinkPending: number; + /** worker pool(off-thread decode)에 제출됐지만 아직 응답이 오지 않은 decode 수다. */ + workerPending: number; + /** close()가 호출되어 추가 전달이 멈춘 상태인지 여부다. */ + closed: boolean; +} + export interface InboundGateway { /** raw 프레임을 gateway에 제출한다. fire-and-forget이며 decode 결과는 이후 sink에 `seq` 순서로 전달된다. */ submit(frame: InboundFrame): void; /** gateway 자원을 해제하고 추가 전달을 멈춘다. */ close(): void; + /** + * 현재 backlog/close 상태 스냅샷을 반환한다. cleanup/drain 계측용이며, 기존 custom gateway 구현과의 + * 호환을 위해 optional이다. 미구현 gateway는 진단 backlog를 0으로 간주한다. + */ + stats?(): InboundGatewayStats; } /** raw 프레임을 PacketBase envelope으로 decode하는 순수 함수다. 공유 상태가 없어 병렬 실행에 안전하다. */ @@ -90,6 +118,11 @@ export class FrameReorderBuffer { return ready; } + /** 선행 `seq`를 기다리며 보류 중인 결과 수다. cleanup 계측에서 reorder backlog로 관측한다. */ + pendingCount(): number { + return this.pending.size; + } + /** 버퍼된 결과를 비운다. gateway close 시 stale result가 contiguous release를 유발하지 않게 한다. */ clear(): void { this.pending.clear(); @@ -129,6 +162,8 @@ export class WorkerGateway implements InboundGateway { private activeWorkers = 0; private closed = false; private sinkChain: Promise = Promise.resolve(); + // sink chain에 스케줄됐지만 아직 전달되지 않은 결과 수. cleanup 계측의 sinkPending으로 관측한다. + private sinkPending = 0; constructor(options: WorkerGatewayOptions) { this.decode = options.decode ?? decodePacketBaseEnvelope; @@ -159,6 +194,23 @@ export class WorkerGateway implements InboundGateway { this.closed = true; this.jobs.length = 0; this.reorder.clear(); + // 스케줄된 sink 전달을 폐기하므로 backlog 계측도 즉시 0으로 본다. 이미 큐된 sinkChain 콜백은 + // closed guard로 전달을 건너뛰며, 아래 finally의 가드가 카운터를 음수로 떨어뜨리지 않는다. + this.sinkPending = 0; + } + + /** 현재 backlog/close 상태 스냅샷을 반환한다(cleanup/drain 계측용). */ + stats(): InboundGatewayStats { + return { + queued: this.jobs.length, + active: this.activeWorkers, + reorderPending: this.reorder.pendingCount(), + sinkPending: this.sinkPending, + // 기본 WorkerGateway는 별도 off-thread worker pool을 두지 않는다. Node worker gateway wrapper가 + // pool pending을 합성해 채운다. + workerPending: 0, + closed: this.closed, + }; } private async workerLoop(): Promise { @@ -196,19 +248,28 @@ export class WorkerGateway implements InboundGateway { } // reorder는 contiguous prefix만 release하므로 batch들은 호출 순서가 곧 seq 순서다. sinkChain으로 // 직렬화해 async sink가 입력 순서를 깨지 않게 한다. + this.sinkPending += ready.length; this.sinkChain = this.sinkChain.then(async () => { for (const r of ready) { - // 스케줄 이후 close되었으면 남은 전달을 중단한다. - if (this.closed) { - return; - } - if (r.error !== null) { - // decode 오류를 seq 순서로 보고하고 계속 진행한다. transport 종료 여부는 onError가 결정한다. - this.onError?.(r.error); - continue; - } - if (r.frame !== null) { - await this.sink(r.frame); + try { + // 스케줄 이후 close되었으면 남은 전달을 건너뛴다(stale delivery 금지). 단 카운터는 finally에서 + // 정리해야 하므로 return이 아니라 continue로 남은 항목을 소비한다. + if (this.closed) { + continue; + } + if (r.error !== null) { + // decode 오류를 seq 순서로 보고하고 계속 진행한다. transport 종료 여부는 onError가 결정한다. + this.onError?.(r.error); + continue; + } + if (r.frame !== null) { + await this.sink(r.frame); + } + } finally { + // close()가 sinkPending을 0으로 리셋했을 수 있으므로 음수로 떨어뜨리지 않는다. + if (this.sinkPending > 0) { + this.sinkPending -= 1; + } } } }); diff --git a/typescript/src/node_inbound_gateway.ts b/typescript/src/node_inbound_gateway.ts index 0afa2f0..406fefb 100644 --- a/typescript/src/node_inbound_gateway.ts +++ b/typescript/src/node_inbound_gateway.ts @@ -7,6 +7,7 @@ import { type FrameDecoder, type InboundFrame, type InboundGateway, + type InboundGatewayStats, WorkerGateway, } from "./inbound_gateway.js"; @@ -72,6 +73,11 @@ class WorkerThreadDecodePool { } } + /** worker에 제출됐지만 아직 응답이 오지 않은 decode 수다. cleanup 계측의 workerPending으로 관측한다. */ + pendingCount(): number { + return this.pending.size; + } + private onReply(reply: WorkerReply): void { const pending = this.pending.get(reply.id); if (pending === undefined) { @@ -108,7 +114,12 @@ function detachableBuffer(bytes: Uint8Array): ArrayBuffer { ) { return bytes.buffer; } - return bytes.slice().buffer as ArrayBuffer; + // bytes가 더 큰(공유/pool) buffer의 view이면 해당 구간만 owned buffer로 복사한다. Node Buffer의 slice()는 + // 복사가 아니라 subarray view라 .buffer가 원본 전체 buffer를 가리키므로, Uint8Array 복사로 명시적으로 + // 정확한 구간만 떼어낸다(transport read loop가 넘기는 Buffer subarray frame을 안전하게 transfer하기 위함). + const copy = new Uint8Array(bytes.byteLength); + copy.set(bytes); + return copy.buffer; } /** @@ -164,5 +175,10 @@ export function createNodeWorkerGateway(options: { gateway.close(); pool.close(); }, + stats(): InboundGatewayStats { + // reorder/sink/queue/active backlog는 WorkerGateway가 소유하고, off-thread decode pending은 pool이 + // 소유하므로 둘을 합성한다. close 후에는 pool.failAll로 pending이 비고 workerPending=0이 된다. + return { ...gateway.stats(), workerPending: pool.pendingCount() }; + }, }; } diff --git a/typescript/src/node_ws_client.ts b/typescript/src/node_ws_client.ts index 5e31207..5dda309 100644 --- a/typescript/src/node_ws_client.ts +++ b/typescript/src/node_ws_client.ts @@ -6,7 +6,7 @@ import WebSocketClient, { type ClientOptions as WebSocketClientOptions } from "w import { BaseClient } from "./base_client.js"; import { type ParserMap } from "./communicator.js"; -import { fromBinary, PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js"; +import { PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js"; const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; const MAX_WS_PAYLOAD_SIZE = 64 << 20; @@ -311,13 +311,11 @@ export class NodeWsClient extends BaseClient { } private async onMessage(data: Uint8Array): Promise { - try { - const base = fromBinary(PacketBaseSchema, data); - await this.communicator.enqueueInbound(base.typeName, base.data, base.nonce, base.responseNonce); - void this.sendHeartbeat(); - } catch { - void this.onDisconnected(); - } + // raw PacketBase binary message를 communicator로 넘긴다. gateway 미설정 시 inline decode + enqueue를 + // await해 기존 backpressure를 유지하고, gateway 설정 시 off-thread decode 경로로 제출된다. decode + // 실패는 setFrameErrorHandler(initBase)에서 onDisconnected로 처리한다. + await this.communicator.onReceivedFrame(data); + void this.sendHeartbeat(); } } diff --git a/typescript/src/tcp_client.ts b/typescript/src/tcp_client.ts index bf7d430..81335a2 100644 --- a/typescript/src/tcp_client.ts +++ b/typescript/src/tcp_client.ts @@ -3,7 +3,7 @@ import * as tls from "node:tls"; import { BaseClient } from "./base_client.js"; import { type ParserMap } from "./communicator.js"; -import { fromBinary, PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js"; +import { PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js"; export const MAX_PACKET_SIZE = 64 << 20; @@ -80,14 +80,11 @@ export class TcpClient extends BaseClient { const packetBytes = this.readBuf.subarray(4, 4 + length); this.readBuf = this.readBuf.subarray(4 + length); - try { - const base = fromBinary(PacketBaseSchema, packetBytes); - await this.communicator.enqueueInbound(base.typeName, base.data, base.nonce, base.responseNonce); - void this.sendHeartbeat(); - } catch { - void this.onDisconnected(); - return; - } + // raw PacketBase frame을 communicator로 넘긴다. gateway 미설정 시 inline decode + + // enqueue를 await해 기존 backpressure를 유지하고, gateway 설정 시 off-thread decode 경로로 제출된다. + // decode 실패는 setFrameErrorHandler(initBase)에서 onDisconnected로 처리한다. + await this.communicator.onReceivedFrame(packetBytes); + void this.sendHeartbeat(); } } } diff --git a/typescript/test/communicator.test.ts b/typescript/test/communicator.test.ts index ec95aef..a59a901 100644 --- a/typescript/test/communicator.test.ts +++ b/typescript/test/communicator.test.ts @@ -734,3 +734,148 @@ describe("WorkerGateway lifecycle", () => { }); }); +describe("WorkerGateway cleanup stats", () => { + test("close()는 queued/reorder/sink backlog를 비우고 closed=true로 만든다", async () => { + let resolveDecode!: (env: DecodedEnvelope) => void; + const delivered: number[] = []; + const gateway = new WorkerGateway({ + workers: 1, + decode: () => + new Promise((resolve) => { + resolveDecode = resolve; + }), + sink: (frame) => { + delivered.push(frame.seq); + }, + }); + + // workers=1이므로 첫 frame은 in-flight decode, 나머지 둘은 queued backlog가 된다. + gateway.submit({ seq: 1, bytes: new Uint8Array([1]) }); + gateway.submit({ seq: 2, bytes: new Uint8Array([2]) }); + gateway.submit({ seq: 3, bytes: new Uint8Array([3]) }); + await waitFor(() => resolveDecode !== undefined); + + const before = gateway.stats(); + expect(before.active).toBe(1); + expect(before.queued).toBe(2); + expect(before.reorderPending).toBe(0); + expect(before.sinkPending).toBe(0); + expect(before.closed).toBe(false); + + gateway.close(); + const after = gateway.stats(); + expect(after.queued).toBe(0); + expect(after.reorderPending).toBe(0); + expect(after.sinkPending).toBe(0); + expect(after.workerPending).toBe(0); + expect(after.closed).toBe(true); + + // close 후 완료되는 in-flight decode는 sink로 전달되지 않고 active를 0으로 drain한다. + resolveDecode({ typeName: "t", data: new Uint8Array(), incomingNonce: 0, responseNonce: 0 }); + await waitFor(() => gateway.stats().active === 0); + expect(delivered).toEqual([]); + }); + + test("stats()는 out-of-order 결과의 reorder pending을 관측하고 close가 비운다", async () => { + const resolvers = new Map void>(); + const delivered: number[] = []; + const gateway = new WorkerGateway({ + workers: 4, + decode: (bytes) => + new Promise((resolve) => { + resolvers.set(bytes[0]!, resolve); + }), + sink: (frame) => { + delivered.push(frame.seq); + }, + }); + + gateway.submit({ seq: 1, bytes: new Uint8Array([1]) }); + gateway.submit({ seq: 2, bytes: new Uint8Array([2]) }); + await waitFor(() => resolvers.size === 2); + + // seq 2를 먼저 완료시키면 reorder가 선행 seq 1을 기다리며 결과를 보류한다. + resolvers.get(2)!({ typeName: "t", data: new Uint8Array(), incomingNonce: 0, responseNonce: 0 }); + await waitFor(() => gateway.stats().reorderPending === 1); + expect(delivered).toEqual([]); + + gateway.close(); + const after = gateway.stats(); + expect(after.reorderPending).toBe(0); + expect(after.queued).toBe(0); + expect(after.closed).toBe(true); + }); + + test("Node worker gateway: pending decode가 있는 상태에서 close 후 workerPending=0", async () => { + const gateway = createNodeWorkerGateway({ + sink: () => {}, + workers: 2, + }); + + const total = 6; + for (let i = 1; i <= total; i += 1) { + gateway.submit({ seq: i, bytes: rawFrame(i, i) }); + } + + // 동기 제출 직후에는 worker pool 응답이 아직 없어 in-flight decode와 queued backlog가 관측된다. + const before = gateway.stats?.(); + expect(before?.workerPending).toBeGreaterThan(0); + expect(before?.queued).toBeGreaterThan(0); + expect(before?.closed).toBe(false); + + gateway.close(); + const after = gateway.stats?.(); + expect(after?.workerPending).toBe(0); + expect(after?.queued).toBe(0); + expect(after?.closed).toBe(true); + }); + + test("close()는 양수 sinkPending을 비우고 release된 sink가 stale delivery를 만들지 않는다", async () => { + let releaseSink!: () => void; + const sinkBlock = new Promise((resolve) => { + releaseSink = resolve; + }); + const delivered: number[] = []; + let sinkCalls = 0; + const gateway = new WorkerGateway({ + workers: 2, + // decode는 즉시 완료되어 두 결과가 reorder를 통해 sink chain에 스케줄된다. + decode: (bytes): DecodedEnvelope => ({ + typeName: "t", + data: new Uint8Array(), + incomingNonce: bytes[0]!, + responseNonce: 0, + }), + sink: (frame) => { + sinkCalls += 1; + delivered.push(frame.seq); + // 첫 sink 호출을 unresolved promise로 막아 sinkChain을 보류시킨다 → sinkPending이 양수가 된다. + if (sinkCalls === 1) { + return sinkBlock; + } + return undefined; + }, + }); + + gateway.submit({ seq: 1, bytes: new Uint8Array([1]) }); + gateway.submit({ seq: 2, bytes: new Uint8Array([2]) }); + + // 첫 sink가 block되어 후속 결과가 sink chain에 적체된다. + await waitFor(() => gateway.stats().sinkPending > 0); + expect(delivered).toEqual([1]); + + gateway.close(); + const after = gateway.stats(); + expect(after.sinkPending).toBe(0); + expect(after.queued).toBe(0); + expect(after.reorderPending).toBe(0); + expect(after.workerPending).toBe(0); + expect(after.closed).toBe(true); + + // blocked sink를 release해도 close 이후에는 두 번째 frame이 sink로 전달되지 않는다. + releaseSink(); + await new Promise((r) => setTimeout(r, 10)); + expect(delivered).toEqual([1]); + }); +}); + diff --git a/typescript/test/tcp.test.ts b/typescript/test/tcp.test.ts index ae955af..5e19b8c 100644 --- a/typescript/test/tcp.test.ts +++ b/typescript/test/tcp.test.ts @@ -10,6 +10,7 @@ import { parserFromSchema, sendRequestTyped, } from "../src/communicator.js"; +import { createNodeWorkerGateway } from "../src/node_inbound_gateway.js"; import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectTcp, connectTcpTls, TcpClient } from "../src/tcp_client.js"; import { TcpServer } from "../src/tcp_server.js"; @@ -212,6 +213,64 @@ describe("TCP", () => { }); }); + test( + "TCP read loop drives server inbound gateway: concurrent request-response keeps FIFO/nonce", + async () => { + const server = new TcpServer("127.0.0.1", 0, (socket) => new TcpClient(socket, 0, 0, parserMap())); + cleanup.push(async () => server.stop()); + await server.start(); + + let lastNonce = 0; + let fifoViolation = false; + server.onClientConnected = (client) => { + const comm = client.communicator; + // 실제 TCP read loop가 onReceivedFrame으로 흘린 raw frame을 worker_threads gateway가 off-thread로 + // decode하고, reorder 후 coordinator가 FIFO로 dispatch하는지 검증한다. + comm.attachInboundGateway( + createNodeWorkerGateway({ + sink: (frame) => + comm.enqueueInbound(frame.typeName, frame.data, frame.incomingNonce, frame.responseNonce), + workers: 2, + }), + ); + // raw addRequestListener로 nonce를 받아 gateway reorder 이후에도 connection별 nonce가 + // 단조 증가(FIFO)하는지 확인한다. + comm.addRequestListener(TestDataSchema.typeName, (msg, nonce) => { + if (nonce < lastNonce) { + fifoViolation = true; + } + lastNonce = nonce; + const req = msg as TestData; + return create(TestDataSchema, { index: req.index * 2, message: `gw echo: ${req.message}` }); + }); + }; + + const client = await connectTcp("127.0.0.1", server.port, 0, 0, parserMap()); + cleanup.push(async () => client.close()); + + // 동시 요청이 gateway off-thread decode + reorder를 거쳐도 nonce 상관관계가 유지되어야 한다. + // worker_threads gateway는 첫 submit 시 worker pool을 lazily spawn하므로 cold-start를 흡수할 수 + // 있게 요청 timeout을 넉넉히 둔다(기존 worker_threads 단위 테스트와 동일한 여유). + const results = await Promise.all( + Array.from({ length: 16 }, (_, i) => + sendRequestTyped( + client.communicator, + create(TestDataSchema, { index: i, message: `req ${i}` }), + TestDataSchema, + 8000, + ), + ), + ); + + results.forEach((res, i) => { + expect(res.index).toBe(i * 2); + expect(res.message).toBe(`gw echo: req ${i}`); + }); + expect(fifoViolation).toBe(false); + }, + 15000, + ); + test("TCP inbound backpressure pauses socket read on server", async () => { let serverClient: TcpClient | null = null; const server = new TcpServer("127.0.0.1", 0, (socket) => { diff --git a/typescript/test/ws.test.ts b/typescript/test/ws.test.ts index 43cd681..8e2328d 100644 --- a/typescript/test/ws.test.ts +++ b/typescript/test/ws.test.ts @@ -12,6 +12,7 @@ import { parserFromSchema, sendRequestTyped, } from "../src/communicator.js"; +import { createNodeWorkerGateway } from "../src/node_inbound_gateway.js"; import { create, TestDataSchema, type TestData } from "../src/packets/message_common_pb.js"; import { connectNodeWs, connectNodeWss, NodeWsClient } from "../src/node_ws_client.js"; import { NodeWsServer } from "../src/node_ws_server.js"; @@ -212,6 +213,63 @@ describe("WS", () => { }); }); + test( + "WS read loop drives server inbound gateway: concurrent request-response keeps FIFO/nonce", + async () => { + const server = new NodeWsServer("127.0.0.1", 0, "/", (ws) => new NodeWsClient(ws, 0, 0, parserMap())); + cleanup.push(async () => server.stop()); + await server.start(); + + let lastNonce = 0; + let fifoViolation = false; + server.onClientConnected = (client) => { + const comm = client.communicator; + // 실제 WS read loop가 onReceivedFrame으로 흘린 raw binary message를 worker_threads gateway가 + // off-thread로 decode하고, reorder 후 coordinator가 FIFO로 dispatch하는지 검증한다. + comm.attachInboundGateway( + createNodeWorkerGateway({ + sink: (frame) => + comm.enqueueInbound(frame.typeName, frame.data, frame.incomingNonce, frame.responseNonce), + workers: 2, + }), + ); + // raw addRequestListener로 nonce를 받아 gateway reorder 이후에도 connection별 nonce가 + // 단조 증가(FIFO)하는지 확인한다. + comm.addRequestListener(TestDataSchema.typeName, (msg, nonce) => { + if (nonce < lastNonce) { + fifoViolation = true; + } + lastNonce = nonce; + const req = msg as TestData; + return create(TestDataSchema, { index: req.index * 2, message: `gw echo: ${req.message}` }); + }); + }; + + const client = await connectNodeWs("127.0.0.1", server.port, "/", 0, 0, parserMap()); + cleanup.push(async () => client.close()); + + // worker_threads gateway는 첫 submit 시 worker pool을 lazily spawn하므로 cold-start를 흡수할 수 + // 있게 요청 timeout을 넉넉히 둔다(기존 worker_threads 단위 테스트와 동일한 여유). + const results = await Promise.all( + Array.from({ length: 16 }, (_, i) => + sendRequestTyped( + client.communicator, + create(TestDataSchema, { index: i, message: `req ${i}` }), + TestDataSchema, + 8000, + ), + ), + ); + + results.forEach((res, i) => { + expect(res.index).toBe(i * 2); + expect(res.message).toBe(`gw echo: req ${i}`); + }); + expect(fifoViolation).toBe(false); + }, + 15000, + ); + test("connectNodeWss rejects when peer never completes WebSocket handshake", async () => { const server = tls.createServer({ cert, key }, (socket) => { socket.on("data", () => {});