proto-socket/agent-task/api_consistency/PLAN.md

7.5 KiB

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.dartsendRequest에 timeout 파라미터가 없다.

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:

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:

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:asyncTimeoutException을 사용한다 (import 'dart:async'는 이미 있음).

수정 파일 및 체크리스트

  • dart/lib/src/communicator.dartsendRequest{Duration timeout = const Duration(seconds: 30)} named 파라미터 추가
  • dart/lib/src/communicator.dartcompleter.future.timeout(timeout, onTimeout: ...) 적용, pending 정리 포함
  • dart/test/communicator_test.dartsendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다 테스트 추가

테스트 작성

  • 파일: dart/test/communicator_test.dart
  • 테스트명: sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다
  • 시나리오: FakeTransport에 packet을 보내지만 응답을 보내지 않음, Duration(milliseconds: 50) timeout으로 호출, throwsA(isA<TimeoutException>()) 단언, timeout 후 _pendingRequests가 비어있는지 확인

중간 검증

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 — write queue가 크기 제한 없는 Array다.

private writeQueue: QueueItem[] = [];

Go/Kotlin/Python은 64개 제한 도달 시 큐에 빈 자리가 생길 때까지 발신을 블로킹한다. TypeScript는 await하지 않고 대량 send()를 발행하면 큐가 무한 증가한다.

해결 방법

MAX_WRITE_QUEUE_SIZE = 64 상수를 추가한다. 큐가 가득 찼을 때 waiter Promise를 등록하고, 항목이 소비될 때마다 waiter를 깨운다 (Python의 asyncio.Queue와 동일한 시맨틱). shutdown() 시 모든 waiter를 깨워 NotConnectedError로 종료한다.

추가할 멤버:

private static readonly MAX_WRITE_QUEUE_SIZE = 64;
private writeQueueWaiters: Array<() => void> = [];

queuePacket 변경:

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 해제:

const item = this.writeQueue.shift();
if (this.writeQueueWaiters.length > 0) {
  this.writeQueueWaiters.shift()?.();
}

shutdown()에서 waiters 플러시:

while (this.writeQueueWaiters.length > 0) {
  this.writeQueueWaiters.shift()?.();
}

initialize()에서 writeQueueWaiters 초기화 추가:

this.writeQueueWaiters = [];

수정 파일 및 체크리스트

  • typescript/src/communicator.tsMAX_WRITE_QUEUE_SIZE = 64 상수 추가
  • typescript/src/communicator.tswriteQueueWaiters 멤버 추가 및 initialize() 초기화
  • typescript/src/communicator.tsqueuePacket()에 full 시 대기 로직 추가
  • typescript/src/communicator.tsrunWriteQueue()에서 waiter 해제
  • typescript/src/communicator.tsshutdown()에서 waiters 플러시
  • typescript/test/communicator.test.ts — 큐 full 시 대기 후 처리 테스트 추가

테스트 작성

  • 파일: typescript/test/communicator.test.ts
  • 테스트명: write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다
  • 시나리오: FakeTransport에서 write를 인위적으로 지연, 65번째 queuePacket 호출이 블로킹되다가 64번째가 처리되면 진행됨을 확인

중간 검증

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

최종 검증

# 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 (신규 큐 테스트 포함)