<!-- task=m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle plan=1 tag=REVIEW_REFACTOR -->

# Node Disconnect Lease Race 검증 보완 계획

## 이 파일을 읽는 구현 에이전트에게

누락된 deterministic race 회귀를 구현하고 검증한 뒤 `CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션에 실제 내용과 stdout/stderr를 채운다. active 파일은 그대로 두고 리뷰 준비 상태를 보고하며, 판정·log rename·`complete.log`·archive 이동은 code-review 전용이다. 선택된 Milestone의 `구현 잠금 > 결정 필요`가 실제 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 연결 근거를 기록하고 멈춘다. 사용자 직접 질문·선택지·`request_user_input`, `USER_REVIEW.md`, archive log, `complete.log` 작성은 금지한다. 환경/secret/service 차단, 일반 범위 변경, 재실행으로 닫을 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

첫 리뷰에서 manager-owned lease FSM과 direct transport lifecycle 구현 및 기존 fresh/race suite는 통과했다. 그러나 SDD S02가 요구하는 current-owner node disconnect와 run/tunnel/send release의 경쟁을 일반·long lease counter로 직접 검증하는 테스트가 빠져 `FAIL`했다. 구현 범위는 이 race evidence 보완과 테스트가 드러내는 최소 release 결함 수정으로 제한한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-G04.md`의 `사용자 리뷰 요청`에 기록한다. 구현 중 직접 사용자 prompt는 금지하며 code-review가 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- Prior task: `agent-task/m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle`
- Archived plan: `agent-task/m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle/plan_cloud_G07_0.log`
- Archived review: `agent-task/m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle/code_review_cloud_G07_0.log`
- Verdict: `FAIL`
- Required:
  - `apps/edge/internal/service/queue_reservation_test.go:232`의 32-goroutine release race에 `releaseNode`가 없고, `apps/edge/internal/transport/integration_test.go:685`는 callback 순서만 검증해 node disconnect와 terminal/send/Close 경쟁 시 일반·long lease/counter exactly-once evidence가 없다.
- Suggested: 없음
- Nit: 없음
- Affected files:
  - `apps/edge/internal/service/queue_reservation_test.go`
  - 테스트가 결함을 재현할 때만 `apps/edge/internal/service/model_queue_release.go`
- Verification evidence:
  - `gofmt -d ... && git diff --check`: PASS, 출력 없음.
  - `GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/...`: PASS.
  - 기존 REFACTOR-1/REFACTOR-2/final targeted `-race -count=1`: PASS.
  - `GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/edge/internal/bootstrap`: PASS.
- Roadmap carryover: `lease-lifecycle`은 SDD S02의 node-disconnect race evidence가 추가된 뒤에만 PASS 후보다.
- Narrow reread allowed: 위 archived plan/review 두 파일만 필요 시 읽는다. `agent-task/archive/**`를 탐색하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Task ids:
  - `lease-lifecycle`: 고유 provider lease와 exactly-once terminal 반환
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 규칙/검증: `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/node/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`
- 기준 문서: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`, `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`
- 계약/스펙: `agent-contract/inner/edge-node-runtime-wire.md`, `agent-spec/runtime/edge-node-execution.md`, `agent-spec/runtime/provider-pool-config-refresh.md`
- source: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `queue_reservation.go`, `model_queue_release.go`, `model_queue_snapshot.go`, `run_submit.go`, `run_types.go`, `run_wire.go`, `run_cancel.go`, `provider_pool.go`, `provider_resolution.go`, `provider_tunnel.go`, `service.go`, `status_provider.go`, `apps/edge/internal/events/bus.go`, `apps/edge/internal/node/registry.go`, `apps/edge/internal/transport/server.go`, `connection_handlers.go`, `apps/edge/internal/bootstrap/runtime.go`
- test: `apps/edge/internal/service/queue_reservation_test.go`, `model_queue_admission_test.go`, `long_context_queue_test.go`, `run_dispatch_internal_test.go`, `queue_dispatch_test.go`, `model_queue_test_support_test.go`, `apps/edge/internal/transport/server_test.go`, `integration_test.go`, `apps/edge/internal/bootstrap/runtime_lifecycle_test.go`
- 선행 evidence: `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/complete.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- 대상: S02 → `lease-lifecycle`.
- Evidence Map은 deterministic connection/event-drop 회귀와 race 검증을 요구한다. 이번 follow-up은 node disconnect가 다른 release 원인과 경쟁하는 일반·long counter/identity evidence만 추가한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- 현재 checkout의 dirty 변경을 보존하고 `GOCACHE=/tmp/iop-go-cache`, `go test -count=1`, `go test -race -count=10`을 사용한다.
- net.Pipe나 외부 provider 없이 service manager fixture로 재현하므로 외부 환경 프리플라이트는 필요 없다.
- full-cycle/live provider smoke는 별도 `capacity-smoke` Task 범위이며 이 검증 보완은 deterministic S02 race evidence에 한정한다.

### 테스트 커버리지 공백

- `TestProviderLeaseConcurrentReleaseExactlyOnce`: send failure, complete/cancel, tunnel terminal 경쟁은 검증하지만 `releaseNode`가 없다.
- `TestCurrentOwnerDisconnectReleasesLeasesBeforeEvent`: transport ownership과 callback/event 순서는 검증하지만 실제 lease/counter는 만들지 않는다.
- 일반·long lease에서 node disconnect와 다른 terminal 원인을 같은 barrier로 경쟁시킨 뒤 stale release가 새 lease를 감소시키지 않는 evidence가 없다.

### 심볼 참조

- rename/remove 대상은 없다.
- 검증 대상 call site는 `modelQueueManager.releaseNode`, `releaseRun`, `releaseLease`, `dropLeasesForNodeLocked`, `decrementSlotLocked`다.

### 분할 판단

- 기존 split task `02+01_lease_lifecycle`의 같은 Required 보완이므로 새 sibling으로 분리하지 않는다.
- predecessor `01_provider_resource_state`는 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/complete.log`로 충족되어 있다.
- 변경은 service manager test 한 파일의 단일 deterministic race 시나리오이며 별도 API/도메인 경계가 없다.

### 범위 결정 근거

- connection generation, disconnect fencing 중 admission 경쟁, offline snapshot, queue unavailable 수렴은 후속 connectivity Task 범위로 유지한다.
- transport callback 순서와 event bus 포화 회귀는 이미 통과했으므로 다시 설계하지 않는다.
- 새 테스트가 현재 release 구현의 결함을 재현할 때만 `model_queue_release.go`를 최소 수정한다.

### 빌드 등급

- `local-G04`: 요구 동작과 검증 fixture가 명확한 단일 패키지 concurrency 회귀 보완이며 deterministic `-race`로 판정 가능하다.

## 구현 체크리스트

- [ ] 일반·long provider lease에서 current-owner node disconnect와 run/tunnel/send release를 같은 barrier로 경쟁시키고, counter·lease·run index 정리와 stale release의 새 lease 격리를 검증한다. 테스트가 실패할 때만 release 구현을 최소 수정한다.
- [ ] focused `-race -count=10`, lifecycle regression race, fresh Edge 전체 테스트를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Node disconnect와 terminal release 경쟁 회귀

#### 문제

`apps/edge/internal/service/queue_reservation_test.go:232-294`의 경쟁 테스트는 `releaseLease`와 `releaseRun`만 실행한다. `apps/edge/internal/transport/integration_test.go:685-731`은 disconnect callback이 event보다 먼저 한 번 호출되는지만 확인하므로 SDD S02의 실제 일반·long lease 반환 경쟁 evidence가 없다.

#### 해결 방법

기존 send/run/tunnel 경쟁 테스트는 보존하고 별도 node-disconnect race 테스트를 추가한다. primary lease의 `releaseNode`와 `releaseRun`/`releaseLease`를 barrier에서 경쟁시킨 뒤 disconnected resource가 0이고 다른 resource lease가 유지되는지 확인한다. 같은 resource에 새 lease를 admission한 후 이전 lease/run identity의 늦은 release를 다시 호출해 새 일반·long counter가 감소하지 않는지도 확인한다.

Before (`queue_reservation_test.go:256-265`):

```go
switch i % 4 {
case 0:
	m.releaseLease(first, "send-error")
case 1:
	m.releaseRun("run-race-1", "complete")
case 2:
	m.releaseRun("run-race-1", "cancelled")
default:
	m.releaseLease(first, "tunnel-END")
}
```

After:

```go
go m.releaseNode(primaryNodeID, "transport-closed")
go m.releaseRun(primaryRunID, "complete")
go m.releaseLease(primaryLeaseID, "tunnel-END")
// Assert primary resource/index cleanup, unrelated lease survival, then
// re-admit and prove late old-identity release cannot decrement the new lease.
```

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/service/queue_reservation_test.go`: `TestProviderLeaseNodeDisconnectRaceExactlyOnce`를 normal/long subtest로 추가한다.
- [ ] primary disconnected resource의 `inFlight`/`longInFlight`, old lease, `leaseByRun`이 0/absent인지 확인한다.
- [ ] 다른 node/resource의 control lease와 disconnect 뒤 같은 resource의 새 lease가 old terminal/release에 영향받지 않는지 확인한다.
- [ ] 테스트가 현재 구현 결함을 재현할 때만 `apps/edge/internal/service/model_queue_release.go`의 identity transition을 최소 수정한다.

#### 테스트 작성

- 작성: `apps/edge/internal/service/queue_reservation_test.go`.
- 이름: `TestProviderLeaseNodeDisconnectRaceExactlyOnce`.
- fixture: capacity가 있는 primary/control provider resource, normal/long table case, barrier와 `sync.WaitGroup`.
- assertion: disconnected primary counter와 index 0, control lease 유지, 재-admission lease의 일반·long counter 1, old identity 재호출 후 값 불변.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -race -count=10 ./apps/edge/internal/service -run '^TestProviderLease(NodeDisconnectRaceExactlyOnce|ConcurrentReleaseExactlyOnce|TrackAndSendFailureRace)$'
```

기대 결과: 10회 모두 PASS, race report 없음.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/queue_reservation_test.go` | REVIEW_REFACTOR-1 |
| `apps/edge/internal/service/model_queue_release.go` | REVIEW_REFACTOR-1, 테스트가 결함을 재현할 때만 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/queue_reservation_test.go apps/edge/internal/service/model_queue_release.go
git diff --check
GOCACHE=/tmp/iop-go-cache go test -race -count=10 ./apps/edge/internal/service -run '^TestProviderLease(NodeDisconnectRaceExactlyOnce|ConcurrentReleaseExactlyOnce|TrackAndSendFailureRace)$'
GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/edge/internal/bootstrap -run 'Test(ProviderLease|RunTerminal|CurrentOwnerDisconnect|StaleDisconnect|ProviderTunnelRelease|RuntimeWiresTransportHandlers)'
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/...
```

기대 결과: gofmt/git diff 오류 없음, focused 10회 race와 lifecycle race PASS, Edge 전체 fresh PASS. Go test cache 결과는 허용하지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
