proto-socket/agent-task/archive/2026/05/nonce_overflow_policy/plan_0.log

350 lines
16 KiB
Text

<!-- task=nonce_overflow_policy plan=0 tag=API -->
# Nonce Overflow Policy Plan
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하고, 항목별 중간 검증과 최종 검증을 실제로 실행하세요. 구현 후 `CODE_REVIEW.md`의 완료 여부, 계획 대비 변경 사항, 주요 설계 결정, 검증 결과를 실제 구현 내용과 명령 출력으로 모두 채우세요. 이 계획은 프로토콜 동작 변경을 포함하므로 문서와 모든 사용 가능 언어 구현이 같은 정책을 따르는지 끝까지 대조해야 합니다.
## 배경
현재 `nonce`는 송신마다 증가하지만 overflow 정책이 명세에 정의되어 있지 않습니다. `PacketBase.nonce`와 `responseNonce`는 protobuf `int32`라서 모든 언어에서 공통으로 안전하게 통용되는 상한은 signed 32-bit max인 `2,147,483,647`입니다. 단, 현재 프로토콜은 `responseNonce == 0`을 "응답 아님"으로 사용하므로 실제 송신 `nonce`로 0을 내보내면 request-response 상관관계가 깨질 수 있습니다. 따라서 구현 정책은 공통 상한 도달 후 내부 카운터를 0으로 리셋하고, 다음 송신 nonce는 다시 1부터 재사용하는 방식으로 명시합니다.
## [API-1] 프로토콜 문서에 공통 nonce overflow 정책 정의
### 문제
[PROTOCOL.md:127](/config/workspace/toki_socket/PROTOCOL.md:127)의 `nonce` 섹션은 시작값과 증가 규칙만 설명하고 overflow 동작을 정의하지 않습니다. [VERSIONING.md:53](/config/workspace/toki_socket/VERSIONING.md:53)는 overflow 정책이 없다고 명시하므로 구현체들이 각 언어의 정수 overflow 특성에 따라 달라질 수 있습니다.
Before:
```markdown
PROTOCOL.md:127
## nonce
- Starts at `1` per connection
- Increments by `1` on every send call (including requests and responses)
- Used for request-response correlation via `responseNonce`
```
```markdown
VERSIONING.md:53
## 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.
```
### 해결 방법
`nonce` 상한을 `INT32_MAX = 2,147,483,647`로 문서화합니다. 정책은 "마지막으로 발급한 nonce가 `INT32_MAX`이면 내부 카운터를 0으로 되돌린 뒤 다음 발급값은 1"로 적습니다. `responseNonce == 0` 예약 의미를 유지하기 위해 `PacketBase.nonce` 역시 0을 송신하지 않는다고 명시합니다.
After:
```markdown
## nonce
- Starts at `1` per connection
- Increments by `1` on every send call (including requests and responses)
- Maximum nonce is `2,147,483,647` (`int32` max), the lowest common signed integer max across supported protocol fields
- After issuing `2,147,483,647`, implementations reset the internal counter to `0`; the next emitted packet nonce is `1`
- `0` is reserved and must not be emitted as `PacketBase.nonce`, because `responseNonce == 0` means "not a response"
- Used for request-response correlation via `responseNonce`
```
### 수정 파일 및 체크리스트
- [ ] [PROTOCOL.md](/config/workspace/toki_socket/PROTOCOL.md): `nonce` 섹션에 `INT32_MAX` 상한, wrap 정책, `0` 예약 규칙 추가
- [ ] [VERSIONING.md](/config/workspace/toki_socket/VERSIONING.md): `nonce Overflow` 섹션을 "정책 없음"에서 "int32 max 기준 wrap"으로 갱신
- [ ] [VERSIONING.md](/config/workspace/toki_socket/VERSIONING.md): 이 변경이 `nonce` semantics 변경이므로 호환성/테스트 필요성을 문서와 맞춤
### 테스트 작성
문서 변경 자체에는 별도 테스트를 작성하지 않습니다. 대신 API-2에서 모든 구현체의 boundary 테스트가 문서 정책을 검증합니다.
### 중간 검증
```bash
rg -n "nonce Overflow|2,147,483,647|INT32|int32 max|responseNonce == 0|PacketBase.nonce" PROTOCOL.md VERSIONING.md
```
기대 결과: 두 문서에서 공통 `int32` max, wrap 정책, `0` 예약 설명이 검색됩니다.
## [API-2] 모든 구현체의 nonce 발급 로직을 공통 int32 max wrap 정책으로 통일
### 문제
각 구현체는 현재 단순 증가만 수행합니다. Dart는 [dart/lib/src/communicator.dart:88](/config/workspace/toki_socket/dart/lib/src/communicator.dart:88), [dart/lib/src/communicator.dart:138](/config/workspace/toki_socket/dart/lib/src/communicator.dart:138), [dart/lib/src/heartbeat_mixin.dart:53](/config/workspace/toki_socket/dart/lib/src/heartbeat_mixin.dart:53)에서 `++nonce`를 직접 사용합니다. Go/Kotlin/Python/TypeScript는 각각 `nextNonce()`가 있지만 overflow 경계 처리가 없습니다.
Before:
```dart
dart/lib/src/communicator.dart:88
final requestNonce = ++nonce;
```
```dart
dart/lib/src/heartbeat_mixin.dart:51
await queuePacket(PacketBase()
..typeName = data.info_.qualifiedMessageName
..nonce = ++nonce
..data = data.writeToBuffer());
```
```go
go/communicator.go:90
func (c *Communicator) nextNonce() int32 {
return c.nonce.Add(1)
}
```
```kotlin
kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt:92
@PublishedApi
internal fun nextNonce(): Int = nonce.incrementAndGet()
```
```python
python/toki_socket/communicator.py:63
def next_nonce(self) -> int:
self._nonce += 1
return self._nonce
```
```typescript
typescript/src/communicator.ts:111
private nextNonce(): number {
this.nonce += 1;
return this.nonce;
}
```
### 해결 방법
모든 언어에 공통 상수 `2147483647`을 두고, `nextNonce` 계열 함수 하나로만 nonce를 발급하게 만듭니다. 발급 로직은 현재 값이 `INT32_MAX` 이상이면 내부 값을 0으로 리셋한 뒤 1을 반환하고, 그 외에는 `current + 1`을 반환합니다. Go와 Kotlin은 동시 송신에서 중복 nonce가 나오지 않도록 CAS loop를 사용합니다.
After 예시:
```dart
static const int maxNonce = 2147483647;
@protected
int nextNonce() {
if (_nonce >= maxNonce) {
_nonce = 0;
}
_nonce += 1;
return _nonce;
}
```
```go
const MaxNonce int32 = 1<<31 - 1
func (c *Communicator) nextNonce() int32 {
for {
current := c.nonce.Load()
next := current + 1
if current >= MaxNonce {
next = 1
}
if c.nonce.CompareAndSwap(current, next) {
return next
}
}
}
```
```kotlin
internal const val MAX_NONCE = Int.MAX_VALUE
@PublishedApi
internal fun nextNonce(): Int {
while (true) {
val current = nonce.get()
val next = if (current >= MAX_NONCE) 1 else current + 1
if (nonce.compareAndSet(current, next)) return next
}
}
```
```python
MAX_NONCE = 2_147_483_647
def next_nonce(self) -> int:
if self._nonce >= MAX_NONCE:
self._nonce = 0
self._nonce += 1
return self._nonce
```
```typescript
private static readonly MAX_NONCE = 2_147_483_647;
private nextNonce(): number {
if (this.nonce >= Communicator.MAX_NONCE) {
this.nonce = 0;
}
this.nonce += 1;
return this.nonce;
}
```
### 수정 파일 및 체크리스트
- [ ] [dart/lib/src/communicator.dart](/config/workspace/toki_socket/dart/lib/src/communicator.dart): `maxNonce`와 protected `nextNonce()` 추가
- [ ] [dart/lib/src/communicator.dart](/config/workspace/toki_socket/dart/lib/src/communicator.dart): `sendRequest`와 request response 발급 지점의 `++nonce`를 `nextNonce()`로 교체
- [ ] [dart/lib/src/heartbeat_mixin.dart](/config/workspace/toki_socket/dart/lib/src/heartbeat_mixin.dart): `send`의 `++nonce`를 `nextNonce()`로 교체
- [ ] [go/communicator.go](/config/workspace/toki_socket/go/communicator.go): `MaxNonce int32 = 1<<31 - 1` 추가
- [ ] [go/communicator.go](/config/workspace/toki_socket/go/communicator.go): `nextNonce()`를 `atomic.Int32.CompareAndSwap` loop로 변경
- [ ] [kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt](/config/workspace/toki_socket/kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt): `MAX_NONCE = Int.MAX_VALUE` 추가
- [ ] [kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt](/config/workspace/toki_socket/kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt): `nextNonce()`를 `AtomicInteger.compareAndSet` loop로 변경
- [ ] [python/toki_socket/communicator.py](/config/workspace/toki_socket/python/toki_socket/communicator.py): `MAX_NONCE = 2_147_483_647` 추가
- [ ] [python/toki_socket/communicator.py](/config/workspace/toki_socket/python/toki_socket/communicator.py): `next_nonce()`에 wrap 처리 추가
- [ ] [typescript/src/communicator.ts](/config/workspace/toki_socket/typescript/src/communicator.ts): `MAX_NONCE` static constant 추가
- [ ] [typescript/src/communicator.ts](/config/workspace/toki_socket/typescript/src/communicator.ts): `nextNonce()`에 wrap 처리 추가
- [ ] 전체 검색으로 `++nonce`, `incrementAndGet`, `Add(1)`, `nonce += 1` 직접 발급이 남지 않았는지 확인
### 테스트 작성
각 언어별 Communicator 단위 테스트에 boundary test를 추가합니다. 테스트 목표는 `MAX_NONCE - 1`에서 두 번 송신했을 때 첫 packet nonce가 `MAX_NONCE`, 두 번째 packet nonce가 `1`이며 `0`이 송신되지 않는지 검증하는 것입니다.
- [ ] [dart/test/communicator_test.dart](/config/workspace/toki_socket/dart/test/communicator_test.dart): `_FakeCommunicator`에 테스트용 nonce setter 또는 helper를 추가하고 `nonce wraps after int32 max without emitting zero` 테스트 작성
- [ ] [go/test/nonce_test.go](/config/workspace/toki_socket/go/test/nonce_test.go) 또는 새 내부 패키지 테스트: `package toki_socket`에서 `communicator.nonce.Store(MaxNonce - 1)` 후 `Send` 두 번을 검증. 기존 `go/test`가 외부 패키지라 private 필드 접근이 안 되면 `go/communicator_nonce_test.go`를 새로 추가
- [ ] [kotlin/src/test/kotlin/com/tokilabs/toki_socket/CommunicatorTest.kt](/config/workspace/toki_socket/kotlin/src/test/kotlin/com/tokilabs/toki_socket/CommunicatorTest.kt): reflection으로 private `nonce: AtomicInteger`를 `Int.MAX_VALUE - 1`로 세팅하고 `testNonceWrapsAfterIntMax` 작성
- [ ] [python/test/test_communicator.py](/config/workspace/toki_socket/python/test/test_communicator.py): `communicator._nonce = MAX_NONCE - 1` 후 `send` 두 번을 검증하는 테스트 작성
- [ ] [typescript/test/communicator.test.ts](/config/workspace/toki_socket/typescript/test/communicator.test.ts): `comm as unknown as { nonce: number }`로 `MAX_NONCE - 1` 설정 후 `send` 두 번 검증
### 중간 검증
```bash
rg -n "\+\+nonce|incrementAndGet|Add\(1\)|nonce \+= 1" dart/lib go kotlin/src/main python/toki_socket typescript/src -g '!**/build/**'
```
기대 결과: nonce 발급 구현 내부 외에는 직접 증가 패턴이 남지 않습니다. Go/Kotlin은 CAS 기반 `nextNonce()`만 남아야 합니다.
## [API-3] 요청-응답 상관관계에서 wrap boundary 회귀 테스트 보강
### 문제
단순 `send` boundary만 검증하면 `sendRequest`의 pending map key와 response handler가 wrap된 nonce를 올바르게 사용하는지 놓칠 수 있습니다. 특히 `responseNonce == 0` 예약 규칙 때문에 `sendRequest`가 0을 request nonce로 발급하지 않는지 직접 확인해야 합니다.
Before:
```typescript
typescript/test/communicator.test.ts:134
test("sendRequest rejects on timeout", async () => {
const comm = new Communicator();
comm.initialize(new MockTransport(), parserMap());
await expect(
comm.sendRequest(create(TestDataSchema, { index: 1, message: "wait" }), TestDataSchema, 10),
).rejects.toThrow(/request timeout for nonce 1/);
});
```
```python
python/test/test_communicator.py:62
@pytest.mark.asyncio
async def test_send_request_response():
transport = FakeTransport()
communicator = Communicator()
communicator.initialize(transport, parser_map())
```
### 해결 방법
가능하면 각 언어 Communicator 테스트에 request boundary도 추가합니다. 시간 비용을 줄이기 위해 최소한 하나의 대표 구현만이 아니라 모든 구현체에서 `sendRequest` 첫 request nonce가 `MAX_NONCE` 또는 wrap 후 `1`을 사용하는지 검증합니다. 응답 매칭은 `responseNonce`에 해당 request nonce를 그대로 넣어 완료되는지 확인합니다.
### 수정 파일 및 체크리스트
- [ ] [dart/test/communicator_test.dart](/config/workspace/toki_socket/dart/test/communicator_test.dart): `sendRequest`가 `maxNonce` request를 pending에 등록하고 matching response로 완료되는지 검증
- [ ] [go/communicator_nonce_test.go](/config/workspace/toki_socket/go/communicator_nonce_test.go): `SendRequestTyped` boundary response matching 테스트 추가
- [ ] [kotlin/src/test/kotlin/com/tokilabs/toki_socket/CommunicatorTest.kt](/config/workspace/toki_socket/kotlin/src/test/kotlin/com/tokilabs/toki_socket/CommunicatorTest.kt): boundary request response matching 테스트 추가
- [ ] [python/test/test_communicator.py](/config/workspace/toki_socket/python/test/test_communicator.py): boundary request response matching 테스트 추가
- [ ] [typescript/test/communicator.test.ts](/config/workspace/toki_socket/typescript/test/communicator.test.ts): boundary request response matching 테스트 추가
### 테스트 작성
테스트를 작성합니다. 이름은 각 언어 관례에 맞춰 `nonce wraps at int32 max for sendRequest` 또는 `TestSendRequestNonceWrapsAtInt32Max`로 둡니다. assertion goal은 `request.nonce != 0`, `request.nonce == MAX_NONCE` 또는 wrap 이후 `1`, 그리고 `responseNonce=request.nonce` 응답이 정상 완료되는 것입니다.
### 중간 검증
```bash
dart test test/communicator_test.dart
go test ./...
cd kotlin && ./gradlew test --tests '*CommunicatorTest*'
cd python && python3 -m pytest test/test_communicator.py -q
cd typescript && npm test -- --run test/communicator.test.ts
```
기대 결과: 각 언어 Communicator 테스트가 통과합니다. 로컬 환경에 Go/JDK가 없으면 실행하지 못한 명령과 이유를 `CODE_REVIEW.md` 검증 결과에 명시합니다.
## 의존 관계 및 구현 순서
1. API-1로 문서의 정책을 먼저 확정합니다.
2. API-2로 모든 언어의 발급 로직을 같은 상수와 같은 wrap 규칙으로 맞춥니다.
3. API-3으로 `send`와 `sendRequest` boundary 회귀 테스트를 추가합니다.
4. 최종 검증을 실행하고 `CODE_REVIEW.md`를 채웁니다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| [PROTOCOL.md](/config/workspace/toki_socket/PROTOCOL.md) | API-1 |
| [VERSIONING.md](/config/workspace/toki_socket/VERSIONING.md) | API-1 |
| [dart/lib/src/communicator.dart](/config/workspace/toki_socket/dart/lib/src/communicator.dart) | API-2 |
| [dart/lib/src/heartbeat_mixin.dart](/config/workspace/toki_socket/dart/lib/src/heartbeat_mixin.dart) | API-2 |
| [dart/test/communicator_test.dart](/config/workspace/toki_socket/dart/test/communicator_test.dart) | API-2, API-3 |
| [go/communicator.go](/config/workspace/toki_socket/go/communicator.go) | API-2 |
| [go/communicator_nonce_test.go](/config/workspace/toki_socket/go/communicator_nonce_test.go) | API-2, API-3 |
| [kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt](/config/workspace/toki_socket/kotlin/src/main/kotlin/com/tokilabs/toki_socket/Communicator.kt) | API-2 |
| [kotlin/src/test/kotlin/com/tokilabs/toki_socket/CommunicatorTest.kt](/config/workspace/toki_socket/kotlin/src/test/kotlin/com/tokilabs/toki_socket/CommunicatorTest.kt) | API-2, API-3 |
| [python/toki_socket/communicator.py](/config/workspace/toki_socket/python/toki_socket/communicator.py) | API-2 |
| [python/test/test_communicator.py](/config/workspace/toki_socket/python/test/test_communicator.py) | API-2, API-3 |
| [typescript/src/communicator.ts](/config/workspace/toki_socket/typescript/src/communicator.ts) | API-2 |
| [typescript/test/communicator.test.ts](/config/workspace/toki_socket/typescript/test/communicator.test.ts) | API-2, API-3 |
## 최종 검증
```bash
tools/check_proto_sync.sh
```
기대 결과: proto schemas are in sync.
```bash
cd dart && dart test
```
기대 결과: 모든 Dart 테스트 통과.
```bash
cd go && go test ./...
```
기대 결과: 모든 Go 테스트 통과.
```bash
cd kotlin && ./gradlew test
```
기대 결과: 모든 Kotlin 테스트 통과.
```bash
cd python && python3 -m pytest test/ -q
```
기대 결과: 모든 Python 테스트 통과.
```bash
cd typescript && npm test -- --run
```
기대 결과: 모든 TypeScript 테스트 통과.