proto-socket/agent-task/python_impl/CODE_REVIEW.md
toki fa273bcc26 feat: add Python implementation and cross-test support
- Add Python toki_socket package (base_client, communicator, tcp/ws client/server)
- Add protobuf message definitions for Python
- Add go_python.go cross-test implementation and python_go_client
- Add agent-task/python_impl plan and code review docs
- Update .gitignore for Python cache files with recursive patterns
2026-04-23 16:44:43 +09:00

159 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 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
```