<!-- task=05_event_bus_replay plan=0 tag=EBR -->

# Code Review Reference - EBR

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**

## 개요

date=2026-05-17
task=05_event_bus_replay, plan=0, tag=EBR

## 이 파일을 읽는 리뷰 에이전트에게

구현과 검증 출력을 대조하고 PASS 시 plan/review를 log로 아카이브하세요.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|---|---|
| [EBR-1] drop stats 추가 | [x] |
| [EBR-2] bounded replay 추가 | [x] |

## 계획 대비 변경 사항

- `service.go`는 수정하지 않았다. 계획상 service.go 항목은 "if replay is exposed through service" 조건부였고, 현 단계에서 replay를 외부로 노출하는 호출자가 없어 Bus 레벨 API(`ReplayRun`, `ReplayNode`)만 추가했다. 향후 ops/debug surface에서 필요해지면 service에 wrapper를 추가한다.
- drop 카운터는 run/node 각각의 총합 두 개로만 유지했다 (subscriber별 분해는 하지 않음). 계획에서 "run/node/all subscriber별 또는 총합"이라 명시했고, 총합으로도 "유실 관찰성" 목표를 충족한다.
- replay buffer는 run/node 각각 단일 ring buffer로 두고 `ReplayRun(runID)`/`ReplayNode(nodeID)` 호출 시 ID 필터링을 한다. ID별 분리 buffer가 아니므로 동일 cap 안에서 여러 run이 섞이지만, "bounded" 보장은 동일하다.

## 주요 설계 결정

- `BusStats`는 `Stats()` 호출 시 `mu`를 잡고 값 복사로 반환한다 — atomic 대신 기존 `sync.Mutex` 경로 안에서 증가시켜 락 잡힌 publish 경로에 의존하는 단순한 모델을 유지했다.
- `offerRun`/`offerNode` 시그니처를 `bool` 반환으로 바꾸어 드롭을 호출 측(`PublishRun`/`PublishNode`)에서 카운트하도록 했다. `b.mu`를 이미 잡고 있는 컨텍스트에서만 호출되므로 별도 동기화는 필요 없다.
- `DefaultReplayCapacity = 64`. "작고 명시적인 상수"라는 계획 지시에 맞춰 명시적 const로 두고, 테스트는 `NewBusWithReplayCapacity`로 작은 cap을 주입해 bounded 동작을 검증한다.
- ring buffer는 `append(s[:0], s[1:]...)`로 단순 shift한다. cap이 작아서 (≤64) shift 비용이 무시할 수준이고, slice header 단순성이 우선이다.
- replay에 사용하는 `*iop.RunEvent`/`*iop.EdgeNodeEvent`는 publish된 포인터를 그대로 보관한다 — proto 객체는 publish 이후 mutation되지 않는 관례를 따른다.

## 리뷰어를 위한 체크포인트

- Drop이 더 이상 관찰 불가능하게 사라지지 않는가. → `PublishRun`/`PublishNode`의 `offer*` 반환값을 확인하여 `droppedRun`/`droppedNode` 증가; `Stats()`로 노출.
- Replay buffer가 bounded인지 테스트되는가. → `TestBusReplayIsBounded`가 `cap+5`개 publish 후 `cap` 길이를 단언.
- Race test가 통과했는가. → `go test -race -count=1 ./apps/edge/internal/events` PASS (아래 검증 결과 참조).

## 검증 결과

```
$ go test -race -count=1 ./apps/edge/internal/events
ok  	iop/apps/edge/internal/events	1.012s
```

```
$ go test -count=1 ./apps/edge/internal/events ./apps/edge/internal/service ./...
ok  	iop/apps/edge/internal/events	0.003s
ok  	iop/apps/edge/internal/service	0.004s
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.005s
ok  	iop/apps/edge/internal/node	0.004s
ok  	iop/apps/edge/internal/opsconsole	0.006s
ok  	iop/apps/edge/internal/transport	0.020s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.006s
ok  	iop/apps/node/internal/adapters/cli	87.661s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	30.935s
?   	iop/apps/node/internal/adapters/mock	[no test files]
?   	iop/apps/node/internal/adapters/ollama	[no test files]
?   	iop/apps/node/internal/adapters/vllm	[no test files]
ok  	iop/apps/node/internal/bootstrap	0.175s
ok  	iop/apps/node/internal/node	0.008s
ok  	iop/apps/node/internal/router	0.006s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.036s
ok  	iop/apps/node/internal/transport	0.019s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.012s
?   	iop/packages/events	[no test files]
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
?   	iop/proto/gen/iop	[no test files]
```
