Merge pull request #2228 from rcourtman/claude/unified-read-state-clone

Stop copying the resource registry on every read-state lookup
This commit is contained in:
rcourtman
2026-09-24 09:12:54 +01:00
committed by GitHub
2 changed files with 89 additions and 0 deletions
+13
View File
@@ -5027,6 +5027,19 @@ func (m *Monitor) GetUnifiedReadState() unifiedresources.ReadState {
// ephemeral snapshot-backed adapter to preserve read access without exposing
// direct state reads to consumer packages.
func (m *Monitor) GetUnifiedReadStateOrSnapshot() unifiedresources.ReadState {
if m == nil {
return nil
}
// A store that is itself the read state needs no view. Building one
// deep-clones every resource, twice, only for this caller to discard the
// copies, and alert evaluation reaches here once per resource per poll
// through the default CPU evaluation window, which made every poll cost
// grow with the square of the estate (#2199).
if !mock.IsMockEnabled() {
if readState := m.GetUnifiedReadState(); readState != nil {
return m.readStateWithStandaloneHostContinuity(readState)
}
}
return m.currentUnifiedStateView().readState
}
@@ -0,0 +1,76 @@
package monitoring
import (
"fmt"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
// Alert evaluation resolves a metrics target once per resource per poll, so a
// read-state lookup that copies the registry makes every poll quadratic in
// estate size (#2199). These tests pin the lookup to the live store.
func newReadStateCloneTestMonitor(t testing.TB, vms int) (*Monitor, *unifiedresources.MonitorAdapter, string) {
t.Helper()
now := time.Now().UTC()
snapshot := models.StateSnapshot{LastUpdate: now}
for i := 0; i < vms; i++ {
snapshot.VMs = append(snapshot.VMs, models.VM{
ID: fmt.Sprintf("lab:pve-%d:%d", i%8, 100+i),
VMID: 100 + i,
Name: fmt.Sprintf("vm-%d", i),
Node: fmt.Sprintf("pve-%d", i%8),
Instance: "lab",
Status: "running",
Type: "qemu",
CPU: 0.1,
Memory: models.Memory{Total: 4 << 30, Used: 1 << 30},
LastSeen: now,
})
}
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(snapshot)
adapter := unifiedresources.NewMonitorAdapter(registry)
m := &Monitor{state: models.NewState(), resourceStore: adapter}
vmID := ""
for _, resource := range adapter.GetAll() {
if resource.Type == unifiedresources.ResourceTypeVM {
vmID = resource.ID
break
}
}
if vmID == "" {
t.Fatal("fixture produced no VM resource")
}
return m, adapter, vmID
}
func TestGetUnifiedReadStateOrSnapshotReturnsLiveStore(t *testing.T) {
m, adapter, _ := newReadStateCloneTestMonitor(t, 4)
readState := m.GetUnifiedReadStateOrSnapshot()
got, ok := readState.(*unifiedresources.MonitorAdapter)
if !ok || got != adapter {
t.Fatalf("read state = %T %p, want the configured store %p", readState, got, adapter)
}
}
func TestMetricsTargetForResourceDoesNotCopyRegistry(t *testing.T) {
small, _, smallID := newReadStateCloneTestMonitor(t, 8)
large, _, largeID := newReadStateCloneTestMonitor(t, 1000)
if small.MetricsTargetForResource(smallID) == nil || large.MetricsTargetForResource(largeID) == nil {
t.Fatal("expected metrics targets for fixture VMs")
}
smallAllocs := testing.AllocsPerRun(20, func() { _ = small.MetricsTargetForResource(smallID) })
largeAllocs := testing.AllocsPerRun(20, func() { _ = large.MetricsTargetForResource(largeID) })
// A registry copy costs several allocations per resource, so any growth
// from 8 to 1,000 resources means the lookup is copying the estate again.
if largeAllocs > smallAllocs+8 {
t.Fatalf("MetricsTargetForResource allocations grew with estate size: %.0f at 8 VMs, %.0f at 1000 VMs", smallAllocs, largeAllocs)
}
}