proto-socket/agent-task/api_consistency/plan_0.log

225 lines
7.5 KiB
Text

<!-- task=api_consistency plan=0 tag=IMPROVE -->
# API 일관성 개선
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
---
## 배경
프로젝트 전반 평가에서 발견된 두 가지 API 비일관성을 수정한다.
| 항목 | 문제 | 영향 |
|------|------|------|
| IMPROVE-1 | Dart `sendRequest`에 timeout 파라미터 없음 | 응답 없으면 무한 대기 |
| IMPROVE-2 | TypeScript write queue 크기 제한 없음 | await 없이 대량 send 시 무한 증가 가능 |
Go/Kotlin/Python/TypeScript는 `sendRequest`에 timeout을 지원한다. Dart만 없다.
Go/Kotlin/Python은 write queue를 64개로 제한한다. TypeScript와 Dart는 제한이 없다.
Dart write queue는 Promise chaining 방식이라 구조상 추가가 어려우므로 이번 범위에서 제외한다.
## 의존 관계 및 구현 순서
IMPROVE-1과 IMPROVE-2는 독립적이다. 이 순서로 진행한다.
---
### [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가
**문제**
[dart/lib/src/communicator.dart](../../dart/lib/src/communicator.dart) — `sendRequest`에 timeout 파라미터가 없다.
```dart
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data) async {
```
다른 언어 비교:
| 언어 | timeout 기본값 |
|------|--------------|
| Go | `time.Duration`, 기본 30s |
| Kotlin | `timeoutMs: Long = 30_000L` |
| Python | `timeout: float = 30.0` |
| TypeScript | `timeoutMs = 30000` |
| **Dart** | **없음 — 무한 대기** |
**해결 방법**
`sendRequest`에 named 파라미터 `{Duration timeout = const Duration(seconds: 30)}`를 추가한다.
응답 대기를 `completer.future.timeout(timeout, onTimeout: ...)` 으로 감싼다.
timeout 발생 시 `_pendingRequests`에서 해당 nonce를 제거하고 `TimeoutException`을 전파한다.
Before:
```dart
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data) async {
if (!isAlive) return Future.error(StateError('not connected'));
final requestNonce = ++nonce;
// ...
return completer.future;
}
```
After:
```dart
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data, {Duration timeout = const Duration(seconds: 30)}) async {
if (!isAlive) return Future.error(StateError('not connected'));
final requestNonce = ++nonce;
// ...
return completer.future.timeout(timeout, onTimeout: () {
_pendingRequests.remove(requestNonce);
throw TimeoutException(
'sendRequest timeout for nonce $requestNonce', timeout);
});
}
```
`dart:async`의 `TimeoutException`을 사용한다 (`import 'dart:async'`는 이미 있음).
**수정 파일 및 체크리스트**
- [x] `dart/lib/src/communicator.dart` — `sendRequest`에 `{Duration timeout = const Duration(seconds: 30)}` named 파라미터 추가
- [x] `dart/lib/src/communicator.dart` — `completer.future.timeout(timeout, onTimeout: ...)` 적용, pending 정리 포함
- [x] `dart/test/communicator_test.dart` — `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다` 테스트 추가
**테스트 작성**
- 파일: `dart/test/communicator_test.dart`
- 테스트명: `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다`
- 시나리오: `FakeTransport`에 packet을 보내지만 응답을 보내지 않음, `Duration(milliseconds: 50)` timeout으로 호출, `throwsA(isA<TimeoutException>())` 단언, timeout 후 `_pendingRequests`가 비어있는지 확인
**중간 검증**
```bash
cd dart && dart test test/communicator_test.dart
# 새 timeout 테스트 포함 전체 PASS
cd dart && dart analyze
# No issues found
```
---
### [IMPROVE-2] TypeScript write queue maxsize 제한
**문제**
[typescript/src/communicator.ts](../../typescript/src/communicator.ts) — write queue가 크기 제한 없는 `Array`다.
```typescript
private writeQueue: QueueItem[] = [];
```
Go/Kotlin/Python은 64개 제한 도달 시 큐에 빈 자리가 생길 때까지 발신을 블로킹한다.
TypeScript는 `await`하지 않고 대량 `send()`를 발행하면 큐가 무한 증가한다.
**해결 방법**
`MAX_WRITE_QUEUE_SIZE = 64` 상수를 추가한다.
큐가 가득 찼을 때 waiter Promise를 등록하고, 항목이 소비될 때마다 waiter를 깨운다 (Python의 `asyncio.Queue`와 동일한 시맨틱).
`shutdown()` 시 모든 waiter를 깨워 `NotConnectedError`로 종료한다.
추가할 멤버:
```typescript
private static readonly MAX_WRITE_QUEUE_SIZE = 64;
private writeQueueWaiters: Array<() => void> = [];
```
`queuePacket` 변경:
```typescript
async queuePacket(base: PacketBase): Promise<void> {
if (!this.isAliveFlag || this.transport === null) {
throw new NotConnectedError();
}
// 큐가 가득 찬 경우 빈 자리가 생길 때까지 대기
while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) {
await new Promise<void>((resolve) => {
this.writeQueueWaiters.push(resolve);
});
if (!this.isAliveFlag) throw new NotConnectedError();
}
// 이하 기존 코드 동일
...
}
```
`runWriteQueue`에서 항목 소비 후 waiter 해제:
```typescript
const item = this.writeQueue.shift();
if (this.writeQueueWaiters.length > 0) {
this.writeQueueWaiters.shift()?.();
}
```
`shutdown()`에서 waiters 플러시:
```typescript
while (this.writeQueueWaiters.length > 0) {
this.writeQueueWaiters.shift()?.();
}
```
`initialize()`에서 `writeQueueWaiters` 초기화 추가:
```typescript
this.writeQueueWaiters = [];
```
**수정 파일 및 체크리스트**
- [x] `typescript/src/communicator.ts` — `MAX_WRITE_QUEUE_SIZE = 64` 상수 추가
- [x] `typescript/src/communicator.ts` — `writeQueueWaiters` 멤버 추가 및 `initialize()` 초기화
- [x] `typescript/src/communicator.ts` — `queuePacket()`에 full 시 대기 로직 추가
- [x] `typescript/src/communicator.ts` — `runWriteQueue()`에서 waiter 해제
- [x] `typescript/src/communicator.ts` — `shutdown()`에서 waiters 플러시
- [x] `typescript/test/communicator.test.ts` — 큐 full 시 대기 후 처리 테스트 추가
**테스트 작성**
- 파일: `typescript/test/communicator.test.ts`
- 테스트명: `write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다`
- 시나리오: FakeTransport에서 write를 인위적으로 지연, 65번째 `queuePacket` 호출이 블로킹되다가 64번째가 처리되면 진행됨을 확인
**중간 검증**
```bash
cd typescript && npx tsc --noEmit
# 타입 오류 없음
cd typescript && npx vitest run test/communicator.test.ts
# 새 큐 테스트 포함 PASS
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/lib/src/communicator.dart` | IMPROVE-1 |
| `dart/test/communicator_test.dart` | IMPROVE-1 |
| `typescript/src/communicator.ts` | IMPROVE-2 |
| `typescript/test/communicator.test.ts` | IMPROVE-2 |
## 최종 검증
```bash
# Dart
cd dart && dart analyze
# No issues found
cd dart && dart test
# 전체 PASS (신규 timeout 테스트 포함)
# TypeScript
cd typescript && npx tsc --noEmit
# 타입 오류 없음
cd typescript && npx vitest run
# 전체 PASS (신규 큐 테스트 포함)
```