feat: control-plane edge wire baseline milestone and implementation
- Add roadmap milestone for control-plane edge wire baseline - Update main.go with edge registration logic - Add edge registry implementation - Add edge server tests - Add agent-task documentation for control-plane edge wire baseline
This commit is contained in:
parent
82744ef6a3
commit
07d4afe580
9 changed files with 1394 additions and 6 deletions
|
|
@ -42,15 +42,15 @@ Control Plane-Edge 연결에서 공유할 protobuf 메시지와 transport 경계
|
|||
|
||||
Edge가 Control Plane에 outbound로 붙고 연결 상태를 안정적으로 유지한다.
|
||||
|
||||
- [ ] [edge-outbound] Edge는 설정된 Control Plane wire endpoint로 outbound 연결을 시작할 수 있다. 검증: 로컬 Control Plane과 Edge를 함께 띄웠을 때 hello가 성공한다.
|
||||
- [ ] [heartbeat] Control Plane과 Edge는 heartbeat 또는 동등한 keepalive로 연결 생존 상태를 판단할 수 있다.
|
||||
- [ ] [disconnect] Edge 종료, transport close, reconnect 시 Control Plane registry의 Edge 상태가 갱신된다.
|
||||
- [x] [edge-outbound] Edge는 설정된 Control Plane wire endpoint로 outbound 연결을 시작할 수 있다. 검증: 로컬 Control Plane과 Edge를 함께 띄웠을 때 hello가 성공한다.
|
||||
- [x] [heartbeat] Control Plane과 Edge는 heartbeat 또는 동등한 keepalive로 연결 생존 상태를 판단할 수 있다.
|
||||
- [x] [disconnect] Edge 종료, transport close, reconnect 시 Control Plane registry의 Edge 상태가 갱신된다.
|
||||
|
||||
### Epic: [cp-edge-registry] Control Plane Edge Registry
|
||||
|
||||
Control Plane은 연결된 Edge를 제품 운영 표면에서 소비할 수 있는 상태로 보관한다.
|
||||
|
||||
- [ ] [edge-registry] Control Plane은 연결된 Edge의 id, name, protocol, connected state, last seen time을 in-memory registry로 조회할 수 있다.
|
||||
- [x] [edge-registry] Control Plane은 연결된 Edge의 id, name, protocol, connected state, last seen time을 in-memory registry로 조회할 수 있다.
|
||||
- [ ] [edge-status] Control Plane은 Edge에 상태 snapshot을 요청하고 Edge-owned 상태를 응답받을 수 있다. 검증: 최소 status request/response 테스트가 통과한다.
|
||||
- [ ] [event-relay] Edge의 node lifecycle event가 Control Plane까지 relay되는 최소 경로가 있다.
|
||||
|
||||
|
|
@ -59,13 +59,13 @@ Control Plane은 연결된 Edge를 제품 운영 표면에서 소비할 수 있
|
|||
Control Plane wire handler가 Edge 내부 구현을 복제하지 않고 Edge-owned operation을 소비하도록 정리한다.
|
||||
|
||||
- [ ] [service-boundary] Edge wire handler는 node registry, run dispatch, command request 로직을 복제하지 않고 `apps/edge/internal/service`와 `events.Bus` 같은 표면 중립 경계를 사용한다.
|
||||
- [ ] [local-smoke] Control Plane-Edge wire baseline을 확인하는 local smoke 기준이 `agent-test/local` 또는 관련 README에 정리된다.
|
||||
- [x] [local-smoke] Control Plane-Edge wire baseline을 확인하는 local smoke 기준이 `agent-test/local` 또는 관련 README에 정리된다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: `[wire-contract]` Epic은 proto 계약과 parser roundtrip 기준으로 충족되었지만, Edge connector, Control Plane registry, operation boundary 기능 Task가 아직 남아 있다.
|
||||
- 완료 근거: `[wire-contract]` Epic은 proto 계약과 parser roundtrip 기준으로 충족되었고, Edge connector outbound/heartbeat/disconnect, Control Plane registry 조회 표면, local smoke 기준 문서화는 완료 근거가 확인되었다. status/event relay와 service-boundary 기능 Task가 아직 남아 있다.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
|
|
@ -87,6 +87,10 @@ Control Plane wire handler가 Edge 내부 구현을 복제하지 않고 Edge-own
|
|||
- 표준선(선택): Edge가 소유한 node registry와 runtime state를 Control Plane이 복제하지 않고, Edge operation boundary를 통해 관찰/제어한다.
|
||||
- 표준선(선택): Control Plane은 Node 직접 연결/직접 스케줄링 경로를 만들지 않는다.
|
||||
- 진행 근거(2026-05-30): `proto/iop/control.proto`에 `EdgeHelloRequest`/`EdgeHelloResponse`를 추가하고 `make proto`, `make proto-dart`로 Go/Dart 생성물을 갱신했다. `apps/control-plane/internal/wire`에 Edge hello parser와 roundtrip/transport-boundary 테스트를 추가했으며 `go test ./apps/control-plane/... ./packages/... ./proto/gen/...`, `go test ./...`, `flutter test`가 통과했다.
|
||||
- 진행 근거(2026-05-30): `agent-task/archive/2026/05/m-control-plane-edge-wire-baseline/02+01_edge_connector/complete.log` 기준 Edge `control_plane` 설정/default/template, outbound hello, heartbeat, disconnect, reconnect, runtime wiring이 PASS 완료되었다. `go test -count=1 ./apps/edge/internal/controlplane`, `go test -race -count=1 ./apps/edge/internal/controlplane` 계열 검증이 통과했다.
|
||||
- 진행 근거(2026-05-30): `agent-task/archive/2026/05/m-control-plane-edge-wire-baseline/03+01,02_local_smoke/complete.log` 기준 `make test-control-plane-edge-wire`가 local Control Plane과 Edge를 함께 띄워 `edge hello accepted`, `connected to control plane`, `edge disconnected`를 확인했다.
|
||||
- 진행 근거(2026-05-30): Control Plane HTTP `/edges`와 `/edges/{edge_id}`에서 Edge registry snapshot을 조회하도록 했고, `go test -count=1 ./apps/control-plane/...`, registry/HTTP focused tests, `make test-control-plane-edge-wire`가 통과했다.
|
||||
- 진행 근거(2026-05-30): `agent-test/local/control-plane-smoke.md`에 Control Plane-Edge TCP wire baseline의 `make test-control-plane-edge-wire` 실행 기준과 hello/connect/disconnect marker 판정 기준을 정리했다.
|
||||
- 선행 작업: Edge Command와 Control Plane 운영 경계
|
||||
- 후속 작업: Control Plane과 Client, 정책, 이력, 감사, Multi-Edge 운영
|
||||
- 확인 필요: 없음
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
<!-- task=m-control-plane-edge-wire-baseline/04_status_snapshot plan=0 tag=STATUS -->
|
||||
|
||||
# Code Review Reference - STATUS
|
||||
|
||||
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-30
|
||||
task=m-control-plane-edge-wire-baseline/04_status_snapshot, plan=0, tag=STATUS
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
|
||||
- Task ids:
|
||||
- `edge-status`: Control Plane은 Edge에 상태 snapshot을 요청하고 Edge-owned 상태를 응답받을 수 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G08.md` -> `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` -> `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-control-plane-edge-wire-baseline/04_status_snapshot/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-control-plane-edge-wire-baseline`이므로 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [STATUS-1] Proto Status Contract | [ ] |
|
||||
| [STATUS-2] Control Plane Status Request Path | [ ] |
|
||||
| [STATUS-3] Edge Service-Backed Status Response | [ ] |
|
||||
| [STATUS-4] Focused Regression And Smoke | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `proto/iop/control.proto`에 Control Plane-Edge status request/response와 node snapshot message를 추가하고 `make proto`로 Go 생성물을 갱신한다.
|
||||
- [ ] Control Plane `wire.EdgeServer`가 connected Edge client에 status request를 보내고 response를 받을 수 있도록 client tracking과 parser/test를 추가한다.
|
||||
- [ ] Edge `controlplane.Connector`가 Control Plane status request를 수신하면 `apps/edge/internal/service`의 node snapshot 표면으로 응답하도록 option과 bootstrap wiring을 추가한다.
|
||||
- [ ] Status proto roundtrip, Control Plane request/response, Edge service-backed response, bootstrap wiring 테스트를 추가하고 대상 Go 테스트를 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-control-plane-edge-wire-baseline/04_status_snapshot/`를 `agent-task/archive/YYYY/MM/m-control-plane-edge-wire-baseline/04_status_snapshot/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-control-plane-edge-wire-baseline`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-control-plane-edge-wire-baseline/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `EdgeStatusRequest`/`EdgeStatusResponse`가 Control Plane-Edge 전용 계약이며 Node direct scheduling 필드를 추가하지 않았는지 확인한다.
|
||||
- Control Plane이 status 응답을 받기 위해 Edge active client만 사용하고 Node registry/transport를 직접 보지 않는지 확인한다.
|
||||
- Edge status response가 `apps/edge/internal/service.ListNodeSnapshots()` 또는 그에 준하는 surface-neutral provider를 통과하는지 확인한다.
|
||||
- generated proto 파일은 `make proto` 결과인지, 사람이 직접 편집한 흔적이 없는지 확인한다.
|
||||
- review stub의 검증 출력이 실제 명령 stdout/stderr와 일치하는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
|
||||
### STATUS-1 중간 검증
|
||||
|
||||
```text
|
||||
$ make proto
|
||||
(output)
|
||||
|
||||
$ go test -count=1 ./apps/control-plane/internal/wire ./proto/gen/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### STATUS-2 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
```
|
||||
|
||||
### STATUS-3 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/service
|
||||
(output)
|
||||
```
|
||||
|
||||
### STATUS-4 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/control-plane/... ./apps/edge/... ./packages/... ./proto/gen/...
|
||||
(output)
|
||||
|
||||
$ make test-control-plane-edge-wire
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ make proto
|
||||
(output)
|
||||
|
||||
$ go test -count=1 ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
|
||||
$ go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/service
|
||||
(output)
|
||||
|
||||
$ go test -count=1 ./apps/control-plane/... ./apps/edge/... ./packages/... ./proto/gen/...
|
||||
(output)
|
||||
|
||||
$ go test ./...
|
||||
(output)
|
||||
|
||||
$ make test-control-plane-edge-wire
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## 섹션 소유권
|
||||
|
||||
| 섹션 | 소유자 | 설명 |
|
||||
|------|--------|------|
|
||||
| 헤더 주석, 개요, 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
|
||||
| Roadmap Targets | plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음 |
|
||||
| 구현 항목별 완료 여부 | 구현 에이전트 | `[ ]` -> `[x]` 체크 |
|
||||
| 구현 체크리스트 | 구현 에이전트 | `[ ]` -> `[x]` 체크, 마지막 항목 필수 |
|
||||
| 코드리뷰 전용 체크리스트 | 리뷰 에이전트 | 구현 에이전트가 수정하지 않음 |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트 | 실제 내용으로 교체 |
|
||||
| 사용자 리뷰 요청 | 구현 에이전트 | 기본 `상태: 없음` 유지 또는 차단 근거 기록 |
|
||||
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 리뷰 초점 |
|
||||
| 검증 결과 | 구현 에이전트 | 실제 stdout/stderr 기록 |
|
||||
| 코드리뷰 결과 | 리뷰 에이전트 | 스텁에 포함하지 않음 |
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
<!-- task=m-control-plane-edge-wire-baseline/04_status_snapshot plan=0 tag=STATUS -->
|
||||
|
||||
# Control Plane-Edge Status Snapshot Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 구현입니다. 구현 후 검증을 실행하고 실제 변경 내용, 검증 출력, 계획 대비 변경 사항을 review stub에 기록한 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고하세요. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 스킬 책임입니다.
|
||||
|
||||
구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 현재 계획과 충돌하는 요구가 발견되면 `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 정확한 결정 항목과 근거를 기록하고 중단하세요. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해결할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 일반 후속 작업입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
현 마일스톤의 3번째 Epic에는 Control Plane이 Edge-owned 상태 snapshot을 요청하고 응답받는 기능이 남아 있습니다. 현재 Control Plane-Edge wire는 hello/enrollment와 registry 조회까지는 갖췄지만, Edge 쪽 node registry 상태를 Control Plane이 wire request/response로 읽는 계약은 없습니다. 이 작업은 status snapshot의 최소 proto 계약과 양방향 handler를 추가하되, Control Plane이 Node를 직접 보거나 스케줄링하지 않는 경계를 유지합니다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단 사유가 생기면 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션을 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 채웁니다. code-review 스킬이 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
|
||||
- Task ids:
|
||||
- `edge-status`: Control Plane은 Edge에 상태 snapshot을 요청하고 Edge-owned 상태를 응답받을 수 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/private/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/rules/project/domain/control-plane/rules.md`
|
||||
- `agent-ops/rules/project/domain/edge/rules.md`
|
||||
- `agent-ops/rules/project/domain/platform-common/rules.md`
|
||||
- `agent-ops/rules/project/domain/testing/rules.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/skills/common/update-test/SKILL.md`
|
||||
- `agent-ops/skills/common/update-roadmap/SKILL.md`
|
||||
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
|
||||
- `agent-ops/skills/project/e2e-smoke/SKILL.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/control-plane-smoke.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-test/local/testing-smoke.md`
|
||||
- `proto/iop/control.proto`
|
||||
- `proto/iop/runtime.proto`
|
||||
- `proto/iop/node.proto`
|
||||
- `apps/control-plane/internal/wire/edge.go`
|
||||
- `apps/control-plane/internal/wire/wire.go`
|
||||
- `apps/control-plane/internal/wire/edge_registry.go`
|
||||
- `apps/control-plane/internal/wire/edge_server.go`
|
||||
- `apps/control-plane/internal/wire/edge_test.go`
|
||||
- `apps/control-plane/internal/wire/edge_server_test.go`
|
||||
- `apps/control-plane/internal/wire/client.go`
|
||||
- `apps/control-plane/internal/wire/client_test.go`
|
||||
- `apps/control-plane/cmd/control-plane/main.go`
|
||||
- `apps/control-plane/cmd/control-plane/main_test.go`
|
||||
- `apps/edge/internal/controlplane/connector.go`
|
||||
- `apps/edge/internal/controlplane/connector_test.go`
|
||||
- `apps/edge/internal/controlplane/heartbeat_test.go`
|
||||
- `apps/edge/internal/controlplane/helpers_test.go`
|
||||
- `apps/edge/internal/service/service.go`
|
||||
- `apps/edge/internal/service/service_test.go`
|
||||
- `apps/edge/internal/events/bus.go`
|
||||
- `apps/edge/internal/events/bus_test.go`
|
||||
- `apps/edge/internal/bootstrap/runtime.go`
|
||||
- `apps/edge/internal/bootstrap/runtime_test.go`
|
||||
- `apps/edge/internal/node/registry.go`
|
||||
- `apps/edge/internal/node/registry_test.go`
|
||||
- `scripts/e2e-control-plane-edge-wire.sh`
|
||||
- `Makefile`
|
||||
- `go.mod`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- `test_env=local`로 판단했고 `agent-test/local/rules.md`를 읽었습니다.
|
||||
- 매칭 profile은 `control-plane-smoke`, `edge-smoke`, `platform-common-smoke`, `testing-smoke`입니다.
|
||||
- 적용 명령은 `make proto`, `go test ./apps/control-plane/...`, `go test ./apps/edge/...`, `go test ./packages/... ./proto/gen/...`, `go test ./...`, `make test-control-plane-edge-wire`입니다.
|
||||
- `control-plane-smoke`는 이번 작업 직전 `make test-control-plane-edge-wire` 기준을 명시하도록 갱신되었고 다시 읽었습니다.
|
||||
- profile에 `<확인 필요>` 값은 없었습니다. Docker compose health/manual full-cycle 항목은 실제 환경이 필요하므로 이 계획의 필수 자동 검증은 Go 테스트와 local wire smoke에 둡니다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- Status proto roundtrip: 현재 `EdgeHelloRequest`/`EdgeHelloResponse`만 parser roundtrip 테스트가 있고 status request/response 테스트가 없습니다.
|
||||
- Control Plane status 요청: 현재 `EdgeServer`는 hello를 받고 registry에 연결 상태만 저장하며, 연결된 Edge에 typed request를 보내는 테스트가 없습니다.
|
||||
- Edge status 응답: 현재 `Connector`는 hello만 보내고 Control Plane request listener가 없으며, `service.ListNodeSnapshots()`로 응답하는 테스트가 없습니다.
|
||||
- Boundary 검증: 현재 service DTO (`NodeSnapshot`) 테스트는 있지만 Control Plane connector가 이 표면을 사용한다는 테스트가 없습니다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none. 새 proto message와 새 handler/API를 추가하는 작업입니다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 plan 파일 선택 전에 평가했습니다.
|
||||
- 공유 task group은 `m-control-plane-edge-wire-baseline`입니다.
|
||||
- `04_status_snapshot`: proto/status request-response foundation입니다. 다른 작업이 이 wire/parser/connector foundation에 의존합니다.
|
||||
- `05+04_event_relay_boundary`: node lifecycle event relay와 service/events boundary 검증입니다. `04_status_snapshot`의 parser/connector 구조가 완료된 뒤 진행합니다.
|
||||
- protocol/schema, Control Plane wire, Edge connector, service boundary가 동시에 걸리므로 단일 plan으로 묶지 않습니다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- Control Plane이 Node에 직접 연결하거나 직접 스케줄링하는 계약은 만들지 않습니다.
|
||||
- Edge 내부 registry map을 Control Plane에 복제하지 않고, Edge가 응답하는 snapshot DTO만 Control Plane이 관찰합니다.
|
||||
- Flutter client, durable DB 저장, 권한/정책/감사, run dispatch/command 실행 제어는 제외합니다.
|
||||
- `scripts/e2e-control-plane-edge-wire.sh`는 이번 status request/response의 필수 구현 파일이 아니며, 기존 local smoke는 회귀 검증으로만 사용합니다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build lane: `cloud-G08`, review lane: `cloud-G08`.
|
||||
- 근거: protobuf 계약과 양쪽 앱의 proto-socket request/response handler를 바꾸는 cross-domain protocol 작업이며, 테스트는 가능하지만 API/경계 회귀 위험이 있습니다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `proto/iop/control.proto`에 Control Plane-Edge status request/response와 node snapshot message를 추가하고 `make proto`로 Go 생성물을 갱신한다.
|
||||
- [ ] Control Plane `wire.EdgeServer`가 connected Edge client에 status request를 보내고 response를 받을 수 있도록 client tracking과 parser/test를 추가한다.
|
||||
- [ ] Edge `controlplane.Connector`가 Control Plane status request를 수신하면 `apps/edge/internal/service`의 node snapshot 표면으로 응답하도록 option과 bootstrap wiring을 추가한다.
|
||||
- [ ] Status proto roundtrip, Control Plane request/response, Edge service-backed response, bootstrap wiring 테스트를 추가하고 대상 Go 테스트를 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## STATUS-1 Proto Status Contract
|
||||
|
||||
### 문제
|
||||
|
||||
`proto/iop/control.proto:57`-`63`에는 Edge enrollment 응답까지만 있고, Control Plane이 Edge-owned 상태 snapshot을 요청하는 message가 없습니다. 기존 `proto/iop/node.proto:7`-`17`의 `NodeInfo`는 legacy placeholder라서 새 Control Plane-Edge status 계약에 그대로 쓰면 Node 직접 관찰처럼 보일 수 있습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```proto
|
||||
// proto/iop/control.proto:57
|
||||
message EdgeHelloResponse {
|
||||
bool accepted = 1;
|
||||
string protocol = 2;
|
||||
int64 server_time_unix_nano = 3;
|
||||
string message = 4;
|
||||
string reason = 5;
|
||||
}
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`control.proto`에 Control Plane-Edge 전용 status message를 추가합니다. node snapshot은 Edge service가 제공하는 surface-neutral DTO만 담고, address/token/scheduling 필드는 넣지 않습니다.
|
||||
|
||||
예상 형태:
|
||||
|
||||
```proto
|
||||
message EdgeStatusRequest {
|
||||
string request_id = 1;
|
||||
}
|
||||
|
||||
message EdgeNodeSnapshot {
|
||||
string node_id = 1;
|
||||
string alias = 2;
|
||||
string label = 3;
|
||||
bool connected = 4;
|
||||
}
|
||||
|
||||
message EdgeStatusResponse {
|
||||
string request_id = 1;
|
||||
string edge_id = 2;
|
||||
string edge_name = 3;
|
||||
int64 observed_time_unix_nano = 4;
|
||||
repeated EdgeNodeSnapshot nodes = 5;
|
||||
map<string, string> metadata = 6;
|
||||
string error = 7;
|
||||
}
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `proto/iop/control.proto`: 새 message 추가, field number 안정화.
|
||||
- [ ] `proto/gen/iop/control.pb.go`: `make proto`로만 갱신.
|
||||
- [ ] `apps/control-plane/internal/wire/edge.go`: status request/response parser 추가.
|
||||
- [ ] `apps/edge/internal/controlplane/connector.go`: Edge 쪽 parser에 status request/response 추가.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `apps/control-plane/internal/wire/edge_test.go`에 `TestEdgeParserMapStatusRoundTrip`을 추가합니다.
|
||||
- assertion: `EdgeStatusRequest`, `EdgeStatusResponse`가 `EdgeParserMap()`에서 marshal/parse roundtrip되고, forbidden direct scheduling field가 status request에도 없음을 확인합니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
make proto
|
||||
go test -count=1 ./apps/control-plane/internal/wire ./proto/gen/...
|
||||
```
|
||||
|
||||
기대 결과: proto 생성이 성공하고 status parser/contract 테스트가 통과합니다.
|
||||
|
||||
## STATUS-2 Control Plane Status Request Path
|
||||
|
||||
### 문제
|
||||
|
||||
`apps/control-plane/internal/wire/edge_server.go:62`-`91`은 hello request listener만 등록하고, 연결된 Edge client를 status 요청에 사용할 수 있게 보관하지 않습니다. `apps/control-plane/internal/wire/edge_registry.go:12`-`24`도 외부 상태 snapshot만 보관하고 active client 접근 표면이 없습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```go
|
||||
// apps/control-plane/internal/wire/edge_server.go:62
|
||||
proto_socket.AddRequestListenerTyped(&client.Communicator, func(req *iop.EdgeHelloRequest) (*iop.EdgeHelloResponse, error) {
|
||||
now := time.Now()
|
||||
if req.GetEdgeId() == "" {
|
||||
// ...
|
||||
}
|
||||
connToken := registry.MarkConnected(req, now)
|
||||
// ...
|
||||
return &iop.EdgeHelloResponse{Accepted: true, Protocol: Protocol}, nil
|
||||
})
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`EdgeServer`가 hello acceptance 이후 active `TcpClient`를 edge id/token 기준으로 추적하고, `RequestStatus(ctx, edgeID string, timeout time.Duration) (*iop.EdgeStatusResponse, error)`를 제공합니다. disconnect가 current token일 때만 registry와 active client map을 함께 정리합니다.
|
||||
|
||||
예상 형태:
|
||||
|
||||
```go
|
||||
func (s *EdgeServer) RequestStatus(edgeID string, timeout time.Duration) (*iop.EdgeStatusResponse, error) {
|
||||
client, ok := s.activeClient(edgeID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("edge %q is not connected", edgeID)
|
||||
}
|
||||
return proto_socket.SendRequestTyped[*iop.EdgeStatusRequest, *iop.EdgeStatusResponse](
|
||||
&client.Communicator,
|
||||
&iop.EdgeStatusRequest{RequestId: fmt.Sprintf("edge-status-%d", time.Now().UnixNano())},
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/control-plane/internal/wire/edge_server.go`: active client map, status request method, stale disconnect cleanup 추가.
|
||||
- [ ] `apps/control-plane/internal/wire/edge_registry.go`: disconnect 갱신 성공 여부를 반환하거나 EdgeServer가 stale cleanup을 판정할 수 있는 helper 추가.
|
||||
- [ ] `apps/control-plane/internal/wire/edge_server_test.go`: connected Edge에 status request를 보내는 통합 테스트 추가.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `TestEdgeServerRequestsStatusFromConnectedEdge`를 추가합니다.
|
||||
- test Edge client에 `EdgeStatusRequest` listener를 등록하고 hello 후 `server.RequestStatus("edge-dgx-group", 2*time.Second)`가 node snapshot response를 받는지 확인합니다.
|
||||
- stale disconnect 기존 테스트가 active client cleanup 회귀를 막도록 유지합니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/control-plane/internal/wire
|
||||
```
|
||||
|
||||
기대 결과: hello/disconnect 기존 테스트와 status request 통합 테스트가 모두 통과합니다.
|
||||
|
||||
## STATUS-3 Edge Service-Backed Status Response
|
||||
|
||||
### 문제
|
||||
|
||||
`apps/edge/internal/controlplane/connector.go:186`-`193`은 Edge hello만 전송하고, `cpParserMap()`은 `EdgeHelloResponse`만 파싱합니다. `apps/edge/internal/bootstrap/runtime.go:64`-`68`도 connector를 만들 때 `Service`를 전달하지 않아 Control Plane request를 Edge-owned status 표면으로 응답할 방법이 없습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/controlplane/connector.go:186
|
||||
resp, err := toki.SendRequestTyped[*iop.EdgeHelloRequest, *iop.EdgeHelloResponse](
|
||||
&cl.Communicator,
|
||||
&iop.EdgeHelloRequest{
|
||||
EdgeId: c.edgeInfo.ID,
|
||||
EdgeName: c.edgeInfo.Name,
|
||||
Version: c.version,
|
||||
Capabilities: []string{"run", "node-registry"},
|
||||
},
|
||||
helloTimeout,
|
||||
)
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
Connector에 optional dependency를 추가하되 기존 tests/call sites가 크게 깨지지 않도록 functional option을 사용합니다. Bootstrap에서는 `WithStatusProvider(svc)`를 전달하고, status listener는 `Service.ListNodeSnapshots()` 결과만 proto response로 변환합니다.
|
||||
|
||||
예상 형태:
|
||||
|
||||
```go
|
||||
type StatusProvider interface {
|
||||
ListNodeSnapshots() []edgeservice.NodeSnapshot
|
||||
}
|
||||
|
||||
func WithStatusProvider(provider StatusProvider) Option {
|
||||
return func(c *Connector) { c.statusProvider = provider }
|
||||
}
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/edge/internal/controlplane/connector.go`: option, status provider, parser, request listener 추가.
|
||||
- [ ] `apps/edge/internal/bootstrap/runtime.go`: `NewConnector(..., WithStatusProvider(svc))` wiring.
|
||||
- [ ] `apps/edge/internal/controlplane/connector_test.go`: fake Control Plane이 status request를 보내고 service-backed node snapshot을 받는 테스트 추가.
|
||||
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: runtime이 status provider를 연결한다는 관찰 가능한 테스트 추가.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `TestConnectorRespondsToStatusRequestFromService`를 추가합니다.
|
||||
- `edgenode.Registry`에 fake node를 등록한 `edgeservice.Service`를 status provider로 전달하고, fake CP server가 hello 후 `EdgeStatusRequest`를 보내 `EdgeStatusResponse.Nodes`를 확인합니다.
|
||||
- provider가 없는 경우 error response 또는 empty response가 명확한지 테스트합니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/service
|
||||
```
|
||||
|
||||
기대 결과: Edge connector, bootstrap wiring, service DTO tests가 통과합니다.
|
||||
|
||||
## STATUS-4 Focused Regression And Smoke
|
||||
|
||||
### 문제
|
||||
|
||||
이 작업은 protobuf 계약, Control Plane wire server, Edge connector, bootstrap wiring을 함께 건드립니다. `agent-test/local/control-plane-smoke.md:52`-`55`는 Control Plane-Edge TCP wire 변경 시 `make test-control-plane-edge-wire` 실행을 요구합니다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
대상 package test를 먼저 통과시킨 뒤 전체 Go 회귀와 local wire smoke를 실행합니다. `make test-control-plane-edge-wire`는 status request 자체를 검증하지는 않지만, hello/connect/disconnect lifecycle이 깨지지 않았는지 확인합니다.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `Makefile`/`scripts/e2e-control-plane-edge-wire.sh`는 status 구현을 위해 수정하지 않습니다.
|
||||
- [ ] package focused tests를 실행합니다.
|
||||
- [ ] 전체 Go test와 local wire smoke를 실행합니다.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- 새 테스트는 STATUS-1~3 항목의 Go 테스트에 둡니다.
|
||||
- smoke script는 이번 작업에서 수정하지 않습니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/control-plane/... ./apps/edge/... ./packages/... ./proto/gen/...
|
||||
make test-control-plane-edge-wire
|
||||
```
|
||||
|
||||
기대 결과: package test와 existing Control Plane-Edge smoke가 통과합니다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `proto/iop/control.proto` | STATUS-1 |
|
||||
| `proto/gen/iop/control.pb.go` | STATUS-1 |
|
||||
| `apps/control-plane/internal/wire/edge.go` | STATUS-1 |
|
||||
| `apps/control-plane/internal/wire/edge_test.go` | STATUS-1 |
|
||||
| `apps/control-plane/internal/wire/edge_server.go` | STATUS-2 |
|
||||
| `apps/control-plane/internal/wire/edge_registry.go` | STATUS-2 |
|
||||
| `apps/control-plane/internal/wire/edge_server_test.go` | STATUS-2 |
|
||||
| `apps/edge/internal/controlplane/connector.go` | STATUS-3 |
|
||||
| `apps/edge/internal/controlplane/connector_test.go` | STATUS-3 |
|
||||
| `apps/edge/internal/bootstrap/runtime.go` | STATUS-3 |
|
||||
| `apps/edge/internal/bootstrap/runtime_test.go` | STATUS-3 |
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
이 subtask는 runtime dependency가 없는 `04_status_snapshot`입니다. 먼저 proto 계약과 parser를 추가한 뒤 Control Plane request path, Edge response path, regression/smoke 순서로 진행합니다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
make proto
|
||||
go test -count=1 ./apps/control-plane/internal/wire
|
||||
go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/service
|
||||
go test -count=1 ./apps/control-plane/... ./apps/edge/... ./packages/... ./proto/gen/...
|
||||
go test ./...
|
||||
make test-control-plane-edge-wire
|
||||
```
|
||||
|
||||
기대 결과: 모든 명령이 성공하고, `make test-control-plane-edge-wire` 출력에 `CP: hello accepted`, `Edge: connected to control plane`, `CP: edge disconnected`, `Control Plane-Edge wire smoke PASSED.`가 포함됩니다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
<!-- task=m-control-plane-edge-wire-baseline/05+04_event_relay_boundary plan=0 tag=EVENTS -->
|
||||
|
||||
# Code Review Reference - EVENTS
|
||||
|
||||
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-30
|
||||
task=m-control-plane-edge-wire-baseline/05+04_event_relay_boundary, plan=0, tag=EVENTS
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
|
||||
- Task ids:
|
||||
- `event-relay`: Edge의 node lifecycle event가 Control Plane까지 relay되는 최소 경로가 있다.
|
||||
- `service-boundary`: Edge wire handler는 node registry, run dispatch, command request 로직을 복제하지 않고 `apps/edge/internal/service`와 `events.Bus` 같은 표면 중립 경계를 사용한다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G08.md` -> `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` -> `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-control-plane-edge-wire-baseline/05+04_event_relay_boundary/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-control-plane-edge-wire-baseline`이므로 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [EVENTS-1] Dependency Gate And Parser | [ ] |
|
||||
| [EVENTS-2] Control Plane Receive And Recent Event Store | [ ] |
|
||||
| [EVENTS-3] Edge Events.Bus Relay | [ ] |
|
||||
| [EVENTS-4] Observation Surface And Boundary Guard | [ ] |
|
||||
| [EVENTS-5] Regression And Smoke | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `04_status_snapshot` subtask가 PASS 완료된 뒤에만 시작한다. runtime이 이 subtask를 시작했다면 의존성이 충족된 것으로 보고, 수동 실행이면 predecessor PASS evidence 또는 runtime 완료 이벤트 경로를 review stub에 기록한다.
|
||||
- [ ] Control Plane `EdgeParserMap`/`EdgeServer`가 `EdgeNodeEvent`를 수신해 edge id와 함께 bounded recent event store에 기록하도록 구현한다.
|
||||
- [ ] Edge `controlplane.Connector`가 `events.Bus.SubscribeAllNodes()`를 통해 node lifecycle event를 Control Plane으로 relay하도록 option과 bootstrap wiring을 추가한다.
|
||||
- [ ] Control Plane에서 relayed node lifecycle event를 조회할 최소 observation surface와 tests를 추가하되, Edge internal registry/run/command 로직을 복제하지 않는다.
|
||||
- [ ] Event relay, recent event snapshot, service/events boundary 회귀 테스트를 추가하고 대상 Go 테스트 및 기존 local wire smoke를 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-control-plane-edge-wire-baseline/05+04_event_relay_boundary/`를 `agent-task/archive/YYYY/MM/m-control-plane-edge-wire-baseline/05+04_event_relay_boundary/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-control-plane-edge-wire-baseline`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-control-plane-edge-wire-baseline/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `04_status_snapshot`의 predecessor PASS evidence 또는 runtime 완료 이벤트 경로가 없는 상태에서 코드 구현이 진행되지 않았는지 확인한다.
|
||||
- Control Plane receive path가 connection의 `edgeID` context에 event를 묶고, hello 전 event를 무시하는지 확인한다.
|
||||
- Edge connector relay가 `events.Bus.SubscribeAllNodes()`를 사용하고 node registry/run dispatch/command request 로직을 직접 복제하지 않는지 확인한다.
|
||||
- relayed event copy/enrichment가 원본 bus event를 mutate하지 않는지 확인한다.
|
||||
- `/edges/{edge_id}/events`는 Control Plane에 도착한 event 조회만 제공하며 Edge internals를 직접 재구성하지 않는지 확인한다.
|
||||
- `-race` 검증과 review stub의 실제 stdout/stderr가 신뢰 가능한지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
|
||||
### EVENTS-1 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
```
|
||||
|
||||
### EVENTS-2 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
```
|
||||
|
||||
### EVENTS-3 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/events
|
||||
(output)
|
||||
```
|
||||
|
||||
### EVENTS-4 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
|
||||
$ rg --sort path -n "apps/edge/internal/node|NodeCommandRequest|RunRequest" apps/edge/internal/controlplane
|
||||
(output)
|
||||
```
|
||||
|
||||
### EVENTS-5 중간 검증
|
||||
|
||||
```text
|
||||
$ go test -race -count=1 ./apps/edge/internal/controlplane ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
|
||||
$ go test ./...
|
||||
(output)
|
||||
|
||||
$ make test-control-plane-edge-wire
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
|
||||
$ go test -count=1 ./apps/control-plane/cmd/control-plane
|
||||
(output)
|
||||
|
||||
$ go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/events
|
||||
(output)
|
||||
|
||||
$ go test -race -count=1 ./apps/edge/internal/controlplane ./apps/control-plane/internal/wire
|
||||
(output)
|
||||
|
||||
$ go test ./...
|
||||
(output)
|
||||
|
||||
$ make test-control-plane-edge-wire
|
||||
(output)
|
||||
|
||||
$ rg --sort path -n "apps/edge/internal/node|NodeCommandRequest|RunRequest" apps/edge/internal/controlplane
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## 섹션 소유권
|
||||
|
||||
| 섹션 | 소유자 | 설명 |
|
||||
|------|--------|------|
|
||||
| 헤더 주석, 개요, 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
|
||||
| Roadmap Targets | plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음 |
|
||||
| 구현 항목별 완료 여부 | 구현 에이전트 | `[ ]` -> `[x]` 체크 |
|
||||
| 구현 체크리스트 | 구현 에이전트 | `[ ]` -> `[x]` 체크, 마지막 항목 필수 |
|
||||
| 코드리뷰 전용 체크리스트 | 리뷰 에이전트 | 구현 에이전트가 수정하지 않음 |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트 | 실제 내용으로 교체 |
|
||||
| 사용자 리뷰 요청 | 구현 에이전트 | 기본 `상태: 없음` 유지 또는 차단 근거 기록 |
|
||||
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 리뷰 초점 |
|
||||
| 검증 결과 | 구현 에이전트 | 실제 stdout/stderr 기록 |
|
||||
| 코드리뷰 결과 | 리뷰 에이전트 | 스텁에 포함하지 않음 |
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
<!-- task=m-control-plane-edge-wire-baseline/05+04_event_relay_boundary plan=0 tag=EVENTS -->
|
||||
|
||||
# Control Plane-Edge Event Relay And Boundary Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 구현입니다. 구현 후 검증을 실행하고 실제 변경 내용, 검증 출력, 계획 대비 변경 사항을 review stub에 기록한 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고하세요. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 스킬 책임입니다.
|
||||
|
||||
구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 현재 계획과 충돌하는 요구가 발견되면 `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 정확한 결정 항목과 근거를 기록하고 중단하세요. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해결할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 일반 후속 작업입니다.
|
||||
|
||||
## 배경
|
||||
|
||||
현 마일스톤의 3-4번째 Epic에는 Edge의 node lifecycle event가 Control Plane까지 도달하고, Edge wire handler가 Edge 내부 구현을 복제하지 않는 boundary가 남아 있습니다. Status snapshot 작업이 끝나면 Control Plane-Edge connector는 양방향 message를 다룰 수 있으므로, 이 작업은 그 구조 위에 `events.Bus` 기반 live relay와 Control Plane의 최소 recent-event 관찰 표면을 붙입니다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단 사유가 생기면 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션을 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 채웁니다. code-review 스킬이 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
|
||||
- Task ids:
|
||||
- `event-relay`: Edge의 node lifecycle event가 Control Plane까지 relay되는 최소 경로가 있다.
|
||||
- `service-boundary`: Edge wire handler는 node registry, run dispatch, command request 로직을 복제하지 않고 `apps/edge/internal/service`와 `events.Bus` 같은 표면 중립 경계를 사용한다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/rules/project/domain/control-plane/rules.md`
|
||||
- `agent-ops/rules/project/domain/edge/rules.md`
|
||||
- `agent-ops/rules/project/domain/platform-common/rules.md`
|
||||
- `agent-ops/rules/project/domain/testing/rules.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||
- `agent-ops/skills/project/e2e-smoke/SKILL.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/control-plane-smoke.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-test/local/testing-smoke.md`
|
||||
- `proto/iop/control.proto`
|
||||
- `proto/iop/runtime.proto`
|
||||
- `proto/iop/node.proto`
|
||||
- `apps/control-plane/internal/wire/edge.go`
|
||||
- `apps/control-plane/internal/wire/edge_registry.go`
|
||||
- `apps/control-plane/internal/wire/edge_server.go`
|
||||
- `apps/control-plane/internal/wire/edge_test.go`
|
||||
- `apps/control-plane/internal/wire/edge_server_test.go`
|
||||
- `apps/control-plane/cmd/control-plane/main.go`
|
||||
- `apps/control-plane/cmd/control-plane/main_test.go`
|
||||
- `apps/edge/internal/controlplane/connector.go`
|
||||
- `apps/edge/internal/controlplane/connector_test.go`
|
||||
- `apps/edge/internal/service/service.go`
|
||||
- `apps/edge/internal/service/service_test.go`
|
||||
- `apps/edge/internal/events/bus.go`
|
||||
- `apps/edge/internal/events/bus_test.go`
|
||||
- `apps/edge/internal/bootstrap/runtime.go`
|
||||
- `apps/edge/internal/bootstrap/runtime_test.go`
|
||||
- `apps/edge/internal/node/registry.go`
|
||||
- `apps/edge/internal/node/registry_test.go`
|
||||
- `scripts/e2e-control-plane-edge-wire.sh`
|
||||
- `Makefile`
|
||||
- `go.mod`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- `test_env=local`로 판단했고 `agent-test/local/rules.md`를 읽었습니다.
|
||||
- 매칭 profile은 `control-plane-smoke`, `edge-smoke`, `platform-common-smoke`, `testing-smoke`입니다.
|
||||
- 적용 명령은 `go test ./apps/control-plane/...`, `go test ./apps/edge/...`, `go test ./...`, `make test-control-plane-edge-wire`입니다.
|
||||
- 이 작업은 기존 `runtime.proto`의 `EdgeNodeEvent`를 사용하므로 새 protobuf 원본 변경이 없다면 `make proto`는 필수가 아닙니다. proto를 바꾸는 선택을 했다면 `platform-common-smoke`에 따라 `make proto`를 추가 실행합니다.
|
||||
- `make test-control-plane-edge-wire`는 hello/connect/disconnect smoke이며 event relay 자체를 검증하지 않습니다. event relay 검증은 focused Go integration test에 둡니다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- Edge event relay: `events.Bus.SubscribeAllNodes()`와 connector를 연결하는 테스트가 없습니다.
|
||||
- Control Plane receive path: `EdgeServer`가 `EdgeNodeEvent`를 수신해 edge id와 함께 저장하는 테스트가 없습니다.
|
||||
- Recent event observation: Control Plane HTTP `/edges`는 registry state만 보여주며 relayed node lifecycle event를 조회하는 표면이 없습니다.
|
||||
- Boundary: connector가 `events.Bus`와 `Service` 같은 표면 중립 dependency만 쓰고 node registry/client internals를 직접 복제하지 않는 회귀 테스트가 없습니다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none. 새 event relay method와 registry event snapshot API를 추가하는 작업입니다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 plan 파일 선택 전에 평가했습니다.
|
||||
- 공유 task group은 `m-control-plane-edge-wire-baseline`입니다.
|
||||
- `04_status_snapshot`은 status request/response foundation이며 이 작업의 선행 조건입니다.
|
||||
- `05+04_event_relay_boundary`는 `04_status_snapshot` 완료 후 같은 connector/parser 구조에 event relay를 추가합니다.
|
||||
- event fanout, Control Plane receive registry, HTTP observation, boundary 검증은 status foundation과 다른 위험 profile이므로 별도 subtask로 분리합니다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- Control Plane DB 저장, durable event history, ack/retry delivery, backpressure policy, multi-edge federation은 제외합니다.
|
||||
- Run event stream relay는 제외하고 node lifecycle/control event인 `EdgeNodeEvent`만 다룹니다.
|
||||
- Edge command/run dispatch/status command 로직은 새로 복제하지 않고, 필요한 상태는 `events.Bus`와 status provider가 제공한 DTO에 한정합니다.
|
||||
- 기본 local smoke script는 유지합니다. event relay success는 deterministic Go test로 검증합니다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build lane: `cloud-G08`, review lane: `cloud-G08`.
|
||||
- 근거: event fanout/concurrency와 Control Plane-Edge protocol handler를 함께 바꾸는 cross-domain 작업이며, status subtask에 의존하는 boundary 검증이 필요합니다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `04_status_snapshot` subtask가 PASS 완료된 뒤에만 시작한다. runtime이 이 subtask를 시작했다면 의존성이 충족된 것으로 보고, 수동 실행이면 predecessor PASS evidence 또는 runtime 완료 이벤트 경로를 review stub에 기록한다.
|
||||
- [ ] Control Plane `EdgeParserMap`/`EdgeServer`가 `EdgeNodeEvent`를 수신해 edge id와 함께 bounded recent event store에 기록하도록 구현한다.
|
||||
- [ ] Edge `controlplane.Connector`가 `events.Bus.SubscribeAllNodes()`를 통해 node lifecycle event를 Control Plane으로 relay하도록 option과 bootstrap wiring을 추가한다.
|
||||
- [ ] Control Plane에서 relayed node lifecycle event를 조회할 최소 observation surface와 tests를 추가하되, Edge internal registry/run/command 로직을 복제하지 않는다.
|
||||
- [ ] Event relay, recent event snapshot, service/events boundary 회귀 테스트를 추가하고 대상 Go 테스트 및 기존 local wire smoke를 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## EVENTS-1 Dependency Gate And Parser
|
||||
|
||||
### 문제
|
||||
|
||||
이 subtask는 `05+04_event_relay_boundary` 이름으로 `04_status_snapshot`에 의존합니다. 또한 `apps/control-plane/internal/wire/edge.go:13`-`21`의 `EdgeParserMap()`은 현재 hello message만 파싱하므로 Edge가 `runtime.proto`의 `EdgeNodeEvent`를 보내도 Control Plane이 받을 수 없습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```go
|
||||
// apps/control-plane/internal/wire/edge.go:13
|
||||
return proto_socket.ParserMap{
|
||||
proto_socket.TypeNameOf(&iop.EdgeHelloRequest{}): func(b []byte) (proto.Message, error) {
|
||||
req := &iop.EdgeHelloRequest{}
|
||||
return req, proto.Unmarshal(b, req)
|
||||
},
|
||||
proto_socket.TypeNameOf(&iop.EdgeHelloResponse{}): func(b []byte) (proto.Message, error) {
|
||||
res := &iop.EdgeHelloResponse{}
|
||||
return res, proto.Unmarshal(b, res)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
구현 시작 시 runtime dependency가 충족되었는지 확인합니다. runtime이 이 subtask를 시작했다면 `04_status_snapshot`이 PASS된 것으로 보고, 수동 실행이면 predecessor PASS evidence 또는 runtime 완료 이벤트 경로를 review stub에 기록합니다. 완료 전이면 code changes를 하지 말고 dependency 대기 상태를 기록합니다. 완료되어 있으면 `EdgeParserMap()`에 `iop.EdgeNodeEvent` parser를 추가하고 roundtrip test를 작성합니다.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `04_status_snapshot` predecessor PASS evidence 또는 runtime 완료 이벤트 경로: 선행 완료 확인.
|
||||
- [ ] `apps/control-plane/internal/wire/edge.go`: `EdgeNodeEvent` parser 추가.
|
||||
- [ ] `apps/control-plane/internal/wire/edge_test.go`: parser roundtrip 추가.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `TestEdgeParserMapNodeEventRoundTrip`를 추가합니다.
|
||||
- assertion: `event_id`, `type`, `node_id`, `alias`, `metadata`, `timestamp`가 roundtrip됩니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/control-plane/internal/wire
|
||||
```
|
||||
|
||||
기대 결과: review stub에 선행 PASS evidence가 기록되고 parser tests가 통과합니다.
|
||||
|
||||
## EVENTS-2 Control Plane Receive And Recent Event Store
|
||||
|
||||
### 문제
|
||||
|
||||
`apps/control-plane/internal/wire/edge_server.go:62`-`107`은 hello와 disconnect만 처리하고, 연결 context의 `edgeID`에 묶인 `EdgeNodeEvent` listener가 없습니다. `apps/control-plane/internal/wire/edge_registry.go:12`-`24`의 state에는 lifecycle event 보관 표면도 없습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```go
|
||||
// apps/control-plane/internal/wire/edge_server.go:93
|
||||
client.AddDisconnectListener(func(c *proto_socket.TcpClient) {
|
||||
idMu.Lock()
|
||||
id := edgeID
|
||||
tok := token
|
||||
idMu.Unlock()
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
info := c.DisconnectInfo()
|
||||
registry.MarkDisconnected(id, tok, time.Now(), info)
|
||||
})
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
hello로 식별된 connection context에서 `EdgeNodeEvent` listener를 등록하고, registry에 bounded recent events를 edge id와 함께 기록합니다. timestamp가 비어 있으면 Control Plane receive time을 채우고, event 자체는 copy/clone해서 caller mutation이 registry를 오염시키지 않게 합니다.
|
||||
|
||||
예상 형태:
|
||||
|
||||
```go
|
||||
proto_socket.AddListenerTyped[*iop.EdgeNodeEvent](&client.Communicator, func(event *iop.EdgeNodeEvent) {
|
||||
idMu.Lock()
|
||||
id := edgeID
|
||||
idMu.Unlock()
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
registry.RecordNodeEvent(id, event, time.Now())
|
||||
})
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/control-plane/internal/wire/edge_registry.go`: `EdgeNodeEventRecord`, `RecordNodeEvent`, `NodeEvents(edgeID, nodeID string)` 추가.
|
||||
- [ ] `apps/control-plane/internal/wire/edge_server.go`: `EdgeNodeEvent` listener 추가.
|
||||
- [ ] `apps/control-plane/internal/wire/edge_server_test.go`: relayed event 수신/저장 테스트 추가.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `TestEdgeServerRecordsNodeLifecycleEventFromEdge`를 추가합니다.
|
||||
- hello 전 event는 무시하고, hello 후 `node.connected` event는 `edgeID` 아래에 저장되는지 확인합니다.
|
||||
- returned event slice를 mutate해도 registry 내부가 바뀌지 않는지 확인합니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/control-plane/internal/wire
|
||||
```
|
||||
|
||||
기대 결과: event receive/store tests와 기존 hello/disconnect tests가 통과합니다.
|
||||
|
||||
## EVENTS-3 Edge Events.Bus Relay
|
||||
|
||||
### 문제
|
||||
|
||||
`apps/edge/internal/events/bus.go:123`-`138`은 all-node subscriber를 제공하고, `apps/edge/internal/bootstrap/runtime.go:85`-`87`은 transport node events를 bus에 publish합니다. 하지만 `apps/edge/internal/controlplane/connector.go:223`-`230`은 Control Plane connection이 열린 동안 disconnect를 기다릴 뿐, bus events를 Control Plane으로 보내지 않습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/bootstrap/runtime.go:85
|
||||
func (r *Runtime) wireHandlers() {
|
||||
r.Server.SetRunEventHandler(r.EventBus.PublishRun)
|
||||
r.Server.SetNodeEventHandler(r.EventBus.PublishNode)
|
||||
}
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
Connector에 `WithNodeEventBus(bus *edgeevents.Bus)` option을 추가하고, accepted Control Plane connection lifetime 동안 `SubscribeAllNodes()`를 사용해 live node lifecycle events를 `TcpClient.Send(event)`로 relay합니다. Event metadata에 `edge_id`가 없으면 copy에만 채우고 원본 bus event는 mutate하지 않습니다.
|
||||
|
||||
예상 형태:
|
||||
|
||||
```go
|
||||
func (c *Connector) startNodeEventRelay(ctx context.Context, cl *toki.TcpClient) func() {
|
||||
if c.nodeEvents == nil {
|
||||
return func() {}
|
||||
}
|
||||
events, unsubscribe := c.nodeEvents.SubscribeAllNodes(64)
|
||||
go func() {
|
||||
defer unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = cl.Send(enrichEdgeMetadata(event, c.edgeInfo.ID))
|
||||
}
|
||||
}
|
||||
}()
|
||||
return unsubscribe
|
||||
}
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/edge/internal/controlplane/connector.go`: node event bus option, relay loop, event copy/enrichment helper 추가.
|
||||
- [ ] `apps/edge/internal/bootstrap/runtime.go`: `WithNodeEventBus(bus)` wiring.
|
||||
- [ ] `apps/edge/internal/controlplane/connector_test.go`: fake Control Plane이 `EdgeNodeEvent`를 받는 테스트 추가.
|
||||
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: runtime wiring 회귀 테스트 추가.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `TestConnectorRelaysNodeLifecycleEventsToControlPlane`를 추가합니다.
|
||||
- fake CP server가 hello 후 `EdgeNodeEvent` listener를 등록하고, Edge `events.Bus.PublishNode()` 호출 후 event를 받는지 확인합니다.
|
||||
- event copy에 edge metadata가 들어가고 원본 event metadata는 mutation되지 않는지 확인합니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/events
|
||||
```
|
||||
|
||||
기대 결과: connector relay, bootstrap wiring, events bus tests가 통과합니다.
|
||||
|
||||
## EVENTS-4 Observation Surface And Boundary Guard
|
||||
|
||||
### 문제
|
||||
|
||||
`apps/control-plane/cmd/control-plane/main.go:223`-`258`의 HTTP handlers는 `/edges`와 `/edges/{edge_id}` registry state만 반환합니다. Relayed lifecycle event를 Control Plane 운영 표면에서 확인할 최소 조회 경로가 없으면 relay가 제품 운영 surface로 이어졌는지 검증하기 어렵습니다.
|
||||
|
||||
현재 코드:
|
||||
|
||||
```go
|
||||
// apps/control-plane/cmd/control-plane/main.go:241
|
||||
mux.HandleFunc("/edges/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", http.MethodGet)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
edgeID := strings.TrimPrefix(r.URL.Path, "/edges/")
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`GET /edges/{edge_id}/events`를 최소 observation surface로 추가하고, registry의 bounded recent node events를 JSON으로 반환합니다. 이 endpoint는 Control Plane에 도착한 event를 보여줄 뿐, Edge node registry나 command/run dispatch 로직을 재구현하지 않습니다.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main.go`: event view DTO와 `/edges/{edge_id}/events` handler 추가.
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: list/get/events handler tests 추가.
|
||||
- [ ] `apps/edge/internal/controlplane/connector.go`: node registry/run dispatch/command request 직접 import가 없는지 유지.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- `TestEdgeRegistryHTTPHandlersListNodeEvents`를 추가합니다.
|
||||
- registry에 edge와 node event를 seed하고 `/edges/edge-a/events`가 event id/type/node/alias/reason/timestamp/metadata를 반환하는지 확인합니다.
|
||||
- `/edges/edge-a/events/extra`나 unknown edge는 404를 반환합니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire
|
||||
rg --sort path -n "apps/edge/internal/node|NodeCommandRequest|RunRequest" apps/edge/internal/controlplane
|
||||
```
|
||||
|
||||
기대 결과: HTTP tests가 통과하고, `apps/edge/internal/controlplane`에는 node internals 또는 command/run dispatch 복제 import가 없습니다. 단, proto type 이름이 test fixture에 필요한 경우는 계획 대비 변경 사항에 이유를 기록합니다.
|
||||
|
||||
## EVENTS-5 Regression And Smoke
|
||||
|
||||
### 문제
|
||||
|
||||
Event relay는 goroutine, disconnect lifetime, bounded buffers가 걸려 있어 unit 통과만으로 lifecycle 회귀를 놓칠 수 있습니다. `agent-test/local/control-plane-smoke.md:55`는 Control Plane-Edge TCP wire 변경 시 existing local smoke 실행을 요구합니다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
focused tests에 `-race`를 포함해 relay loop/data race를 확인하고, 전체 Go 회귀 및 existing local Control Plane-Edge smoke를 실행합니다. 기존 smoke가 event 자체를 확인하지 않는다는 한계는 review stub에 남기고, event success는 focused integration tests 출력으로 증명합니다.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] focused package tests를 실행합니다.
|
||||
- [ ] race test를 relay 관련 package에 실행합니다.
|
||||
- [ ] 전체 Go 회귀와 existing local wire smoke를 실행합니다.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- 새 테스트는 EVENTS-1~4 항목에 둡니다.
|
||||
- 기존 smoke script 수정은 이 plan 범위 밖입니다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -race -count=1 ./apps/edge/internal/controlplane ./apps/control-plane/internal/wire
|
||||
go test ./...
|
||||
make test-control-plane-edge-wire
|
||||
```
|
||||
|
||||
기대 결과: race/focused/full tests와 existing local wire smoke가 통과합니다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/control-plane/internal/wire/edge.go` | EVENTS-1 |
|
||||
| `apps/control-plane/internal/wire/edge_test.go` | EVENTS-1 |
|
||||
| `apps/control-plane/internal/wire/edge_registry.go` | EVENTS-2 |
|
||||
| `apps/control-plane/internal/wire/edge_server.go` | EVENTS-2 |
|
||||
| `apps/control-plane/internal/wire/edge_server_test.go` | EVENTS-2 |
|
||||
| `apps/edge/internal/controlplane/connector.go` | EVENTS-3, EVENTS-4 |
|
||||
| `apps/edge/internal/controlplane/connector_test.go` | EVENTS-3 |
|
||||
| `apps/edge/internal/bootstrap/runtime.go` | EVENTS-3 |
|
||||
| `apps/edge/internal/bootstrap/runtime_test.go` | EVENTS-3 |
|
||||
| `apps/control-plane/cmd/control-plane/main.go` | EVENTS-4 |
|
||||
| `apps/control-plane/cmd/control-plane/main_test.go` | EVENTS-4 |
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
이 subtask directory는 `05+04_event_relay_boundary`이므로 runtime dependency source of truth는 `04_status_snapshot`입니다. runtime이 이 subtask를 시작했다면 선행 PASS가 충족된 것으로 보고, 수동 실행이면 predecessor PASS evidence 또는 runtime 완료 이벤트 경로를 먼저 기록합니다. 완료 후 EVENTS-1 -> EVENTS-2 -> EVENTS-3 -> EVENTS-4 -> EVENTS-5 순서로 진행합니다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/control-plane/internal/wire
|
||||
go test -count=1 ./apps/control-plane/cmd/control-plane
|
||||
go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap ./apps/edge/internal/events
|
||||
go test -race -count=1 ./apps/edge/internal/controlplane ./apps/control-plane/internal/wire
|
||||
go test ./...
|
||||
make test-control-plane-edge-wire
|
||||
rg --sort path -n "apps/edge/internal/node|NodeCommandRequest|RunRequest" apps/edge/internal/controlplane
|
||||
```
|
||||
|
||||
기대 결과: 선행 PASS evidence가 review stub에 기록되고 모든 Go/race/smoke 명령이 성공합니다. 마지막 `rg`는 controlplane package가 Edge node internals 또는 run/command dispatch 로직을 직접 복제하지 않았다는 boundary check로 사용하며, 의도된 proto type 언급이 있으면 review stub에 근거를 기록합니다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -177,6 +178,7 @@ func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error
|
|||
return fmt.Errorf("start edge wire server: %w", err)
|
||||
}
|
||||
defer func() { _ = edgeServer.Stop() }()
|
||||
registerEdgeRegistryHandlers(mux, edgeServer.Registry())
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.Server.Listen,
|
||||
|
|
@ -204,6 +206,76 @@ func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error
|
|||
}
|
||||
}
|
||||
|
||||
type edgeRegistryResponse struct {
|
||||
Edges []edgeRegistryView `json:"edges"`
|
||||
}
|
||||
|
||||
type edgeRegistryView struct {
|
||||
EdgeID string `json:"edge_id"`
|
||||
EdgeName string `json:"edge_name"`
|
||||
Version string `json:"version"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Protocol string `json:"protocol"`
|
||||
Connected bool `json:"connected"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
func registerEdgeRegistryHandlers(mux *http.ServeMux, registry *wire.EdgeRegistry) {
|
||||
mux.HandleFunc("/edges", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/edges" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", http.MethodGet)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
states := registry.Snapshots()
|
||||
views := make([]edgeRegistryView, 0, len(states))
|
||||
for _, state := range states {
|
||||
views = append(views, edgeRegistryViewFromState(state))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, edgeRegistryResponse{Edges: views})
|
||||
})
|
||||
mux.HandleFunc("/edges/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", http.MethodGet)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
edgeID := strings.TrimPrefix(r.URL.Path, "/edges/")
|
||||
if edgeID == "" || strings.Contains(edgeID, "/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
state, ok := registry.Snapshot(edgeID)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, edgeRegistryViewFromState(state))
|
||||
})
|
||||
}
|
||||
|
||||
func edgeRegistryViewFromState(state wire.EdgeConnectionState) edgeRegistryView {
|
||||
return edgeRegistryView{
|
||||
EdgeID: state.EdgeID,
|
||||
EdgeName: state.EdgeName,
|
||||
Version: state.Version,
|
||||
Capabilities: append([]string(nil), state.Capabilities...),
|
||||
Protocol: state.Protocol,
|
||||
Connected: state.Connected,
|
||||
LastSeen: state.LastSeen,
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func databaseLogFields(rawURL string) []zap.Field {
|
||||
return serviceURLLogFields(rawURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"iop/apps/control-plane/internal/wire"
|
||||
iop "iop/proto/gen/iop"
|
||||
)
|
||||
|
||||
func TestLoadConfigDatabaseDefaultsToLocal(t *testing.T) {
|
||||
|
|
@ -230,3 +237,81 @@ server:
|
|||
t.Fatalf("wire_listen should stay from YAML: got %q", cfg.Server.WireListen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeRegistryHTTPHandlersListAndGetEdge(t *testing.T) {
|
||||
registry := wire.NewEdgeRegistry()
|
||||
at := time.Unix(1780142200, 123).UTC()
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{
|
||||
EdgeId: "edge-b",
|
||||
EdgeName: "Edge B",
|
||||
Version: "1.2.3",
|
||||
Capabilities: []string{"run", "node-registry"},
|
||||
}, at)
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{
|
||||
EdgeId: "edge-a",
|
||||
EdgeName: "Edge A",
|
||||
Version: "1.0.0",
|
||||
}, at.Add(time.Second))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
registerEdgeRegistryHandlers(mux, registry)
|
||||
|
||||
listResp := httptest.NewRecorder()
|
||||
mux.ServeHTTP(listResp, httptest.NewRequest(http.MethodGet, "/edges", nil))
|
||||
if listResp.Code != http.StatusOK {
|
||||
t.Fatalf("GET /edges status=%d body=%s", listResp.Code, listResp.Body.String())
|
||||
}
|
||||
var list edgeRegistryResponse
|
||||
if err := json.Unmarshal(listResp.Body.Bytes(), &list); err != nil {
|
||||
t.Fatalf("decode list response: %v", err)
|
||||
}
|
||||
if len(list.Edges) != 2 {
|
||||
t.Fatalf("expected 2 edges, got %d", len(list.Edges))
|
||||
}
|
||||
if list.Edges[0].EdgeID != "edge-a" || list.Edges[1].EdgeID != "edge-b" {
|
||||
t.Fatalf("expected stable edge_id ordering, got %+v", list.Edges)
|
||||
}
|
||||
if list.Edges[1].Protocol != wire.Protocol {
|
||||
t.Fatalf("protocol: got %q want %q", list.Edges[1].Protocol, wire.Protocol)
|
||||
}
|
||||
if !list.Edges[1].Connected {
|
||||
t.Fatal("expected edge-b to be connected")
|
||||
}
|
||||
|
||||
getResp := httptest.NewRecorder()
|
||||
mux.ServeHTTP(getResp, httptest.NewRequest(http.MethodGet, "/edges/edge-b", nil))
|
||||
if getResp.Code != http.StatusOK {
|
||||
t.Fatalf("GET /edges/edge-b status=%d body=%s", getResp.Code, getResp.Body.String())
|
||||
}
|
||||
var got edgeRegistryView
|
||||
if err := json.Unmarshal(getResp.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode get response: %v", err)
|
||||
}
|
||||
if got.EdgeName != "Edge B" || got.Version != "1.2.3" {
|
||||
t.Fatalf("unexpected edge view: %+v", got)
|
||||
}
|
||||
if len(got.Capabilities) != 2 || got.Capabilities[0] != "run" || got.Capabilities[1] != "node-registry" {
|
||||
t.Fatalf("capabilities: got %+v", got.Capabilities)
|
||||
}
|
||||
if !got.LastSeen.Equal(at) {
|
||||
t.Fatalf("last_seen: got %s want %s", got.LastSeen, at)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeRegistryHTTPHandlersRejectUnsupportedCases(t *testing.T) {
|
||||
registry := wire.NewEdgeRegistry()
|
||||
mux := http.NewServeMux()
|
||||
registerEdgeRegistryHandlers(mux, registry)
|
||||
|
||||
postResp := httptest.NewRecorder()
|
||||
mux.ServeHTTP(postResp, httptest.NewRequest(http.MethodPost, "/edges", nil))
|
||||
if postResp.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("POST /edges status=%d body=%s", postResp.Code, postResp.Body.String())
|
||||
}
|
||||
|
||||
missingResp := httptest.NewRecorder()
|
||||
mux.ServeHTTP(missingResp, httptest.NewRequest(http.MethodGet, "/edges/missing-edge", nil))
|
||||
if missingResp.Code != http.StatusNotFound {
|
||||
t.Fatalf("GET /edges/missing-edge status=%d body=%s", missingResp.Code, missingResp.Body.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package wire
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -88,6 +89,25 @@ func (r *EdgeRegistry) Snapshot(edgeID string) (EdgeConnectionState, bool) {
|
|||
return snapshot, true
|
||||
}
|
||||
|
||||
// Snapshots returns a stable, copied view of all tracked Edge states.
|
||||
func (r *EdgeRegistry) Snapshots() []EdgeConnectionState {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
ids := make([]string, 0, len(r.edges))
|
||||
for id := range r.edges {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
snapshots := make([]EdgeConnectionState, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
state := r.edges[id]
|
||||
snapshot := *state
|
||||
snapshot.Capabilities = append([]string(nil), state.Capabilities...)
|
||||
snapshots = append(snapshots, snapshot)
|
||||
}
|
||||
return snapshots
|
||||
}
|
||||
|
||||
// Len returns the number of tracked Edge connections.
|
||||
func (r *EdgeRegistry) Len() int {
|
||||
r.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -225,6 +225,38 @@ func TestEdgeServerStaleDisconnectDoesNotClearReconnect(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEdgeRegistrySnapshotsAreSortedAndCopied(t *testing.T) {
|
||||
registry := NewEdgeRegistry()
|
||||
at := time.Unix(1780142300, 0).UTC()
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{
|
||||
EdgeId: "edge-b",
|
||||
EdgeName: "Edge B",
|
||||
Capabilities: []string{"run"},
|
||||
}, at)
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{
|
||||
EdgeId: "edge-a",
|
||||
EdgeName: "Edge A",
|
||||
Capabilities: []string{"status"},
|
||||
}, at.Add(time.Second))
|
||||
|
||||
snapshots := registry.Snapshots()
|
||||
if len(snapshots) != 2 {
|
||||
t.Fatalf("expected 2 snapshots, got %d", len(snapshots))
|
||||
}
|
||||
if snapshots[0].EdgeID != "edge-a" || snapshots[1].EdgeID != "edge-b" {
|
||||
t.Fatalf("expected snapshots sorted by edge_id, got %+v", snapshots)
|
||||
}
|
||||
|
||||
snapshots[0].Capabilities[0] = "mutated"
|
||||
again, ok := registry.Snapshot("edge-a")
|
||||
if !ok {
|
||||
t.Fatal("edge-a missing from registry")
|
||||
}
|
||||
if again.Capabilities[0] != "status" {
|
||||
t.Fatalf("snapshot capabilities alias registry storage: %+v", again.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeHeartbeatWaitExceedsInterval(t *testing.T) {
|
||||
if EdgeHeartbeatWaitSec <= EdgeHeartbeatIntervalSec {
|
||||
t.Fatalf("EdgeHeartbeatWaitSec (%d) must exceed EdgeHeartbeatIntervalSec (%d)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue