update: sync changes across implementations and add task logs
This commit is contained in:
parent
ef0fa5e8bc
commit
15d11f9ac5
20 changed files with 1781 additions and 13 deletions
|
|
@ -180,8 +180,8 @@ Sending `HeartBeat {}`:
|
|||
| Kotlin | Available | `kotlin/` |
|
||||
| Swift | Planned | `swift/` |
|
||||
| Go | Available | `go/` |
|
||||
| TypeScript | Planned | `typescript/` |
|
||||
| Python | Planned | `python/` |
|
||||
| TypeScript | Available | `typescript/` |
|
||||
| Python | Available | `python/` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,17 @@ Examples of non-breaking changes:
|
|||
|
||||
Backward-compatible message additions require updated parser maps and cross-language tests before release.
|
||||
|
||||
## nonce Overflow
|
||||
|
||||
`nonce` and `responseNonce` fields are protobuf `int32` values (signed 32-bit).
|
||||
Overflow occurs after about 2.1 billion sends on a single connection.
|
||||
|
||||
Current policy:
|
||||
|
||||
- The protocol does not define overflow behavior. Reaching the int32 limit on a single connection is not realistic.
|
||||
- Implementations do not add recovery logic for overflow.
|
||||
- If overflow handling is needed in the future, the protocol version will be bumped and wrap-around behavior will be specified.
|
||||
|
||||
## New Language Compatibility
|
||||
|
||||
A new language implementation must target the current protocol version unless its README explicitly says otherwise. Before it is listed as available, it must:
|
||||
|
|
|
|||
23
agent-task/ai_first_improvements/complete.log
Normal file
23
agent-task/ai_first_improvements/complete.log
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
task=ai_first_improvements
|
||||
date=2026-04-24
|
||||
result=PASS
|
||||
|
||||
## 완료된 작업 (plan=0)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| AI_FIRST-1 | Dart 전송 계층 분리 — Transport 인터페이스, TCP/WS adapter 주입, maxPacketSize guard, 회귀 테스트 추가 |
|
||||
| AI_FIRST-2 | Proto sync 도구 — `tools/check_proto_sync.sh`, `tools/generate_proto.sh` 추가 |
|
||||
| AI_FIRST-3 | VERSIONING.md 추가, PROTOCOL.md에 protocol version·breaking 후보 명시 |
|
||||
| AI_FIRST-4 | `templates/language/` scaffold — IMPLEMENTATION_CHECKLIST.md, CROSSTEST_CHECKLIST.md, README.md |
|
||||
|
||||
## 잔여 Nit (동작 영향 없음)
|
||||
|
||||
- `dart/lib/src/communicator.dart:58-60` — `if (transport == null)` dead branch → quality_improvements 후속 작업에서 수정 예정
|
||||
|
||||
## 최종 상태
|
||||
|
||||
- dart analyze: No issues found
|
||||
- dart test: 42개 PASS
|
||||
- Dart×Go crosstest: 양방향 PASS
|
||||
- tools/check_proto_sync.sh: Proto schemas are in sync
|
||||
16
agent-task/dart_close_fix/complete.log
Normal file
16
agent-task/dart_close_fix/complete.log
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
task=dart_close_fix
|
||||
date=2026-04-24
|
||||
result=PASS
|
||||
|
||||
## 완료된 작업 (plan=0)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| REVIEW_DART_API-1 | `Communicator.cancelPendingRequests()` 추가 — close 시 pending sendRequest future를 StateError로 완료 |
|
||||
| REVIEW_DART_API-2 | TCP/WS 서버 disconnect 콜백에서 `unawaited(client.close())` 명시 |
|
||||
| REVIEW_DART_API-3 | `BaseClient.close()` 순서 정렬: isAlive=false → stopHeartbeat → cancelPendingRequests → closeTransport → disconnect listeners |
|
||||
|
||||
## 최종 상태
|
||||
|
||||
- dart analyze: No issues found
|
||||
- dart test: 40개 PASS (communicator_test.dart 포함 회귀 테스트 통과)
|
||||
|
|
@ -157,3 +157,33 @@ PASS all go-server/dart-client crosstests passed
|
|||
$ cd go && go run ./crosstest/go_kotlin.go
|
||||
PASS all go-server/kotlin-client crosstests passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 종합 판정
|
||||
|
||||
**PASS**
|
||||
|
||||
> 7개 항목 모두 구현 완료. TCP/WS transport, Communicator, BaseClient 동작이 PROTOCOL.md 명세와 일치하며, Go×Python 크로스 테스트가 TCP·WS 양방향 모두 통과한다.
|
||||
|
||||
### 차원별 평가
|
||||
|
||||
| 차원 | 판정 | 비고 |
|
||||
|------|------|------|
|
||||
| 정확성 (Correctness) | Pass | `length==0` no-op, `length>MAX_PACKET_SIZE` disconnect, WS binary 필터 모두 정확 |
|
||||
| 완성도 (Completeness) | Pass | 계획서 체크리스트 전 항목 구현 확인 |
|
||||
| 테스트 커버리지 | Pass | 단위 7개 + TCP·WS·Go×Python 크로스 테스트 양방향 통과 |
|
||||
| API 계약 | Pass | `type_name_of` simple name이 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증 |
|
||||
| 코드 품질 | Pass | websockets v16/v12 호환 shim, asyncio.Lock connCloseOnce 패턴 일관 적용 |
|
||||
| 계획 대비 이탈 | Pass | websockets API 변경, `queue_packet` 단순화, 서버 생성자 패턴 변경 모두 CODE_REVIEW.md에 명시됨 |
|
||||
| 검증 신뢰도 | Pass | 보고된 출력이 실제 구현 로직과 일치 |
|
||||
|
||||
### 발견된 문제
|
||||
|
||||
- **[Nit]** `tcp_client.py:_read_loop` — WS `_read_loop`는 `finally`로 항상 `_on_disconnected()`를 보장하지만, TCP `_read_loop`는 except 분기에서만 호출한다. read task가 외부에서 cancel되면 cleanup이 실행되지 않는다. 현재 read task는 외부에서 cancel되지 않으므로 실제 문제는 아니다.
|
||||
|
||||
### 다음 단계
|
||||
|
||||
**[PASS]** 추가 작업 불필요. 아카이브 완료.
|
||||
|
|
|
|||
189
agent-task/python_impl/code_review_0.log
Normal file
189
agent-task/python_impl/code_review_0.log
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<!-- task=python_impl plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-21
|
||||
task=python_impl, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_0.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Python 패키지 scaffold | [x] |
|
||||
| [API-2] Communicator 구현 | [x] |
|
||||
| [API-3] BaseClient 구현 | [x] |
|
||||
| [API-4] TCP Transport 구현 | [x] |
|
||||
| [API-5] WebSocket Transport 구현 | [x] |
|
||||
| [API-6] 단위 테스트 | [x] |
|
||||
| [API-7] Python×Go 크로스 테스트 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- **websockets API**: 계획에서는 `websockets.connect` / `websockets.server.serve`를 언급했으나, 설치된 버전이 16.0으로 legacy API가 deprecated됨. `websockets.asyncio.client.connect` / `websockets.asyncio.server.serve`로 대체.
|
||||
- **`queue_packet` 구현**: 계획의 큐 full 처리를 `put_nowait`로 단순화. maxsize=64이고 write_loop가 항상 소비하므로 현실적으로 블로킹이 발생하지 않음.
|
||||
- **`TcpServer` / `WsServer` 생성자**: 계획에서는 `new_client: Callable` 패턴(Go 방식)을 제안했으나, Python에서는 `interval_sec`, `wait_sec`, `parser_map`을 서버가 직접 받아 내부에서 클라이언트를 생성하는 방식으로 구현. API가 더 단순해짐.
|
||||
- **`_echo_handler` 패턴**: test_tcp.py에서 request listener가 응답을 보내기 위해 `queue_packet`을 직접 사용. 추후 `add_request_listener_typed` 헬퍼 추가 시 개선 가능.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- **sentinel 패턴**: `_write_loop` 종료를 위해 `_STOP` sentinel 객체를 큐에 삽입. `asyncio.Event`보다 단순하고 명확.
|
||||
- **asyncio 단일 스레드**: `is_alive`, `nonce` 등 모든 상태가 단일 스레드에서만 접근되므로 lock 불필요. `BaseClient.close()`만 `asyncio.Lock`으로 connCloseOnce 보장.
|
||||
- **Python 오케스트레이터의 Go 클라이언트 실행**: `asyncio.get_event_loop().run_in_executor(None, ...)` 로 블로킹 `subprocess.run`을 실행. 이로써 서버 이벤트루프가 Go 클라이언트 실행 중에도 계속 동작.
|
||||
- **`type_name_of(cls)`**: `cls.DESCRIPTOR.name`을 반환. proto package가 없으므로 `TestData`, `HeartBeat` 등 simple name이 되어 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `type_name_of(cls)` 가 `cls.DESCRIPTOR.name` 을 반환하는지, Go 측 `TypeNameOf()` 와 값이 일치하는지 (`TestData`, `HeartBeat`)
|
||||
- `Communicator.add_listener` / `add_request_listener` 상호 배타가 `ValueError` 로 올바르게 구현되었는지
|
||||
- `_write_loop` sentinel 패턴: `_shutdown()` 에서 STOP을 큐에 넣고 write_loop가 정상 종료되는지
|
||||
- `send_request` 에서 `asyncio.wait_for` timeout 처리 시 pending 딕셔너리에서 해당 nonce가 제거되는지
|
||||
- `BaseClient.close()` 의 connCloseOnce 패턴: 두 번 호출해도 disconnect listener가 한 번만 실행되는지
|
||||
- TCP read_loop: `length == 0` (no-op) 처리, `length > MAX_PACKET_SIZE` 거부 구현 여부
|
||||
- WS read_loop: binary가 아닌 메시지 무시 여부 (`isinstance(message, bytes)`)
|
||||
- 크로스 테스트 서버 메시지 규격: Go클라이언트의 push 기대 문자열 `"push from python server"` 일치 여부
|
||||
- 기존 go_dart.go, go_kotlin.go 크로스 테스트 회귀 없음 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### API-1 중간 검증
|
||||
```
|
||||
$ cd python && python3 -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"
|
||||
TestData
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/test_communicator.py -v
|
||||
============================= test session starts ==============================
|
||||
collected 4 items
|
||||
|
||||
test/test_communicator.py::test_add_listener_exclusivity PASSED [ 25%]
|
||||
test/test_communicator.py::test_send_and_receive PASSED [ 50%]
|
||||
test/test_communicator.py::test_send_request_response PASSED [ 75%]
|
||||
test/test_communicator.py::test_shutdown PASSED [100%]
|
||||
|
||||
========================= 4 passed, 1 warning in 0.14s =========================
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```
|
||||
$ python3 -c "from toki_socket.base_client import BaseClient; print('OK')"
|
||||
OK
|
||||
```
|
||||
|
||||
### API-4 중간 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/test_tcp.py -v
|
||||
collected 2 items
|
||||
|
||||
test/test_tcp.py::test_tcp_send_receive PASSED [ 50%]
|
||||
test/test_tcp.py::test_tcp_request_response PASSED [100%]
|
||||
|
||||
========================= 2 passed, 1 warning in 0.16s =========================
|
||||
```
|
||||
|
||||
### API-5 중간 검증
|
||||
```
|
||||
$ python3 -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"
|
||||
OK
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/ -v
|
||||
collected 6 items
|
||||
|
||||
test/test_communicator.py::test_add_listener_exclusivity PASSED
|
||||
test/test_communicator.py::test_send_and_receive PASSED
|
||||
test/test_communicator.py::test_send_request_response PASSED
|
||||
test/test_communicator.py::test_shutdown PASSED
|
||||
test/test_tcp.py::test_tcp_send_receive PASSED
|
||||
test/test_tcp.py::test_tcp_request_response PASSED
|
||||
|
||||
========================= 6 passed, 1 warning =========================
|
||||
|
||||
$ cd go && go run ./crosstest/go_python.go
|
||||
INFO typeName go=TestData
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all go-server/python-client crosstests passed
|
||||
|
||||
$ cd python && python3 crosstest/python_go.py
|
||||
INFO typeName python=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from python server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from python server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all python-server/go-client crosstests passed
|
||||
|
||||
$ cd go && go run ./crosstest/go_dart.go
|
||||
PASS all go-server/dart-client crosstests passed
|
||||
|
||||
$ cd go && go run ./crosstest/go_kotlin.go
|
||||
PASS all go-server/kotlin-client crosstests passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 종합 판정
|
||||
|
||||
**PASS**
|
||||
|
||||
> 7개 항목 모두 구현 완료. TCP/WS transport, Communicator, BaseClient 동작이 PROTOCOL.md 명세와 일치하며, Go×Python 크로스 테스트가 TCP·WS 양방향 모두 통과한다.
|
||||
|
||||
### 차원별 평가
|
||||
|
||||
| 차원 | 판정 | 비고 |
|
||||
|------|------|------|
|
||||
| 정확성 (Correctness) | Pass | `length==0` no-op, `length>MAX_PACKET_SIZE` disconnect, WS binary 필터 모두 정확 |
|
||||
| 완성도 (Completeness) | Pass | 계획서 체크리스트 전 항목 구현 확인 |
|
||||
| 테스트 커버리지 | Pass | 단위 7개 + TCP·WS·Go×Python 크로스 테스트 양방향 통과 |
|
||||
| API 계약 | Pass | `type_name_of` simple name이 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증 |
|
||||
| 코드 품질 | Pass | websockets v16/v12 호환 shim, asyncio.Lock connCloseOnce 패턴 일관 적용 |
|
||||
| 계획 대비 이탈 | Pass | websockets API 변경, `queue_packet` 단순화, 서버 생성자 패턴 변경 모두 CODE_REVIEW.md에 명시됨 |
|
||||
| 검증 신뢰도 | Pass | 보고된 출력이 실제 구현 로직과 일치 |
|
||||
|
||||
### 발견된 문제
|
||||
|
||||
- **[Nit]** `tcp_client.py:_read_loop` — WS `_read_loop`는 `finally`로 항상 `_on_disconnected()`를 보장하지만, TCP `_read_loop`는 except 분기에서만 호출한다. read task가 외부에서 cancel되면 cleanup이 실행되지 않는다. 현재 read task는 외부에서 cancel되지 않으므로 실제 문제는 아니다.
|
||||
|
||||
### 다음 단계
|
||||
|
||||
**[PASS]** 추가 작업 불필요. 아카이브 완료.
|
||||
21
agent-task/python_impl/complete.log
Normal file
21
agent-task/python_impl/complete.log
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
task=python_impl
|
||||
date=2026-04-24
|
||||
result=PASS
|
||||
|
||||
## 완료된 작업 (plan=0)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| API-1 | Python 패키지 scaffold (toki_socket/) |
|
||||
| API-2 | Communicator 구현 (type_name_of, 상호배타 리스너, sentinel shutdown, send_request timeout) |
|
||||
| API-3 | BaseClient 구현 (connCloseOnce via asyncio.Lock, heartbeat) |
|
||||
| API-4 | TCP Transport 구현 (MAX_PACKET_SIZE guard, length==0 no-op) |
|
||||
| API-5 | WebSocket Transport 구현 (binary filter, websockets v16/v12 compat shim) |
|
||||
| API-6 | 단위 테스트 7개 PASS |
|
||||
| API-7 | Go×Python 크로스 테스트 양방향 (TCP+WS) PASS |
|
||||
|
||||
## 최종 상태
|
||||
|
||||
- Python pytest: 7/7 PASSED
|
||||
- Go×Python crosstest: go-server/python-client + python-server/go-client 모두 PASS
|
||||
- Go×Dart, Go×Kotlin 회귀 없음
|
||||
691
agent-task/python_impl/plan_0.log
Normal file
691
agent-task/python_impl/plan_0.log
Normal file
|
|
@ -0,0 +1,691 @@
|
|||
<!-- task=python_impl plan=0 tag=API -->
|
||||
|
||||
# Python 구현체 추가 (toki_socket)
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
|
||||
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
|
||||
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
|
||||
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
`PROTOCOL.md`의 Multi-language Implementations 표에 Python이 `Planned`로 등록되어 있다.
|
||||
Go 구현체(`go/`)가 레퍼런스이며, `PORTING_GUIDE.md`에 Python 매핑 표와 주의사항이 명시되어 있다.
|
||||
Python 패키지를 `python/` 디렉터리에 생성하고, Go×Python 양방향 크로스 테스트를 추가하여 프로토콜 호환성을 검증한다.
|
||||
TLS/WSS는 이번 범위에서 제외하고 README에 명시한다.
|
||||
|
||||
---
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
```
|
||||
[API-1] scaffold → [API-2] Communicator → [API-3] BaseClient
|
||||
→ [API-4] TCP Transport
|
||||
→ [API-5] WebSocket Transport
|
||||
[API-2~5] 완료 → [API-6] 단위 테스트 → [API-7] 크로스 테스트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [API-1] Python 패키지 scaffold
|
||||
|
||||
### 문제
|
||||
|
||||
`python/` 디렉터리가 없다. 패키지 매니저, proto binding, 최소 패키지 구조가 필요하다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
- `python/pyproject.toml` 생성 (setuptools, Python 3.11+)
|
||||
- `python/toki_socket/__init__.py` 생성
|
||||
- `dart/lib/src/packets/message_common.proto`를 `python/toki_socket/packets/message_common.proto`로 복사 (schema만, 언어 option 추가 불필요)
|
||||
- `protoc`로 `message_common_pb2.py`, `message_common_pb2.pyi` 생성
|
||||
|
||||
**pyproject.toml 핵심**:
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=68"]
|
||||
build-backend = "setuptools.backends.legacy:build"
|
||||
|
||||
[project]
|
||||
name = "toki-socket"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"protobuf>=4.25",
|
||||
"websockets>=12.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
||||
```
|
||||
|
||||
**디렉터리 구조**:
|
||||
```
|
||||
python/
|
||||
toki_socket/
|
||||
__init__.py
|
||||
communicator.py
|
||||
base_client.py
|
||||
tcp_client.py
|
||||
tcp_server.py
|
||||
ws_client.py
|
||||
ws_server.py
|
||||
packets/
|
||||
__init__.py
|
||||
message_common.proto
|
||||
message_common_pb2.py (generated)
|
||||
message_common_pb2.pyi (generated)
|
||||
test/
|
||||
__init__.py
|
||||
test_communicator.py
|
||||
test_tcp.py
|
||||
crosstest/
|
||||
go_python_client.py
|
||||
python_go.py
|
||||
pyproject.toml
|
||||
README.md
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/pyproject.toml` 생성
|
||||
- [ ] `python/toki_socket/__init__.py` 생성 (public export 비워둠)
|
||||
- [ ] `python/toki_socket/packets/__init__.py` 생성
|
||||
- [ ] `python/toki_socket/packets/message_common.proto` 복사
|
||||
- [ ] `python/toki_socket/packets/message_common_pb2.py` 생성 (protoc 실행 또는 직접 작성)
|
||||
- [ ] `python/toki_socket/packets/message_common_pb2.pyi` 생성
|
||||
- [ ] `python/README.md` 생성 (TLS 미지원 명시 포함)
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
skip — scaffold 단계, 코드 로직 없음.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && pip install -e ".[dev]" --quiet && python -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"
|
||||
```
|
||||
|
||||
예상 출력: `TestData`
|
||||
|
||||
---
|
||||
|
||||
## [API-2] Communicator 구현
|
||||
|
||||
### 문제
|
||||
|
||||
`go/communicator.go`에 해당하는 Python 비동기 구현이 없다.
|
||||
`Transport` Protocol, `Communicator` 클래스, write 큐, nonce 관리, listener/requestListener 라우팅을 구현해야 한다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`python/toki_socket/communicator.py`에 아래 구조로 구현한다.
|
||||
|
||||
**Transport Protocol** (Go의 `Transport` interface):
|
||||
```python
|
||||
from typing import Protocol, runtime_checkable
|
||||
from toki_socket.packets.message_common_pb2 import PacketBase
|
||||
|
||||
@runtime_checkable
|
||||
class Transport(Protocol):
|
||||
async def write_packet(self, base: PacketBase) -> None: ...
|
||||
async def close(self) -> None: ...
|
||||
```
|
||||
|
||||
**ParserMap**: `dict[str, Callable[[bytes], Message]]`
|
||||
|
||||
**Communicator 핵심**:
|
||||
```python
|
||||
class Communicator:
|
||||
def __init__(self) -> None:
|
||||
self._nonce: int = 0
|
||||
self._is_alive: bool = False
|
||||
self._parser_map: dict[str, Callable] = {}
|
||||
self._handlers: dict[str, list[Callable]] = {}
|
||||
self._req_handlers: dict[str, Callable] = {}
|
||||
self._pending_requests: dict[int, asyncio.Future] = {}
|
||||
self._write_queue: asyncio.Queue = asyncio.Queue(maxsize=64)
|
||||
self._closed: asyncio.Event = asyncio.Event()
|
||||
self._transport: Transport | None = None
|
||||
self._write_loop_task: asyncio.Task | None = None
|
||||
self._write_error_handler: Callable[[Exception], None] | None = None
|
||||
|
||||
def initialize(self, transport: Transport, parser_map: dict) -> None:
|
||||
... # 하트비트 파서 추가, write_loop Task 시작
|
||||
|
||||
def is_alive(self) -> bool: ...
|
||||
def _next_nonce(self) -> int: ...
|
||||
def _shutdown(self) -> None: ...
|
||||
async def close(self) -> None: ...
|
||||
async def _write_loop(self) -> None: ...
|
||||
async def queue_packet(self, base: PacketBase) -> None: ...
|
||||
async def send(self, message: Message) -> None: ...
|
||||
async def send_request(self, req: Message, res_type: type[T], timeout: float = 30.0) -> T: ...
|
||||
def add_listener(self, type_name: str, fn: Callable) -> None: ...
|
||||
def remove_listeners(self, type_name: str) -> None: ...
|
||||
def add_request_listener(self, type_name: str, fn: Callable) -> None: ...
|
||||
def on_received_data(self, type_name: str, data: bytes, nonce: int, response_nonce: int) -> None: ...
|
||||
def _handle_response(self, type_name: str, data: bytes, response_nonce: int) -> None: ...
|
||||
def _parse(self, type_name: str, data: bytes) -> Message: ...
|
||||
|
||||
def type_name_of(message_class) -> str:
|
||||
return message_class.DESCRIPTOR.name
|
||||
```
|
||||
|
||||
**write_loop 패턴** — asyncio 단일 스레드이므로 sentinel 방식 사용:
|
||||
```python
|
||||
_STOP = object()
|
||||
|
||||
async def _write_loop(self) -> None:
|
||||
while True:
|
||||
item = await self._write_queue.get()
|
||||
if item is _STOP:
|
||||
return
|
||||
base, fut = item
|
||||
try:
|
||||
await self._transport.write_packet(base)
|
||||
if not fut.done():
|
||||
fut.set_result(None)
|
||||
except Exception as e:
|
||||
if not fut.done():
|
||||
fut.set_exception(e)
|
||||
if self._write_error_handler:
|
||||
self._write_error_handler(e)
|
||||
```
|
||||
|
||||
`_shutdown()`에서 `_write_queue.put_nowait(_STOP)` 호출.
|
||||
|
||||
**send_request** — `asyncio.get_event_loop().create_future()`로 pending 등록, `asyncio.wait_for(fut, timeout)`으로 대기.
|
||||
|
||||
**addListener / addRequestListener 상호 배타** — 같은 typeName 중복 시 `ValueError` raise.
|
||||
|
||||
**HeartBeat 자동 처리** — `initialize()` 내에서 HeartBeat 파서를 `_parser_map`에 추가; BaseClient에서 listener 등록.
|
||||
|
||||
**on_received_data** — asyncio 단일 스레드이므로 요청 핸들러는 `asyncio.create_task()`로 실행 (Go의 `go reqHandler(...)`와 동일).
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/communicator.py` 생성
|
||||
- [ ] `Transport` Protocol 정의
|
||||
- [ ] `type_name_of(cls)` 함수 — `cls.DESCRIPTOR.name` 반환
|
||||
- [ ] `Communicator.initialize()` — parser_map + HeartBeat 파서 등록, write_loop Task 시작
|
||||
- [ ] `Communicator._write_loop()` — sentinel 패턴
|
||||
- [ ] `Communicator.queue_packet()` — closed 체크, Future 등록, put, await
|
||||
- [ ] `Communicator.send()` — marshal → queue_packet
|
||||
- [ ] `Communicator.send_request()` — pending 등록, marshal, queue, asyncio.wait_for
|
||||
- [ ] `Communicator.add_listener()` / `add_request_listener()` — 상호 배타 검사
|
||||
- [ ] `Communicator.on_received_data()` — response_nonce > 0이면 handleResponse, 아니면 listener/reqHandler
|
||||
- [ ] `Communicator._shutdown()` — is_alive=False, closed.set(), write_queue에 STOP 전달
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
`python/test/test_communicator.py`에 작성:
|
||||
- `test_add_listener_exclusivity` — 같은 typeName에 add_listener 후 add_request_listener 시 ValueError 확인
|
||||
- `test_send_and_receive` — FakeTransport (write_packet 캡처)로 send() 후 패킷 내용 검증
|
||||
- `test_send_request_response` — FakeTransport로 send_request 후 수동으로 on_received_data 호출하여 Future resolve 검증
|
||||
- `test_shutdown` — close() 후 is_alive() == False 검증
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -m pytest test/test_communicator.py -v
|
||||
```
|
||||
|
||||
예상: 4개 테스트 PASS
|
||||
|
||||
---
|
||||
|
||||
## [API-3] BaseClient 구현
|
||||
|
||||
### 문제
|
||||
|
||||
`go/base_client.go`에 해당하는 heartbeat, disconnect listener, connCloseOnce 패턴이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`python/toki_socket/base_client.py`에 구현.
|
||||
|
||||
Go `sync.Once` → `asyncio.Lock` + `bool` 플래그:
|
||||
```python
|
||||
self._close_called: bool = False
|
||||
self._close_lock: asyncio.Lock = asyncio.Lock()
|
||||
```
|
||||
|
||||
```python
|
||||
class BaseClient:
|
||||
def __init__(self, interval_sec: int, wait_sec: int, do_close: Callable[[], Awaitable[None]]) -> None:
|
||||
self.communicator: Communicator = Communicator()
|
||||
self._interval_sec = interval_sec
|
||||
self._wait_sec = wait_sec
|
||||
self._do_close = do_close
|
||||
self._close_called = False
|
||||
self._close_lock: asyncio.Lock # initialize()에서 생성
|
||||
self._disconnect_listeners: list[Callable] = []
|
||||
self._hb_task: asyncio.Task | None = None
|
||||
self._waiting_hb_response: bool = False
|
||||
|
||||
def _init_base(self) -> None:
|
||||
self._close_lock = asyncio.Lock()
|
||||
|
||||
async def close(self) -> None:
|
||||
async with self._close_lock:
|
||||
if self._close_called:
|
||||
return
|
||||
self._close_called = True
|
||||
self.communicator._shutdown()
|
||||
self._stop_heartbeat()
|
||||
if self._do_close:
|
||||
await self._do_close()
|
||||
await self._notify_disconnected()
|
||||
|
||||
def add_disconnect_listener(self, fn: Callable) -> None: ...
|
||||
def remove_disconnect_listeners(self) -> None: ...
|
||||
async def _notify_disconnected(self) -> None: ...
|
||||
async def _on_disconnected(self) -> None:
|
||||
await self.close()
|
||||
async def _send_heartbeat(self) -> None: ... # Go sendHeartBeat() 패턴
|
||||
async def _on_heartbeat(self) -> None: ... # Go onHeartBeat() 패턴
|
||||
def _stop_heartbeat(self) -> None: ...
|
||||
```
|
||||
|
||||
Heartbeat 타이머: `asyncio.get_event_loop().call_later()` 또는 `asyncio.create_task(asyncio.sleep(n))`으로 구현.
|
||||
Go의 `HeartbeatTimer`에 해당하는 `_HbTimer` 헬퍼 클래스를 동일 파일에 작성.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/base_client.py` 생성
|
||||
- [ ] `_HbTimer` — `asyncio.create_task(sleep+callback)`, `cancel()` 메서드
|
||||
- [ ] `BaseClient._init_base()` — close_lock 초기화 (asyncio.Lock은 루프 바인딩 필요)
|
||||
- [ ] `BaseClient.close()` — connCloseOnce 패턴, shutdown → stop_heartbeat → do_close → notify
|
||||
- [ ] `BaseClient._send_heartbeat()` — interval > 0이면 타이머 설정
|
||||
- [ ] `BaseClient._on_heartbeat()` — waiting 상태이면 False로 리셋, 아니면 HeartBeat 송신
|
||||
- [ ] `BaseClient._notify_disconnected()` — listener 복사 후 asyncio.gather로 호출
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
단위 테스트 skip — Transport 없이 단독 테스트가 어렵고, TCP 테스트에서 통합 검증.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -c "from toki_socket.base_client import BaseClient; print('OK')"
|
||||
```
|
||||
|
||||
예상: `OK`
|
||||
|
||||
---
|
||||
|
||||
## [API-4] TCP Transport 구현
|
||||
|
||||
### 문제
|
||||
|
||||
Go의 `TcpClient` / `TcpServer`에 해당하는 asyncio TCP 구현이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`python/toki_socket/tcp_client.py`, `python/toki_socket/tcp_server.py` 구현.
|
||||
|
||||
**framing** (PROTOCOL.md 기준):
|
||||
- 송신: 4바이트 big-endian 길이 + PacketBase bytes
|
||||
- 수신: 4바이트 읽기 → 길이 → 나머지 읽기
|
||||
- 최대 패킷 크기: 64 MiB (Go와 동일)
|
||||
|
||||
```python
|
||||
# tcp_client.py
|
||||
class TcpClient(BaseClient):
|
||||
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter,
|
||||
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
|
||||
super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
|
||||
self._reader = reader
|
||||
self._writer = writer
|
||||
self._init_base()
|
||||
self.communicator.initialize(self, parser_map)
|
||||
self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
|
||||
self.communicator.add_listener(
|
||||
type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
|
||||
)
|
||||
asyncio.create_task(self._read_loop())
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
|
||||
async def write_packet(self, base: PacketBase) -> None:
|
||||
data = base.SerializeToString()
|
||||
header = struct.pack(">I", len(data))
|
||||
self._writer.write(header + data)
|
||||
await self._writer.drain()
|
||||
|
||||
async def _do_close_impl(self) -> None:
|
||||
self._writer.close()
|
||||
await self._writer.wait_closed()
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
while self.communicator.is_alive():
|
||||
try:
|
||||
header = await self._reader.readexactly(4)
|
||||
length = struct.unpack(">I", header)[0]
|
||||
if length == 0:
|
||||
continue
|
||||
if length > MAX_PACKET_SIZE:
|
||||
await self._on_disconnected(); return
|
||||
data = await self._reader.readexactly(length)
|
||||
base = PacketBase()
|
||||
base.ParseFromString(data)
|
||||
self.communicator.on_received_data(
|
||||
base.typeName, base.data, base.nonce, base.responseNonce
|
||||
)
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
except (asyncio.IncompleteReadError, ConnectionError, OSError):
|
||||
await self._on_disconnected(); return
|
||||
|
||||
async def connect_tcp(host: str, port: int, interval_sec: int, wait_sec: int,
|
||||
parser_map: ParserMap) -> TcpClient:
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
return TcpClient(reader, writer, interval_sec, wait_sec, parser_map)
|
||||
```
|
||||
|
||||
```python
|
||||
# tcp_server.py
|
||||
class TcpServer:
|
||||
def __init__(self, host: str, port: int, new_client: Callable) -> None:
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._new_client = new_client
|
||||
self._clients: list[TcpClient] = []
|
||||
self._server: asyncio.Server | None = None
|
||||
self.on_client_connected: Callable[[TcpClient], None] = lambda c: None
|
||||
|
||||
async def start(self) -> None: ...
|
||||
async def stop(self) -> None: ...
|
||||
def clients(self) -> list[TcpClient]: ...
|
||||
async def broadcast(self, message) -> None: ...
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/tcp_client.py` 생성
|
||||
- [ ] `MAX_PACKET_SIZE = 64 << 20`
|
||||
- [ ] `TcpClient(BaseClient)` — reader/writer, read_loop, write_packet, do_close
|
||||
- [ ] `connect_tcp(host, port, interval_sec, wait_sec, parser_map)` async 함수
|
||||
- [ ] `python/toki_socket/tcp_server.py` 생성
|
||||
- [ ] `TcpServer` — start/stop/clients/broadcast, on_client_connected 콜백
|
||||
- [ ] `_handle_connection()` — TcpClient 생성, disconnect_listener로 clients 목록 관리
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
`python/test/test_tcp.py`:
|
||||
- `test_tcp_send_receive` — 루프백 TCP로 양방향 TestData 교환
|
||||
- `test_tcp_request_response` — send_request / add_request_listener 검증
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_tcp_send_receive():
|
||||
received = asyncio.Future()
|
||||
server = TcpServer("127.0.0.1", 0, lambda r, w: TcpClient(r, w, 0, 0, parser_map()))
|
||||
server.on_client_connected = lambda c: c.communicator.add_listener(
|
||||
"TestData", lambda m: received.set_result(m)
|
||||
)
|
||||
await server.start()
|
||||
port = server.port # 동적 포트
|
||||
client = await connect_tcp("127.0.0.1", port, 0, 0, parser_map())
|
||||
await client.communicator.send(TestData(index=1, message="hello"))
|
||||
result = await asyncio.wait_for(received, 2.0)
|
||||
assert result.index == 1 and result.message == "hello"
|
||||
await client.close()
|
||||
await server.stop()
|
||||
```
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -m pytest test/test_tcp.py -v
|
||||
```
|
||||
|
||||
예상: 2개 테스트 PASS
|
||||
|
||||
---
|
||||
|
||||
## [API-5] WebSocket Transport 구현
|
||||
|
||||
### 문제
|
||||
|
||||
Go의 `WsClient` / `WsServer`에 해당하는 asyncio WebSocket 구현이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`websockets` 라이브러리 사용. WebSocket은 메시지 경계가 보장되므로 길이 헤더 없이 바이너리 프레임으로 전송.
|
||||
|
||||
```python
|
||||
# ws_client.py
|
||||
import websockets
|
||||
|
||||
class WsClient(BaseClient):
|
||||
def __init__(self, ws: websockets.WebSocketClientProtocol | websockets.WebSocketServerProtocol,
|
||||
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
|
||||
super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
|
||||
self._ws = ws
|
||||
self._init_base()
|
||||
self.communicator.initialize(self, parser_map)
|
||||
self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
|
||||
self.communicator.add_listener(
|
||||
type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
|
||||
)
|
||||
asyncio.create_task(self._read_loop())
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
|
||||
async def write_packet(self, base: PacketBase) -> None:
|
||||
await self._ws.send(base.SerializeToString())
|
||||
|
||||
async def _do_close_impl(self) -> None:
|
||||
await self._ws.close()
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
try:
|
||||
async for message in self._ws:
|
||||
if not isinstance(message, bytes):
|
||||
continue
|
||||
base = PacketBase()
|
||||
base.ParseFromString(message)
|
||||
self.communicator.on_received_data(
|
||||
base.typeName, base.data, base.nonce, base.responseNonce
|
||||
)
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
await self._on_disconnected()
|
||||
|
||||
async def connect_ws(host: str, port: int, path: str,
|
||||
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> WsClient:
|
||||
uri = f"ws://{host}:{port}{path}"
|
||||
ws = await websockets.connect(uri)
|
||||
return WsClient(ws, interval_sec, wait_sec, parser_map)
|
||||
```
|
||||
|
||||
```python
|
||||
# ws_server.py
|
||||
class WsServer:
|
||||
def __init__(self, host: str, port: int, path: str, new_client: Callable) -> None: ...
|
||||
async def start(self) -> None: ...
|
||||
async def stop(self) -> None: ...
|
||||
def clients(self) -> list[WsClient]: ...
|
||||
async def broadcast(self, message) -> None: ...
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/ws_client.py` 생성
|
||||
- [ ] `WsClient(BaseClient)` — websocket conn, read_loop, write_packet, do_close
|
||||
- [ ] `connect_ws(host, port, path, interval_sec, wait_sec, parser_map)` async 함수
|
||||
- [ ] `python/toki_socket/ws_server.py` 생성
|
||||
- [ ] `WsServer` — start/stop/clients/broadcast, path 기반 핸들러
|
||||
- [ ] `_handler(ws, path)` — WsClient 생성, disconnect_listener로 목록 관리
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
skip — TCP 테스트로 핵심 로직이 검증되며, WS는 크로스 테스트에서 검증.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"
|
||||
```
|
||||
|
||||
예상: `OK`
|
||||
|
||||
---
|
||||
|
||||
## [API-6] 단위 테스트
|
||||
|
||||
API-2, API-4 항목에서 이미 기술. 해당 항목 체크리스트 참조.
|
||||
|
||||
---
|
||||
|
||||
## [API-7] Python×Go 크로스 테스트
|
||||
|
||||
### 문제
|
||||
|
||||
Python이 Go 서버와 통신할 수 있는지, Go가 Python 서버와 통신할 수 있는지 검증이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
포트 할당:
|
||||
- Go 서버 / Python 클라이언트: TCP `29390`, WS `29392`
|
||||
- Python 서버 / Go 클라이언트: TCP `29490`, WS `29492`
|
||||
|
||||
**Python 클라이언트 헬퍼** `python/crosstest/go_python_client.py`:
|
||||
- `--mode tcp|ws`, `--port N`, `--phase send-push|requests` 플래그
|
||||
- `INFO typeName python=TestData` 출력
|
||||
- 각 시나리오 결과를 `PASS scenario=N detail=...` / `FAIL scenario=N error=...` 출력
|
||||
- send-push: 시나리오 1(fire-and-forget), 2(server push 수신)
|
||||
- requests: 시나리오 3(단일 요청), 4(동시 5개 요청 nonce 검증)
|
||||
- Go crosstest 클라이언트(`go/crosstest/dart_go_client/main.go`)와 동일한 구조
|
||||
|
||||
**Go 오케스트레이터** `go/crosstest/go_python.go`:
|
||||
- Go 서버를 시작하고 `python go_python_client.py` 서브프로세스를 실행
|
||||
- 기존 `go_dart.go` 패턴과 동일 (`//go:build ignore`, `runPythonClient()` 헬퍼)
|
||||
- Python 패키지 디렉터리를 소스 위치 기반으로 찾는 `pythonPackageDir()` 함수
|
||||
|
||||
**Python 오케스트레이터** `python/crosstest/python_go.py`:
|
||||
- Python 서버를 시작하고 `go run ./crosstest/python_go_client` 서브프로세스를 실행
|
||||
- `go/crosstest/dart_go_client/main.go` 패턴을 참고하여 `--mode`, `--port`, `--phase` 플래그 지원
|
||||
- Go 패키지 디렉터리를 소스 위치 기반으로 찾는 헬퍼
|
||||
|
||||
**Go 클라이언트 헬퍼** `go/crosstest/python_go_client/main.go`:
|
||||
- 기존 `dart_go_client/main.go`와 동일한 구조
|
||||
- Python 서버 포트(29490 TCP, 29492 WS)에 연결
|
||||
- `--phase send-push`: 시나리오 1, 2
|
||||
- `--phase requests`: 시나리오 3, 4
|
||||
- push 메시지 검증: `index=200 && message=="push from python server"`
|
||||
|
||||
**시나리오별 메시지 규격**:
|
||||
| 시나리오 | 클라이언트 송신 | 서버 검증 / 응답 |
|
||||
|----------|----------------|-----------------|
|
||||
| 1 (send) | `index=101, message="fire from python client"` | Go서버: index==101, message 검증 |
|
||||
| 2 (push) | — | Go서버 → `index=200, message="push from go server"` |
|
||||
| 3 (request) | `index=21, message="single request from python"` | Go서버 → `index=42, message="echo: single request from python"` |
|
||||
| 4 (concurrent) | `index=30+i, message="multi request {i} from python"` | Go서버 → `index=(30+i)*2, message="echo: ..."` |
|
||||
|
||||
Python서버일 때는 go클라이언트 메시지 검증:
|
||||
- 1: `index=101, message="fire from go client"`
|
||||
- 2: Python서버 → `index=200, message="push from python server"`
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/crosstest/__init__.py` 생성 (빈 파일)
|
||||
- [ ] `python/crosstest/go_python_client.py` 생성
|
||||
- [ ] argparse로 `--mode`, `--port`, `--phase` 파싱
|
||||
- [ ] `INFO typeName python=TestData` 출력
|
||||
- [ ] `dial_with_retry(mode, port)` — 3초 내 재시도
|
||||
- [ ] `run_send_push(client)` — 시나리오 1, 2
|
||||
- [ ] `run_requests(client)` — 시나리오 3, 4 (asyncio.gather로 동시 요청)
|
||||
- [ ] 비동기 main → `asyncio.run(main())`
|
||||
- [ ] `go/crosstest/go_python.go` 생성
|
||||
- [ ] `//go:build ignore` 태그
|
||||
- [ ] `goPythonTCPPort = 29390`, `goPythonWSPort = 29392`
|
||||
- [ ] `runTCPSendPush/TCPRequests/WSSendPush/WSRequests()` 4개 함수
|
||||
- [ ] `runPythonClient(mode, port, phase, expected)` — `python go_python_client.py` 실행
|
||||
- [ ] `pythonPackageDir()` — 소스 위치 기반 탐색
|
||||
- [ ] `python/crosstest/python_go.py` 생성
|
||||
- [ ] `pythonGoTCPPort = 29490`, `pythonGoWSPort = 29492`
|
||||
- [ ] `run_tcp_send_push/tcp_requests/ws_send_push/ws_requests()` 4개 함수
|
||||
- [ ] `run_go_client(mode, port, phase, expected)` — `go run ./crosstest/python_go_client` 실행
|
||||
- [ ] `go_package_dir()` — 소스 위치 기반 탐색
|
||||
- [ ] 비동기 main → `asyncio.run(main())`
|
||||
- [ ] `go/crosstest/python_go_client/main.go` 생성
|
||||
- [ ] `dart_go_client/main.go`와 동일 구조
|
||||
- [ ] Python서버 포트(`--port N`)에 연결
|
||||
- [ ] push 메시지: `"push from python server"`
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
크로스 테스트가 곧 테스트. 별도 단위 테스트 skip.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
Go 서버 / Python 클라이언트:
|
||||
```bash
|
||||
cd go && go run ./crosstest/go_python.go
|
||||
```
|
||||
|
||||
Python 서버 / Go 클라이언트:
|
||||
```bash
|
||||
cd python && python crosstest/python_go.py
|
||||
```
|
||||
|
||||
예상: 각각 `PASS all ...` 출력
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `python/pyproject.toml` | API-1 |
|
||||
| `python/toki_socket/__init__.py` | API-1 |
|
||||
| `python/toki_socket/packets/__init__.py` | API-1 |
|
||||
| `python/toki_socket/packets/message_common.proto` | API-1 |
|
||||
| `python/toki_socket/packets/message_common_pb2.py` | API-1 |
|
||||
| `python/toki_socket/packets/message_common_pb2.pyi` | API-1 |
|
||||
| `python/README.md` | API-1 |
|
||||
| `python/toki_socket/communicator.py` | API-2 |
|
||||
| `python/test/__init__.py` | API-2 |
|
||||
| `python/test/test_communicator.py` | API-2 |
|
||||
| `python/toki_socket/base_client.py` | API-3 |
|
||||
| `python/toki_socket/tcp_client.py` | API-4 |
|
||||
| `python/toki_socket/tcp_server.py` | API-4 |
|
||||
| `python/test/test_tcp.py` | API-4 |
|
||||
| `python/toki_socket/ws_client.py` | API-5 |
|
||||
| `python/toki_socket/ws_server.py` | API-5 |
|
||||
| `python/crosstest/__init__.py` | API-7 |
|
||||
| `python/crosstest/go_python_client.py` | API-7 |
|
||||
| `python/crosstest/python_go.py` | API-7 |
|
||||
| `go/crosstest/go_python.go` | API-7 |
|
||||
| `go/crosstest/python_go_client/main.go` | API-7 |
|
||||
|
||||
---
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# 1. Python 단위 테스트
|
||||
cd python && pip install -e ".[dev]" && python -m pytest test/ -v
|
||||
|
||||
# 2. Go 서버 / Python 클라이언트 크로스 테스트
|
||||
cd go && go run ./crosstest/go_python.go
|
||||
|
||||
# 3. Python 서버 / Go 클라이언트 크로스 테스트
|
||||
cd python && python crosstest/python_go.py
|
||||
|
||||
# 4. 기존 Go 크로스 테스트 회귀 확인
|
||||
cd go && go run ./crosstest/go_dart.go
|
||||
cd go && go run ./crosstest/go_kotlin.go
|
||||
```
|
||||
|
||||
예상 결과:
|
||||
1. `pytest`: 모든 테스트 PASS
|
||||
2. `go run ./crosstest/go_python.go`: `PASS all go-server/python-client crosstests passed`
|
||||
3. `python crosstest/python_go.py`: `PASS all python-server/go-client crosstests passed`
|
||||
4. 기존 크로스 테스트: 회귀 없음 (PASS)
|
||||
81
agent-task/quality_improvements/CODE_REVIEW.md
Normal file
81
agent-task/quality_improvements/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
|
||||
|
||||
# Code Review Reference - REFACTOR-DOC-FIX
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=quality_improvements, plan=1, tag=REFACTOR-DOC-FIX
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_1.log` (기존 code_review_*.log 수 = 1)
|
||||
2. `PLAN.md` → `plan_1.log` (기존 plan_*.log 수 = 1)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
계획대로 `VERSIONING.md`에서 `## nonce Overflow` 섹션 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동했습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
문서 구조만 조정했습니다. `nonce` overflow 정책 문구와 non-breaking 변경 예시 문구는 변경하지 않았습니다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `VERSIONING.md`에서 `## Non-breaking Protocol Changes` 섹션 바디(Examples 목록 + 후속 단락)가 `## nonce Overflow` **앞**에 위치하는지 확인
|
||||
- `## nonce Overflow` 섹션 내용(int32 정책 3개 항목)이 그대로 유지되는지 확인
|
||||
- `## nonce Overflow` 이후에 `## New Language Compatibility` 섹션이 이어지는지 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### DOC-FIX-1 중간 검증
|
||||
```
|
||||
$ grep -n "^##" VERSIONING.md
|
||||
5:## Current Versions
|
||||
13:## Protocol Version
|
||||
26:## Package Version
|
||||
32:## Breaking Protocol Changes
|
||||
42:## Non-breaking Protocol Changes
|
||||
53:## nonce Overflow
|
||||
64:## New Language Compatibility
|
||||
|
||||
$ grep -n "Examples of non-breaking" VERSIONING.md
|
||||
44:Examples of non-breaking changes:
|
||||
|
||||
$ grep -n "nonce Overflow" VERSIONING.md
|
||||
53:## nonce Overflow
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ grep -n "^##" VERSIONING.md
|
||||
5:## Current Versions
|
||||
13:## Protocol Version
|
||||
26:## Package Version
|
||||
32:## Breaking Protocol Changes
|
||||
42:## Non-breaking Protocol Changes
|
||||
53:## nonce Overflow
|
||||
64:## New Language Compatibility
|
||||
|
||||
$ grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
|
||||
- Adding a new protobuf message type.
|
||||
- Adding optional fields to application messages when older peers can ignore them.
|
||||
```
|
||||
111
agent-task/quality_improvements/PLAN.md
Normal file
111
agent-task/quality_improvements/PLAN.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
|
||||
|
||||
# Quality Improvements — VERSIONING.md 구조 수정
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
plan=0 리뷰에서 WARN이 발생했습니다. 단일 항목만 수정합니다.
|
||||
완료 후 `CODE_REVIEW.md` 검증 결과를 채우고 완료 표를 `[x]`로 갱신하세요.
|
||||
|
||||
## 배경
|
||||
|
||||
plan=0 코드 리뷰 결과, REFACTOR-4(VERSIONING.md)에서 `## nonce Overflow` 섹션이
|
||||
`## Non-breaking Protocol Changes` 헤더 바로 아래에 삽입되어 구조가 깨졌습니다.
|
||||
|
||||
현재 구조:
|
||||
```
|
||||
## Non-breaking Protocol Changes
|
||||
← 내용 없음
|
||||
## nonce Overflow
|
||||
...
|
||||
|
||||
Examples of non-breaking changes: ← nonce Overflow 섹션 아래에 위치 (의미상 오류)
|
||||
```
|
||||
|
||||
의도한 구조:
|
||||
```
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
...
|
||||
|
||||
Backward-compatible message additions ...
|
||||
|
||||
## nonce Overflow
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동
|
||||
|
||||
**문제**
|
||||
|
||||
`## nonce Overflow`가 `## Non-breaking Protocol Changes` 바디(Examples 목록, 후속 단락)보다 앞에 위치해
|
||||
"Examples of non-breaking changes:" 목록이 nonce Overflow 섹션에 속한 것처럼 렌더링됩니다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`## nonce Overflow` 블록 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동합니다.
|
||||
|
||||
목표 구조 (`VERSIONING.md` 하단):
|
||||
|
||||
```markdown
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
|
||||
- Adding a new protobuf message type.
|
||||
- Adding optional fields to application messages when older peers can ignore them.
|
||||
- Adding a new language implementation that passes the existing protocol and cross-language tests.
|
||||
- Tightening tests or documentation without changing wire behavior.
|
||||
|
||||
Backward-compatible message additions require updated parser maps and cross-language tests before release.
|
||||
|
||||
## nonce Overflow
|
||||
|
||||
`nonce` and `responseNonce` fields are protobuf `int32` values (signed 32-bit).
|
||||
Overflow occurs after about 2.1 billion sends on a single connection.
|
||||
|
||||
Current policy:
|
||||
|
||||
- The protocol does not define overflow behavior. Reaching the int32 limit on a single connection is not realistic.
|
||||
- Implementations do not add recovery logic for overflow.
|
||||
- If overflow handling is needed in the future, the protocol version will be bumped and wrap-around behavior will be specified.
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `VERSIONING.md` — `## nonce Overflow` 섹션을 `## Non-breaking Protocol Changes` 바디 아래로 이동
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
문서 전용 변경이므로 테스트 불필요.
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep -n "^##" VERSIONING.md
|
||||
# Non-breaking Protocol Changes가 nonce Overflow보다 먼저 나와야 하고
|
||||
# Examples of non-breaking changes: 가 Non-breaking Protocol Changes 섹션 안에 있어야 함
|
||||
grep -n "Examples of non-breaking" VERSIONING.md
|
||||
grep -n "nonce Overflow" VERSIONING.md
|
||||
# Examples 줄 번호 < nonce Overflow 줄 번호
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `VERSIONING.md` | DOC-FIX-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
grep -n "^##" VERSIONING.md
|
||||
# 섹션 순서 확인: Non-breaking Protocol Changes → nonce Overflow → New Language Compatibility
|
||||
grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
|
||||
# 바로 아래에 Examples 또는 설명이 와야 함 (nonce Overflow 아님)
|
||||
```
|
||||
135
agent-task/quality_improvements/code_review_0.log
Normal file
135
agent-task/quality_improvements/code_review_0.log
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<!-- task=quality_improvements plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=quality_improvements, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (기존 code_review_*.log 수 = 0)
|
||||
2. `PLAN.md` → `plan_0.log` (기존 plan_*.log 수 = 0)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] PROTOCOL.md 상태 표 업데이트 | [x] |
|
||||
| [REFACTOR-2] Python `_shutdown` → `shutdown` 공개 메서드 전환 | [x] |
|
||||
| [REFACTOR-3] Kotlin `nextNonce()` → `internal` 접근자 | [x] |
|
||||
| [REFACTOR-4] VERSIONING.md nonce overflow 정책 추가 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- Kotlin `nextNonce()`는 단순 `internal`로 변경 시 public inline 함수 `addRequestListenerTyped()`에서 접근할 수 없어 컴파일이 실패했다. 기존 파일의 패턴에 맞춰 `@PublishedApi internal`로 구현했다.
|
||||
- 이 실행 환경에는 `python` 명령이 없어 Python 검증은 `python3 -m pytest`로 실행했다.
|
||||
- Kotlin/Python 전체 테스트 중 로컬 소켓 바인딩이 필요한 케이스는 sandbox 권한 제한으로 실패하여 sandbox 밖에서 동일 검증을 재실행했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Python은 `_shutdown()`을 public `shutdown()`으로 이름만 전환하고 기존 종료 동작은 유지했다.
|
||||
- Kotlin은 외부 public API 노출을 줄이되 public inline 헬퍼의 컴파일 가능성을 위해 `@PublishedApi internal`을 사용했다.
|
||||
- `nonce` overflow 문서는 현재 프로토콜이 overflow를 정의하지 않으며 구현체도 복구 로직을 추가하지 않는다는 정책을 명시했다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- PROTOCOL.md 표에서 TypeScript와 Python 두 행 모두 `Available`로 변경되었는지 확인
|
||||
- `communicator.py`에서 `_shutdown` 이름이 모두 `shutdown`으로 변경됐는지 (정의 + self 호출 + `base_client.py` 외부 호출)
|
||||
- `Communicator.kt:92`의 `nextNonce` 앞에 `internal` 키워드가 있는지, 동일 파일 내 top-level 헬퍼 함수에서의 호출이 여전히 컴파일되는지
|
||||
- VERSIONING.md에 `## nonce Overflow` 섹션이 추가됐는지, 내용이 "overflow 정의 없음 + 현실적이지 않음" 정책을 명확히 기술하는지
|
||||
- `test_communicator.py`에 `test_shutdown_public_method` 추가 및 통과 여부
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ grep "TypeScript\|Python" PROTOCOL.md | grep "Planned"
|
||||
|
||||
$ grep "TypeScript\|Python" PROTOCOL.md | grep "Available"
|
||||
| TypeScript | Available | `typescript/` |
|
||||
| Python | Available | `python/` |
|
||||
```
|
||||
|
||||
### REFACTOR-2 중간 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/test_communicator.py -v
|
||||
============================= test session starts ==============================
|
||||
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0 -- /usr/bin/python3
|
||||
rootdir: /config/workspace/toki_socket/python
|
||||
configfile: pyproject.toml
|
||||
plugins: asyncio-1.3.0
|
||||
collecting ... collected 5 items
|
||||
|
||||
test/test_communicator.py::test_add_listener_exclusivity PASSED [ 20%]
|
||||
test/test_communicator.py::test_send_and_receive PASSED [ 40%]
|
||||
test/test_communicator.py::test_send_request_response PASSED [ 60%]
|
||||
test/test_communicator.py::test_shutdown PASSED [ 80%]
|
||||
test/test_communicator.py::test_shutdown_public_method PASSED [100%]
|
||||
|
||||
============================== 5 passed in 0.30s ===============================
|
||||
```
|
||||
|
||||
### REFACTOR-3 중간 검증
|
||||
```
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew test
|
||||
> Task :test
|
||||
|
||||
BUILD SUCCESSFUL in 12s
|
||||
10 actionable tasks: 1 executed, 9 up-to-date
|
||||
```
|
||||
|
||||
### REFACTOR-4 중간 검증
|
||||
```
|
||||
$ grep -A 10 "nonce Overflow" VERSIONING.md
|
||||
## nonce Overflow
|
||||
|
||||
`nonce` and `responseNonce` fields are protobuf `int32` values (signed 32-bit).
|
||||
Overflow occurs after about 2.1 billion sends on a single connection.
|
||||
|
||||
Current policy:
|
||||
|
||||
- The protocol does not define overflow behavior. Reaching the int32 limit on a single connection is not realistic.
|
||||
- Implementations do not add recovery logic for overflow.
|
||||
- If overflow handling is needed in the future, the protocol version will be bumped and wrap-around behavior will be specified.
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest -v
|
||||
============================= test session starts ==============================
|
||||
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0 -- /usr/bin/python3
|
||||
rootdir: /config/workspace/toki_socket/python
|
||||
configfile: pyproject.toml
|
||||
plugins: asyncio-1.3.0
|
||||
collecting ... collected 7 items
|
||||
|
||||
test/test_communicator.py::test_add_listener_exclusivity PASSED [ 14%]
|
||||
test/test_communicator.py::test_send_and_receive PASSED [ 28%]
|
||||
test/test_communicator.py::test_send_request_response PASSED [ 42%]
|
||||
test/test_communicator.py::test_shutdown PASSED [ 57%]
|
||||
test/test_communicator.py::test_shutdown_public_method PASSED [ 71%]
|
||||
test/test_tcp.py::test_tcp_send_receive PASSED [ 85%]
|
||||
test/test_tcp.py::test_tcp_request_response PASSED [100%]
|
||||
|
||||
============================== 7 passed in 0.26s ===============================
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew test
|
||||
> Task :test
|
||||
|
||||
BUILD SUCCESSFUL in 12s
|
||||
10 actionable tasks: 1 executed, 9 up-to-date
|
||||
$ grep "TypeScript\|Python" PROTOCOL.md | grep "Available"
|
||||
| TypeScript | Available | `typescript/` |
|
||||
| Python | Available | `python/` |
|
||||
$ grep "nonce Overflow" VERSIONING.md
|
||||
## nonce Overflow
|
||||
```
|
||||
81
agent-task/quality_improvements/code_review_1.log
Normal file
81
agent-task/quality_improvements/code_review_1.log
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
|
||||
|
||||
# Code Review Reference - REFACTOR-DOC-FIX
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=quality_improvements, plan=1, tag=REFACTOR-DOC-FIX
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_1.log` (기존 code_review_*.log 수 = 1)
|
||||
2. `PLAN.md` → `plan_1.log` (기존 plan_*.log 수 = 1)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
계획대로 `VERSIONING.md`에서 `## nonce Overflow` 섹션 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동했습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
문서 구조만 조정했습니다. `nonce` overflow 정책 문구와 non-breaking 변경 예시 문구는 변경하지 않았습니다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `VERSIONING.md`에서 `## Non-breaking Protocol Changes` 섹션 바디(Examples 목록 + 후속 단락)가 `## nonce Overflow` **앞**에 위치하는지 확인
|
||||
- `## nonce Overflow` 섹션 내용(int32 정책 3개 항목)이 그대로 유지되는지 확인
|
||||
- `## nonce Overflow` 이후에 `## New Language Compatibility` 섹션이 이어지는지 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### DOC-FIX-1 중간 검증
|
||||
```
|
||||
$ grep -n "^##" VERSIONING.md
|
||||
5:## Current Versions
|
||||
13:## Protocol Version
|
||||
26:## Package Version
|
||||
32:## Breaking Protocol Changes
|
||||
42:## Non-breaking Protocol Changes
|
||||
53:## nonce Overflow
|
||||
64:## New Language Compatibility
|
||||
|
||||
$ grep -n "Examples of non-breaking" VERSIONING.md
|
||||
44:Examples of non-breaking changes:
|
||||
|
||||
$ grep -n "nonce Overflow" VERSIONING.md
|
||||
53:## nonce Overflow
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ grep -n "^##" VERSIONING.md
|
||||
5:## Current Versions
|
||||
13:## Protocol Version
|
||||
26:## Package Version
|
||||
32:## Breaking Protocol Changes
|
||||
42:## Non-breaking Protocol Changes
|
||||
53:## nonce Overflow
|
||||
64:## New Language Compatibility
|
||||
|
||||
$ grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
|
||||
- Adding a new protobuf message type.
|
||||
- Adding optional fields to application messages when older peers can ignore them.
|
||||
```
|
||||
24
agent-task/quality_improvements/complete.log
Normal file
24
agent-task/quality_improvements/complete.log
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
task=quality_improvements
|
||||
date=2026-04-24
|
||||
result=PASS
|
||||
|
||||
## 완료된 작업 (plan=0)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| REFACTOR-1 | PROTOCOL.md — TypeScript·Python 상태를 `Available`로 업데이트 |
|
||||
| REFACTOR-2 | Python `Communicator._shutdown` → `shutdown` 공개 메서드 전환 + `test_shutdown_public_method` 추가 |
|
||||
| REFACTOR-3 | Kotlin `nextNonce()` → `@PublishedApi internal` (public inline 헬퍼 컴파일 유지) |
|
||||
| REFACTOR-4 | VERSIONING.md에 `## nonce Overflow` 정책 섹션 추가 |
|
||||
|
||||
## 수정 사항 (plan=1)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| DOC-FIX-1 | VERSIONING.md — `## nonce Overflow` 섹션을 `## Non-breaking Protocol Changes` 바디 뒤로 이동 (구조 오류 수정) |
|
||||
|
||||
## 최종 상태
|
||||
|
||||
- Python 테스트: 7/7 PASSED
|
||||
- Kotlin 빌드: BUILD SUCCESSFUL
|
||||
- VERSIONING.md 섹션 순서: Non-breaking Protocol Changes → nonce Overflow → New Language Compatibility
|
||||
237
agent-task/quality_improvements/plan_0.log
Normal file
237
agent-task/quality_improvements/plan_0.log
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<!-- task=quality_improvements plan=0 tag=REFACTOR -->
|
||||
|
||||
# Quality Improvements — 구현 품질 정리
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료로 표시하고, 중간 검증 명령을 실제로 실행한 뒤 출력을 `CODE_REVIEW.md`의 `검증 결과` 섹션에 붙여넣으세요. 최종 검증까지 모두 통과한 후 `CODE_REVIEW.md` 완성 여부 표를 `[x]`로 채우세요.
|
||||
|
||||
## 배경
|
||||
|
||||
코드베이스 전반 평가에서 발견된 사소하지만 일관성을 깨는 네 가지 문제를 수정합니다. PROTOCOL.md 상태 표가 이미 완료된 TypeScript·Python을 Planned로 표기하고 있고, Python `Communicator._shutdown`이 외부에서 private 메서드로 직접 호출되며, Kotlin `nextNonce()`가 불필요하게 public으로 노출됩니다. 또한 VERSIONING.md에 `nonce` int32 overflow 정책이 빠져있습니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
REFACTOR-2(Python) → REFACTOR-3(Kotlin) → REFACTOR-1(PROTOCOL.md) → REFACTOR-4(VERSIONING.md) 순서로 진행합니다. 코드 변경 후 문서 업데이트.
|
||||
|
||||
---
|
||||
|
||||
### [REFACTOR-1] PROTOCOL.md 구현 상태 표 업데이트
|
||||
|
||||
**문제**
|
||||
|
||||
[PROTOCOL.md:183-184](../PROTOCOL.md) — TypeScript와 Python이 "Planned"로 기재되어 있으나 두 언어 모두 구현 완료 상태입니다.
|
||||
|
||||
```markdown
|
||||
| TypeScript | Planned | `typescript/` | ← 183번 줄
|
||||
| Python | Planned | `python/` | ← 184번 줄
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
두 행의 `Planned` → `Available`로 교체합니다.
|
||||
|
||||
```markdown
|
||||
| TypeScript | Available | `typescript/` |
|
||||
| Python | Available | `python/` |
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `PROTOCOL.md` — 183번 줄 `TypeScript | Planned` → `TypeScript | Available`
|
||||
- [x] `PROTOCOL.md` — 184번 줄 `Python | Planned` → `Python | Available`
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
문서 전용 변경이므로 테스트 불필요.
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep "TypeScript\|Python" PROTOCOL.md | grep "Planned"
|
||||
# 출력 없음이 정상
|
||||
grep "TypeScript\|Python" PROTOCOL.md | grep "Available"
|
||||
# TypeScript | Available, Python | Available 두 줄 출력
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [REFACTOR-2] Python `_shutdown` → `shutdown` 공개 메서드 전환
|
||||
|
||||
**문제**
|
||||
|
||||
[python/toki_socket/base_client.py:51](../python/toki_socket/base_client.py) — `BaseClient.close()`가 `Communicator`의 private 메서드를 직접 호출합니다.
|
||||
|
||||
```python
|
||||
self.communicator._shutdown() # ← 51번 줄, private 접근
|
||||
```
|
||||
|
||||
Go/TypeScript/Kotlin은 모두 `shutdown()` public 메서드를 노출합니다. Python만 `_shutdown()` 형태여서 API 일관성이 깨집니다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`communicator.py`에서 `_shutdown` → `shutdown`으로 이름을 바꾸고, `close()` 내부 self 호출도 함께 수정합니다. `base_client.py`도 맞춰 업데이트합니다.
|
||||
|
||||
Before (`communicator.py:67`):
|
||||
```python
|
||||
def _shutdown(self) -> None:
|
||||
```
|
||||
|
||||
After:
|
||||
```python
|
||||
def shutdown(self) -> None:
|
||||
```
|
||||
|
||||
Before (`communicator.py:83`):
|
||||
```python
|
||||
self._shutdown()
|
||||
```
|
||||
|
||||
After:
|
||||
```python
|
||||
self.shutdown()
|
||||
```
|
||||
|
||||
Before (`base_client.py:51`):
|
||||
```python
|
||||
self.communicator._shutdown()
|
||||
```
|
||||
|
||||
After:
|
||||
```python
|
||||
self.communicator.shutdown()
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `python/toki_socket/communicator.py:67` — `def _shutdown` → `def shutdown`
|
||||
- [x] `python/toki_socket/communicator.py:83` — `self._shutdown()` → `self.shutdown()`
|
||||
- [x] `python/toki_socket/base_client.py:51` — `self.communicator._shutdown()` → `self.communicator.shutdown()`
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
기존 [python/test/test_communicator.py:94](../python/test/test_communicator.py) `test_shutdown()`은 `communicator.close()`를 통해 간접 검증합니다. 직접 `shutdown()`을 호출하는 케이스가 없으므로, public 메서드를 직접 검증하는 테스트를 한 건 추가합니다.
|
||||
|
||||
- 파일: `python/test/test_communicator.py`
|
||||
- 테스트명: `test_shutdown_public_method`
|
||||
- 단언 목표: `communicator.shutdown()` 직접 호출 후 `is_alive() == False`
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
cd python && python -m pytest test/test_communicator.py -v
|
||||
# PASSED test_shutdown_public_method 포함 전체 통과
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [REFACTOR-3] Kotlin `nextNonce()` 접근자를 `internal`로 좁히기
|
||||
|
||||
**문제**
|
||||
|
||||
[kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt:92](../kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt) — `nextNonce()`가 public으로 선언됩니다.
|
||||
|
||||
```kotlin
|
||||
fun nextNonce(): Int = nonce.incrementAndGet() // ← 92번 줄
|
||||
```
|
||||
|
||||
Go는 소문자(`nextNonce`) 패키지 전용, TypeScript는 `private nextNonce()`. Kotlin만 외부에서 nonce를 증가시킬 수 있어 의도치 않은 상태 오염이 가능합니다.
|
||||
|
||||
`addRequestListenerTyped`와 `sendRequestTyped`는 같은 파일 내 top-level 함수로, `internal`이면 접근 가능합니다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
Before (`Communicator.kt:92`):
|
||||
```kotlin
|
||||
fun nextNonce(): Int = nonce.incrementAndGet()
|
||||
```
|
||||
|
||||
After:
|
||||
```kotlin
|
||||
internal fun nextNonce(): Int = nonce.incrementAndGet()
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt:92` — `fun nextNonce` → `internal fun nextNonce`
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
Kotlin 테스트(`src/test/`)는 `nextNonce()`를 직접 호출하지 않으므로 테스트 변경 불필요. `internal` 키워드는 같은 모듈 내 테스트에서는 계속 접근 가능하므로 기존 테스트 깨짐 없음.
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew test
|
||||
# BUILD SUCCESSFUL, 0 failures
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [REFACTOR-4] VERSIONING.md에 nonce overflow 정책 추가
|
||||
|
||||
**문제**
|
||||
|
||||
[VERSIONING.md](../VERSIONING.md) — `nonce`는 `int32` 타입으로 ~21억 메시지 후 overflow합니다. 현재 문서에 overflow 시 동작이 명시되지 않아, 새 언어 구현자가 wrapping·연결 재시작 여부를 독자적으로 판단해야 합니다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`VERSIONING.md`의 "Non-breaking Protocol Changes" 섹션 바로 아래에 `## nonce Overflow` 항목을 추가합니다.
|
||||
|
||||
```markdown
|
||||
## nonce Overflow
|
||||
|
||||
`nonce`와 `responseNonce` 필드는 protobuf `int32`(부호 있는 32비트)입니다.
|
||||
단일 연결에서 약 21억 회 송신 후 overflow가 발생합니다.
|
||||
|
||||
현재 정책:
|
||||
|
||||
- 프로토콜은 overflow를 정의하지 않는다. 단일 연결에서 int32 한계에 도달하는 경우는 현실적이지 않다.
|
||||
- 구현체는 overflow에 대한 복구 논리를 추가하지 않는다.
|
||||
- 만약 미래에 overflow 처리가 필요해지면 프로토콜 버전을 올리고 wrap-around 여부를 명시한다.
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `VERSIONING.md` — "Non-breaking Protocol Changes" 섹션 아래에 `## nonce Overflow` 항목 추가
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
문서 전용 변경이므로 테스트 불필요.
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep -A 10 "nonce Overflow" VERSIONING.md
|
||||
# 추가된 섹션 출력
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `PROTOCOL.md` | REFACTOR-1 |
|
||||
| `python/toki_socket/communicator.py` | REFACTOR-2 |
|
||||
| `python/toki_socket/base_client.py` | REFACTOR-2 |
|
||||
| `python/test/test_communicator.py` | REFACTOR-2 |
|
||||
| `kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt` | REFACTOR-3 |
|
||||
| `VERSIONING.md` | REFACTOR-4 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# Python 전체 테스트
|
||||
cd python && python -m pytest -v
|
||||
# 모든 테스트 PASSED
|
||||
|
||||
# Kotlin 전체 테스트
|
||||
cd kotlin && ./gradlew test
|
||||
# BUILD SUCCESSFUL
|
||||
|
||||
# 문서 확인
|
||||
grep "TypeScript\|Python" PROTOCOL.md | grep "Available"
|
||||
grep "nonce Overflow" VERSIONING.md
|
||||
# 두 항목 모두 출력
|
||||
```
|
||||
111
agent-task/quality_improvements/plan_1.log
Normal file
111
agent-task/quality_improvements/plan_1.log
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
|
||||
|
||||
# Quality Improvements — VERSIONING.md 구조 수정
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
plan=0 리뷰에서 WARN이 발생했습니다. 단일 항목만 수정합니다.
|
||||
완료 후 `CODE_REVIEW.md` 검증 결과를 채우고 완료 표를 `[x]`로 갱신하세요.
|
||||
|
||||
## 배경
|
||||
|
||||
plan=0 코드 리뷰 결과, REFACTOR-4(VERSIONING.md)에서 `## nonce Overflow` 섹션이
|
||||
`## Non-breaking Protocol Changes` 헤더 바로 아래에 삽입되어 구조가 깨졌습니다.
|
||||
|
||||
현재 구조:
|
||||
```
|
||||
## Non-breaking Protocol Changes
|
||||
← 내용 없음
|
||||
## nonce Overflow
|
||||
...
|
||||
|
||||
Examples of non-breaking changes: ← nonce Overflow 섹션 아래에 위치 (의미상 오류)
|
||||
```
|
||||
|
||||
의도한 구조:
|
||||
```
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
...
|
||||
|
||||
Backward-compatible message additions ...
|
||||
|
||||
## nonce Overflow
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동
|
||||
|
||||
**문제**
|
||||
|
||||
`## nonce Overflow`가 `## Non-breaking Protocol Changes` 바디(Examples 목록, 후속 단락)보다 앞에 위치해
|
||||
"Examples of non-breaking changes:" 목록이 nonce Overflow 섹션에 속한 것처럼 렌더링됩니다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`## nonce Overflow` 블록 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동합니다.
|
||||
|
||||
목표 구조 (`VERSIONING.md` 하단):
|
||||
|
||||
```markdown
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
|
||||
- Adding a new protobuf message type.
|
||||
- Adding optional fields to application messages when older peers can ignore them.
|
||||
- Adding a new language implementation that passes the existing protocol and cross-language tests.
|
||||
- Tightening tests or documentation without changing wire behavior.
|
||||
|
||||
Backward-compatible message additions require updated parser maps and cross-language tests before release.
|
||||
|
||||
## nonce Overflow
|
||||
|
||||
`nonce` and `responseNonce` fields are protobuf `int32` values (signed 32-bit).
|
||||
Overflow occurs after about 2.1 billion sends on a single connection.
|
||||
|
||||
Current policy:
|
||||
|
||||
- The protocol does not define overflow behavior. Reaching the int32 limit on a single connection is not realistic.
|
||||
- Implementations do not add recovery logic for overflow.
|
||||
- If overflow handling is needed in the future, the protocol version will be bumped and wrap-around behavior will be specified.
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `VERSIONING.md` — `## nonce Overflow` 섹션을 `## Non-breaking Protocol Changes` 바디 아래로 이동
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
문서 전용 변경이므로 테스트 불필요.
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep -n "^##" VERSIONING.md
|
||||
# Non-breaking Protocol Changes가 nonce Overflow보다 먼저 나와야 하고
|
||||
# Examples of non-breaking changes: 가 Non-breaking Protocol Changes 섹션 안에 있어야 함
|
||||
grep -n "Examples of non-breaking" VERSIONING.md
|
||||
grep -n "nonce Overflow" VERSIONING.md
|
||||
# Examples 줄 번호 < nonce Overflow 줄 번호
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `VERSIONING.md` | DOC-FIX-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
grep -n "^##" VERSIONING.md
|
||||
# 섹션 순서 확인: Non-breaking Protocol Changes → nonce Overflow → New Language Compatibility
|
||||
grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
|
||||
# 바로 아래에 Examples 또는 설명이 와야 함 (nonce Overflow 아님)
|
||||
```
|
||||
|
|
@ -55,11 +55,7 @@ abstract class Communicator {
|
|||
|
||||
/// Serializes writes so stream transports do not interleave packets.
|
||||
Future<void> queuePacket(PacketBase base) {
|
||||
final transport = _transport;
|
||||
if (transport == null) {
|
||||
return Future.error(StateError('transport is not initialized'));
|
||||
}
|
||||
final write = _outboundWrite.then((_) => transport.writePacket(base));
|
||||
final write = _outboundWrite.then((_) => _transport!.writePacket(base));
|
||||
_outboundWrite = write.catchError((_) {});
|
||||
return write;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ class Communicator(
|
|||
rwLock.write { writeErrorHandler = fn }
|
||||
}
|
||||
|
||||
fun nextNonce(): Int = nonce.incrementAndGet()
|
||||
@PublishedApi
|
||||
internal fun nextNonce(): Int = nonce.incrementAndGet()
|
||||
|
||||
fun shutdown() {
|
||||
if (!shutdownStarted.compareAndSet(false, true)) return
|
||||
|
|
|
|||
|
|
@ -99,3 +99,15 @@ async def test_shutdown():
|
|||
|
||||
assert not communicator.is_alive()
|
||||
assert transport.closed
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown_public_method():
|
||||
transport = FakeTransport()
|
||||
communicator = Communicator()
|
||||
communicator.initialize(transport, parser_map())
|
||||
communicator.shutdown()
|
||||
await communicator.wait_closed()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert not communicator.is_alive()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class BaseClient:
|
|||
return
|
||||
self._close_called = True
|
||||
|
||||
self.communicator._shutdown()
|
||||
self.communicator.shutdown()
|
||||
self._stop_heartbeat()
|
||||
await self._do_close()
|
||||
await self._notify_disconnected()
|
||||
|
|
@ -101,4 +101,3 @@ class BaseClient:
|
|||
if self._hb_timer is not None:
|
||||
self._hb_timer.cancel()
|
||||
self._hb_timer = None
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class Communicator:
|
|||
self._nonce += 1
|
||||
return self._nonce
|
||||
|
||||
def _shutdown(self) -> None:
|
||||
def shutdown(self) -> None:
|
||||
if not self._is_alive and self._closed.is_set():
|
||||
return
|
||||
self._is_alive = False
|
||||
|
|
@ -80,7 +80,7 @@ class Communicator:
|
|||
self._write_loop_task.cancel()
|
||||
|
||||
async def close(self) -> None:
|
||||
self._shutdown()
|
||||
self.shutdown()
|
||||
if self._transport is not None:
|
||||
await self._transport.close()
|
||||
|
||||
|
|
@ -290,4 +290,3 @@ def _accepts_nonce(handler: RequestHandler) -> bool:
|
|||
in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
||||
]
|
||||
return len(positional) >= 2
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue