<!-- task=m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup plan=0 tag=REFACTOR -->

# Cross-group Global Wake-up Scheduler 구현 계획

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

선행 `01_provider_resource_state`와 `02+01_lease_lifecycle`의 `complete.log`를 확인한 뒤 구현한다. 구현·검증 후 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 내용과 stdout/stderr로 채우고 active 파일을 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 선택된 Milestone의 `구현 잠금 > 결정 필요`가 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 연결 근거를 기록하고 멈춘다. 직접 사용자 질문·선택지·`request_user_input`, `USER_REVIEW.md`, archive log, `complete.log` 작성은 금지한다. 환경/서비스/evidence follow-up은 사용자 리뷰 요청이 아니다.

## 배경

현재 release는 예약이 속했던 `modelQueueGroup`만 pump하므로 같은 provider를 기다리는 다른 model group은 깨지지 않는다. queue item은 enqueue 당시 candidate snapshot을 보유하고 group 내부 순서만 알기 때문에 refresh·disable·disconnect 뒤 전역 FIFO와 live eligibility를 함께 보장할 수 없다. 이 작업은 단조 증가 `enqueue_seq`와 모든 group을 보는 scheduler를 도입해 가장 이른 현재 dispatch 가능한 요청을 선택한다.

## 사용자 리뷰 요청 흐름

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

## 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:
  - `cross-group-wakeup`: provider 변화 뒤 전역 enqueue 순서 기반 재평가
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 기준 문서: `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-config-runtime-refresh.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `agent-spec/runtime/edge-node-execution.md`, `agent-spec/runtime/provider-pool-config-refresh.md`
- 소스: `apps/edge/internal/service/model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, `model_queue_snapshot.go`, `provider_resolution.go`, `provider_pool.go`, `run_submit.go`, `provider_tunnel.go`, `service.go`, `status_provider.go`, `apps/edge/internal/node/registry.go`
- 테스트: `apps/edge/internal/service/model_queue_admission_test.go`, `long_context_queue_test.go`, `model_queue_test_support_test.go`, `provider_scheduling_test.go`, `provider_scheduling_advanced_test.go`, `queue_dispatch_test.go`, `service_internal_test.go`, `status_provider_test.go`

### SDD 기준

- SDD 상태 `[승인됨]`, 잠금 해제.
- S03 → `cross-group-wakeup`: 여러 group이 같은 provider를 기다리고 앞 long item만 long-full일 때 release/capacity 증가 후 전역 `enqueue_seq`상 가장 이른 dispatch 가능한 item을 선택하며 starvation·중복 예약·slot 누수가 없어야 한다.
- Interface Contract는 release, capacity 증가, disable/re-enable, disconnect 후 모든 group 재평가와 dispatch 직전 live eligibility 재확인을 요구한다.
- Evidence Map은 deterministic queue test와 race 검증을 요구하므로 release/refresh/disable/disconnect trigger, long HOL 회피, FIFO와 final zero counter를 checklist에 고정한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- Go 1.26.2, repo `/config/workspace/iop`, dirty worktree 보존. fresh test를 위해 `GOCACHE=/tmp/iop-go-cache`와 `-count=1`을 사용한다.
- local deterministic fixture만 사용하며 외부 provider/Docker는 필요 없다.

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

- 기존 `tryDispatchLocked(group)` 테스트는 한 group 안의 long head-of-line 회피만 검증한다.
- 다른 group의 lease release가 대기자를 깨우는 테스트와 전역 enqueue ordering 검증이 없다.
- refresh 테스트는 candidate limit 변경을 확인하지만 queued item의 live re-evaluation과 즉시 pump를 검증하지 않는다.

### 심볼 참조

- `tryDispatchLocked` call site는 `model_queue_admission.go`, `model_queue_release.go`, `long_context_queue_test.go`에 있다.
- `modelQueueGroup.queue`, `queueItem.candidates`, `removeItemLocked` 직접 참조를 service tests에서 확인했다. 전역 pending index 도입 시 모든 mutation call site를 같은 helper로 통합한다.

### 분할 판단

- split 정책을 적용했다. task group은 `m-provider-resource-admission-ownership-alignment`다.
- 현재 디렉터리 `03+01,02_cross_group_wakeup`의 predecessors는 `01_provider_resource_state`, `02+01_lease_lifecycle`다.
- 선행 충족 상태: 두 active sibling 모두 `complete.log`가 없으므로 구현 시작 전 각각 PASS evidence가 필요하다.
- scheduler가 resource counter와 lease release API에 의존하므로 선행 없이 병렬 구현하지 않는다.

### 범위 결정 근거

- Edge 전체 pending 상한/timeout 소유권은 `queue-ownership`, snapshot candidate-pressure 문서화는 `snapshot-source` Task다. 이 작업은 기존 queue policy 의미를 바꾸지 않는다.
- disconnect generation과 no-candidate 즉시 unavailable은 connectivity epic 범위다. 여기서는 선행 lifecycle이 전달한 resource exclusion을 scheduler trigger로 소비하고 live 후보에서 제외한다.
- provider endpoint active probe, cross-Edge semaphore, 요청별 token ledger는 제외한다.

### 빌드 등급

- `cloud-G07`: 전역 ordering, live re-evaluation, refresh/release concurrency를 함께 바꾸는 scheduler state-machine 작업이다.

## 의존 관계 및 구현 순서

1. `01_provider_resource_state`의 `complete.log`로 전역 resource state가 확정되어야 한다.
2. `02+01_lease_lifecycle`의 `complete.log`로 exactly-once release trigger가 확정되어야 한다.
3. global pending index와 pump를 추가하고 모든 release/refresh/exclusion trigger를 연결한다.
4. deterministic ordering 테스트 후 race suite를 실행한다.

## 구현 체크리스트

- [ ] queue item에 단조 증가 `enqueue_seq`를 부여하고 모든 model group에서 가장 이른 현재 dispatch 가능한 item을 선택하는 global pump를 구현·검증한다.
- [ ] lease 반환, capacity refresh, provider disable/re-enable, node exclusion이 global pump를 호출하고 dispatch 직전 live resource eligibility를 재확인하도록 구현·검증한다.
- [ ] long-full head 뒤 normal 진행, cross-group FIFO, starvation·중복 예약·slot 누수 없음과 final zero counter를 deterministic/race 테스트로 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Enqueue sequence 기반 global pending scheduler

#### 문제

`apps/edge/internal/service/model_queue_admission.go:318-375`의 `tryDispatchLocked`는 인자로 받은 group만 순회한다. `model_queue_release.go:140-156`도 lease가 속한 group 하나만 pump하므로 provider slot을 공유하는 다른 group은 timeout 또는 자기 group event까지 잠든다.

#### 해결 방법

manager에 monotonic `nextEnqueueSeq`와 global pending index를 둔다. pump는 모든 pending item을 enqueue 순서로 검사하되 현재 resource/long slot을 얻을 수 없는 item은 건너뛰고 가장 이른 dispatch 가능한 item 하나를 원자 예약한 뒤 다시 스캔한다. group queue는 조회/정책 attribution용 index로 유지하거나 global index에서 파생하되 이중 mutation helper를 하나로 제한한다.

Before (`model_queue_release.go:150-156`):

```go
if group.inflight[slot] > 0 {
	group.inflight[slot]--
}
m.tryDispatchLocked(group)
```

After:

```go
m.releaseLeaseResourceLocked(lease)
m.pumpAllLocked(resourceChangeRelease)
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: `enqueueSeq`, global pending index와 sequence allocator를 추가한다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: enqueue/remove/timeout/cancel을 단일 pending helper로 통합하고 `pumpAllLocked`를 구현한다.
- [ ] `apps/edge/internal/service/model_queue_release.go`: release 후 group-local pump를 global pump로 바꾼다.
- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: 두 group FIFO와 cross-group wake-up 테스트를 추가한다.
- [ ] `apps/edge/internal/service/long_context_queue_test.go`: 오래된 long-full item 뒤 다른 group normal item 진행을 검증한다.
- [ ] `apps/edge/internal/service/provider_scheduling_test.go`: 동일 dispatchability 안에서 enqueue order와 provider tie-break가 보존되는지 검증한다.

#### 테스트 작성

- `TestGlobalPumpWakesDifferentModelGroup`: group A release가 group B waiter를 즉시 dispatch한다.
- `TestGlobalPumpPreservesEarliestDispatchableSequence`: blocked item을 건너뛰되 dispatch 가능한 item끼리는 sequence를 보존한다.
- `TestGlobalPumpLongHeadDoesNotBlockNormalOtherGroup`: long-full head 뒤 normal request가 진행한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/service -run 'TestGlobalPump(WakesDifferentModelGroup|PreservesEarliestDispatchableSequence|LongHeadDoesNotBlockNormalOtherGroup)$'
```

기대 결과: cross-group wake-up, FIFO, HOL 회피 PASS.

### [REFACTOR-2] Resource 변화 trigger와 dispatch 직전 live re-evaluation

#### 문제

`model_queue_types.go`의 queue item은 최초 `candidates` slice를 저장하고, `service.go:54-62` refresh는 store를 교체할 뿐 pending queue를 pump하지 않는다. disable/capacity 변화 뒤 stale candidate metadata로 예약하거나 새 여유가 생겨도 waiter가 깨지지 않을 수 있다.

#### 해결 방법

queued item은 resource identity와 served target을 유지하고 dispatch 후보의 capacity/enabled/connectivity/priority는 선행 01의 live resource state와 registry에서 다시 해석한다. `setStore`, lease release, provider exclusion/re-enable hook은 변경된 resource key를 global pump에 전달한다. reservation 직전 eligibility를 같은 manager lock에서 재확인해 중복 slot을 방지한다.

Before (`model_queue_admission.go:343-355`):

```go
candidate := m.findAvailableNodeLocked(group, item.candidates, item.long)
if candidate == nil {
	continue
}
group.inflight[candidate.slotKey()]++
```

After:

```go
candidate := m.resolveLiveCandidateLocked(item)
if candidate == nil {
	continue
}
m.reserveLeaseLocked(item, candidate)
```

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

- [ ] `apps/edge/internal/service/model_queue_types.go`: queued candidate reference와 live resource metadata 경계를 명시한다.
- [ ] `apps/edge/internal/service/model_queue_admission.go`: dispatch 직전 live resolve + reserve를 한 critical section으로 구현한다.
- [ ] `apps/edge/internal/service/service.go`: runtime config 교체 뒤 affected resource refresh와 global pump를 호출한다.
- [ ] `apps/edge/internal/service/provider_resolution.go`: queued re-resolution이 최초 provider resolution과 동일 adapter/target/priority 규칙을 재사용하도록 helper를 추출한다.
- [ ] `apps/edge/internal/service/service_internal_test.go`: capacity 증가·disable/re-enable 즉시 wake-up과 over-capacity 차단을 추가한다.
- [ ] `apps/edge/internal/service/queue_dispatch_test.go`: node exclusion 뒤 남은 provider로 다른 group waiter가 진행하는 회귀를 추가한다.

#### 테스트 작성

- `TestGlobalPumpCapacityRefreshWakesWaiter`: capacity 증가가 다른 group pending을 즉시 깨운다.
- `TestGlobalPumpDisabledProviderIsNotDispatched`: enqueue 뒤 disable된 stale candidate를 선택하지 않는다.
- `TestGlobalPumpNodeExclusionFallsBackAcrossGroups`: exclusion 후 남은 live provider를 사용한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/service -run 'TestGlobalPump(CapacityRefreshWakesWaiter|DisabledProviderIsNotDispatched|NodeExclusionFallsBackAcrossGroups)$'
```

기대 결과: resource change trigger와 live re-evaluation PASS.

### [REFACTOR-3] Scheduler race·누수 회귀 고정

#### 문제

global pump는 release, timeout/cancel, refresh가 동시에 pending item을 제거·예약할 수 있어 중복 handoff와 slot 누수 위험이 있다. 기존 테스트는 group-local 순차 경로 중심이다.

#### 해결 방법

모든 pending state 전이와 lease reserve를 manager mutex 아래 하나의 helper로 제한하고 `waitCh` 전달 실패 시 동일 lease identity로 rollback한다. 반복 race fixture에서 총 dispatch 수, unique lease ID, resource peak와 final zero를 계측한다.

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

- [ ] `apps/edge/internal/service/model_queue_admission_test.go`: cancel/release/refresh barrier race fixture를 추가한다.
- [ ] `apps/edge/internal/service/model_queue_test_support_test.go`: peak/resource/unique lease 계측 helper를 추가한다.
- [ ] `apps/edge/internal/service/long_context_queue_test.go`: 일반·long 혼합 반복 후 모든 counter 0을 검증한다.

#### 테스트 작성

- `TestGlobalPumpConcurrentReleaseCancelRefreshNoLeak`: 반복 경쟁에서 한 item당 최대 한 lease, capacity peak 이하, final zero를 검증한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service -run 'TestGlobalPumpConcurrentReleaseCancelRefreshNoLeak$'
```

기대 결과: race detector PASS, 중복 lease/slot 누수 없음.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/model_queue_types.go` | REFACTOR-1, REFACTOR-2 |
| `apps/edge/internal/service/model_queue_admission.go` | REFACTOR-1, REFACTOR-2 |
| `apps/edge/internal/service/model_queue_release.go` | REFACTOR-1 |
| `apps/edge/internal/service/service.go` | REFACTOR-2 |
| `apps/edge/internal/service/provider_resolution.go` | REFACTOR-2 |
| `apps/edge/internal/service/model_queue_admission_test.go` | REFACTOR-1, REFACTOR-3 |
| `apps/edge/internal/service/long_context_queue_test.go` | REFACTOR-1, REFACTOR-3 |
| `apps/edge/internal/service/provider_scheduling_test.go` | REFACTOR-1 |
| `apps/edge/internal/service/service_internal_test.go` | REFACTOR-2 |
| `apps/edge/internal/service/queue_dispatch_test.go` | REFACTOR-2 |
| `apps/edge/internal/service/model_queue_test_support_test.go` | REFACTOR-3 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/model_queue_types.go apps/edge/internal/service/model_queue_admission.go apps/edge/internal/service/model_queue_release.go apps/edge/internal/service/service.go apps/edge/internal/service/provider_resolution.go apps/edge/internal/service/model_queue_admission_test.go apps/edge/internal/service/long_context_queue_test.go apps/edge/internal/service/provider_scheduling_test.go apps/edge/internal/service/service_internal_test.go apps/edge/internal/service/queue_dispatch_test.go apps/edge/internal/service/model_queue_test_support_test.go
GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache go test -race -count=1 ./apps/edge/internal/service -run 'TestGlobalPump'
```

기대 결과: gofmt diff 없음, Edge 전체 PASS, global scheduler race 없음. Go test cache 결과는 허용하지 않는다.

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