iop/apps/edge/internal/node/store.go
toki 4dcf6f0cf9 feat(edge): provider 리소스 승인 소유권 정렬 구현
- 노드 연결 2단계 핸드셰이크 추가 (Register→NodeReady→DispatchReady)
- ConnectionGeneration 기반 연결 세대 관리로 stale 연결 차폐
- configured 노드 catalog 기반 snapshot rebuild (Connected 상태 분리)
- provider 리스 소유권 일원화: edge가 소유권 승인·반환 전까지 대기
- 모델 대기열 승인/해제/스냅샷 서비스 구현
- 재연결 준비도 통합 테스트, 아카이브된 하위태스크 8건 포함
2026-07-22 18:10:54 +09:00

122 lines
2.9 KiB
Go

package node
import (
"fmt"
"sort"
"sync"
"github.com/google/uuid"
"iop/packages/go/config"
)
// NodeRecord is the pre-registered node definition stored in edge.
type NodeRecord struct {
ID string
Alias string
Token string
AgentKind string
Index int
Adapters config.AdaptersConf
Providers []config.NodeProviderConf
Runtime config.RuntimeConf
}
// NodeStore holds pre-registered node definitions, keyed by token.
type NodeStore struct {
mu sync.RWMutex
byToken map[string]*NodeRecord
byID map[string]*NodeRecord
}
func NewNodeStore() *NodeStore {
return &NodeStore{
byToken: make(map[string]*NodeRecord),
byID: make(map[string]*NodeRecord),
}
}
func (s *NodeStore) Add(rec *NodeRecord) {
s.mu.Lock()
defer s.mu.Unlock()
s.byToken[rec.Token] = rec
s.byID[rec.ID] = rec
}
func (s *NodeStore) FindByToken(token string) (*NodeRecord, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
r, ok := s.byToken[token]
return r, ok
}
func (s *NodeStore) FindByID(id string) (*NodeRecord, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
r, ok := s.byID[id]
return r, ok
}
// All returns all configured NodeRecords in deterministic, ascending Index order.
// Callers (e.g. status snapshot builder) rely on this ordering so the surface
// exposed to CLI/HTTP/Control Plane is reproducible across calls.
func (s *NodeStore) All() []*NodeRecord {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]*NodeRecord, 0, len(s.byID))
for _, r := range s.byID {
out = append(out, r)
}
sort.Slice(out, func(i, j int) bool {
return out[i].Index < out[j].Index
})
return out
}
// LoadFromConfig seeds the store from EdgeConfig.Nodes.
func LoadFromConfig(defs []config.NodeDefinition) (*NodeStore, error) {
s := NewNodeStore()
seenToken := make(map[string]bool)
seenAlias := make(map[string]bool)
seenID := make(map[string]bool)
for i, d := range defs {
if d.Token == "" {
return nil, fmt.Errorf("node[%d] alias=%q: token must not be empty", i, d.Alias)
}
if seenToken[d.Token] {
return nil, fmt.Errorf("node[%d] alias=%q: duplicate token", i, d.Alias)
}
if seenAlias[d.Alias] {
return nil, fmt.Errorf("node[%d] alias=%q: duplicate alias", i, d.Alias)
}
if d.ID != "" && seenID[d.ID] {
return nil, fmt.Errorf("node[%d] alias=%q: duplicate id %q", i, d.Alias, d.ID)
}
seenToken[d.Token] = true
seenAlias[d.Alias] = true
nodeID := d.ID
if nodeID == "" {
nodeID = uuid.NewString()
} else {
seenID[d.ID] = true
}
agentKind := d.AgentKind
if agentKind == "" {
agentKind = config.AgentKindGenericNode
}
adapters := d.Adapters
if err := config.NormalizeAdapters(&adapters); err != nil {
return nil, fmt.Errorf("node[%d] alias=%q: adapters: %w", i, d.Alias, err)
}
s.Add(&NodeRecord{
ID: nodeID,
Alias: d.Alias,
Token: d.Token,
AgentKind: agentKind,
Index: i,
Adapters: adapters,
Providers: d.Providers,
Runtime: d.Runtime,
})
}
return s, nil
}