iop/apps/edge/internal/service/status_provider.go
toki 2f560e3f3b feat: provider pool admission, policy config, snapshot source task archive + runtime updates
- Archive completed subtask plans/code reviews (04, 05+03,04, 07+03)
- Add provider_pool_admission_test.go
- Update edge config types, load, catalog validation
- Update runtime, config refresh, service layers for admission
- Update test docs and inventory
- Update provider scheduling, resolution, tunnel, status modules
2026-07-19 22:41:05 +09:00

216 lines
6.9 KiB
Go

package service
import (
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
// NodeSnapshot is a surface-neutral view of a registered node, suitable for
// CLI, HTTP, or other transports. Label is the short display label (node0, node1).
// AgentKind and LifecycleState carry the registry entry's generic node
// classification and lifecycle.
type NodeSnapshot struct {
NodeID string
Alias string
Label string
AgentKind string
LifecycleState string
Config *iop.NodeConfigPayload
ProviderSnapshots []*iop.ProviderSnapshot
}
// ListNodeSnapshots returns surface-neutral node descriptors derived from the
// current registry contents. For nodes backed by a queue manager, the entire
// snapshot is built inside a single queue-held critical section so that
// runtime config, queue state, and candidate pressure are observed atomically
// under the same lock order (queue → service) that SetRuntimeConfig follows.
// Nodes without a queue fall back to the catalog-only path under s.mu.
func (s *Service) ListNodeSnapshots() []NodeSnapshot {
if s.queue == nil {
return s.buildSnapshotNoQueue()
}
// Queue-backed: acquire the queue lock for the entire snapshot build so
// the runtime config, resource state, and candidate pressure are captured
// as a single consistent old-or-new tuple (no torn mixed state).
s.queue.mu.Lock()
defer s.queue.mu.Unlock()
return s.buildSnapshotLocked()
}
// buildSnapshotNoQueue builds node snapshots using only the registry and
// catalog — no queue involvement. Used when the queue is disabled.
func (s *Service) buildSnapshotNoQueue() []NodeSnapshot {
entries := s.registry.All()
out := make([]NodeSnapshot, 0, len(entries))
store, _, _ := s.runtimeConfigSnapshot()
for _, entry := range entries {
snap := s.emptySnapshot(entry)
if store != nil {
if r, ok := store.FindByID(entry.NodeID); ok {
if payload, err := edgenode.BuildConfigPayload(r); err == nil {
snap.Config = payload
}
if len(r.Providers) > 0 {
snap.ProviderSnapshots = staticProviderCatalogSnapshots(r)
}
}
}
out = append(out, snap)
}
return out
}
// buildSnapshotLocked builds node snapshots under the assumption that the
// caller already holds s.queue.mu. ListNodeSnapshots uses this variant to keep
// runtime config and queue state inside one queue-held critical section.
func (s *Service) buildSnapshotLocked() []NodeSnapshot {
entries := s.registry.All()
store, _, _ := s.runtimeConfigSnapshot()
out := make([]NodeSnapshot, 0, len(entries))
for _, entry := range entries {
snap := s.emptySnapshot(entry)
if store != nil {
if r, ok := store.FindByID(entry.NodeID); ok {
if payload, err := edgenode.BuildConfigPayload(r); err == nil {
snap.Config = payload
}
if len(r.Providers) > 0 {
snap.ProviderSnapshots = s.queue.getSnapshotForNodeLocked(entry.NodeID, r)
} else {
snap.ProviderSnapshots = s.queue.getSnapshotForNodeLocked(entry.NodeID, r)
}
}
}
out = append(out, snap)
}
return out
}
// emptySnapshot returns a NodeSnapshot populated from the registry entry.
func (s *Service) emptySnapshot(entry *edgenode.NodeEntry) NodeSnapshot {
return NodeSnapshot{
NodeID: entry.NodeID,
Alias: entry.Alias,
Label: nodeLabel(entry),
AgentKind: entry.AgentKind,
LifecycleState: entry.LifecycleState,
}
}
// NodeEntrySnapshot converts a registry entry into the surface-neutral DTO.
func NodeEntrySnapshot(entry *edgenode.NodeEntry) NodeSnapshot {
if entry == nil {
return NodeSnapshot{}
}
return NodeSnapshot{
NodeID: entry.NodeID,
Alias: entry.Alias,
Label: nodeLabel(entry),
AgentKind: entry.AgentKind,
LifecycleState: entry.LifecycleState,
}
}
// staticProviderCatalogSnapshots builds catalog-based ProviderSnapshots with
// zero inflight/queued for nodes whose config has a providers[] catalog but
// whose service has no queue (e.g. queue disabled at startup).
func staticProviderCatalogSnapshots(rec *edgenode.NodeRecord) []*iop.ProviderSnapshot {
snaps := make([]*iop.ProviderSnapshot, 0, len(rec.Providers))
for _, prov := range rec.Providers {
if prov.ID == "" {
continue
}
servedModels := make([]string, len(prov.Models))
copy(servedModels, prov.Models)
lifecycleCaps := make([]string, len(prov.LifecycleCapabilities))
copy(lifecycleCaps, prov.LifecycleCapabilities)
if !config.ProviderEnabled(prov) {
snaps = append(snaps, &iop.ProviderSnapshot{
Adapter: prov.Adapter,
Status: "disabled",
Capacity: 0,
Id: prov.ID,
Type: prov.Type,
Category: string(prov.Category),
ServedModels: servedModels,
Health: "disabled",
LifecycleCapabilities: lifecycleCaps,
LongContextCapacity: 0,
LongInFlight: 0,
LongQueued: 0,
})
continue
}
snaps = append(snaps, &iop.ProviderSnapshot{
Adapter: prov.Adapter,
Status: "available",
Capacity: int32(prov.Capacity),
Id: prov.ID,
Type: prov.Type,
Category: string(prov.Category),
ServedModels: servedModels,
Health: prov.Health,
LifecycleCapabilities: lifecycleCaps,
LongContextCapacity: int32(prov.LongContextCapacity),
LongInFlight: 0,
LongQueued: 0,
})
}
return snaps
}
func (s *Service) ResolveNode(ref string) (*edgenode.NodeEntry, error) {
return s.registry.Resolve(ref)
}
// ResolveNodeSnapshot returns a surface-neutral DTO for the resolved node.
func (s *Service) ResolveNodeSnapshot(ref string) (NodeSnapshot, error) {
entry, err := s.ResolveNode(ref)
if err != nil {
return NodeSnapshot{}, err
}
return NodeEntrySnapshot(entry), nil
}
func (s *Service) GetCapabilities() []*iop.EdgeCapabilitySummary {
var entries []*edgenode.NodeEntry
if s.registry != nil {
entries = s.registry.All()
}
if len(entries) == 0 {
return nil
}
return []*iop.EdgeCapabilitySummary{
{
Kind: "run",
Available: true,
Status: "ready",
Summary: "Generic execution node available",
},
}
}
func (s *Service) GetDomainAgents() []*iop.EdgeDomainAgentSummary {
return nil
}
// HealthStatusView is the Edge-owned status summary used by control commands
// such as health.check. It is derived from the live node registry and
// capability snapshot rather than a fixed simulation delay.
type HealthStatusView struct {
Nodes []NodeSnapshot
Capabilities []*iop.EdgeCapabilitySummary
}
// HealthStatus returns a status summary derived from the current node registry
// and capability snapshot. It is the real Edge-owned boundary that backs the
// health.check control command.
func (s *Service) HealthStatus() HealthStatusView {
return HealthStatusView{
Nodes: s.ListNodeSnapshots(),
Capabilities: s.GetCapabilities(),
}
}