mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Cache mock unified snapshots instead of rebuilding registries per read
In mock mode every unified read-state access built two throwaway registries: mock.UnifiedResourceSnapshot constructed one to derive the resource list, and the monitor's currentUnifiedStateView ingested that list into another, deep-cloning all resources both ways. Chart requests, broadcasts, alert evaluation, and API reads each repaid that full cost — the dominant share of the demo's 76TB/9.5d allocation churn, since every one of those reads runs against a world that only changes on the 2-second mock tick. Introduce fixtureDataVersion, a token that advances on every observable mock-graph change (metric ticks and the structural changes that bump fixtureRevision, which stays structural-only so seeded trend history remains reusable). Memoize the package-level UnifiedResourceSnapshot and the monitor's mock-branch state view against it, so consumers between ticks share one immutable build. Sharing mirrors the semantics the persistent-store ReadState path has always had in real mode: all consumers were audited — they ingest (which clones), copy before top-level writes, or build fresh outputs. Real-mode paths are untouched. Contract-Neutral: mock snapshot memoization: identical data served from cache, no contract delta
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func withMockEnabledForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
previous := IsMockEnabled()
|
||||
if err := SetEnabled(true); err != nil {
|
||||
t.Fatalf("enable mock mode: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := SetEnabled(previous); err != nil {
|
||||
t.Fatalf("restore mock mode: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFixtureDataVersionAdvancesOnMetricTick(t *testing.T) {
|
||||
withMockEnabledForTest(t)
|
||||
|
||||
before := FixtureDataVersion()
|
||||
updateMetrics(GetConfig())
|
||||
after := FixtureDataVersion()
|
||||
|
||||
if after <= before {
|
||||
t.Fatalf("expected FixtureDataVersion to advance on a metric tick, got %d -> %d", before, after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifiedResourceSnapshotMemoizedPerDataVersion(t *testing.T) {
|
||||
withMockEnabledForTest(t)
|
||||
|
||||
first, firstFreshness := UnifiedResourceSnapshot()
|
||||
if len(first) == 0 {
|
||||
t.Fatal("expected mock resources")
|
||||
}
|
||||
second, secondFreshness := UnifiedResourceSnapshot()
|
||||
if len(second) != len(first) {
|
||||
t.Fatalf("expected identical memoized result, got %d vs %d resources", len(first), len(second))
|
||||
}
|
||||
if &first[0] != &second[0] {
|
||||
t.Fatal("expected the memoized snapshot to be returned for an unchanged data version")
|
||||
}
|
||||
if !firstFreshness.Equal(secondFreshness) {
|
||||
t.Fatalf("expected identical freshness, got %v vs %v", firstFreshness, secondFreshness)
|
||||
}
|
||||
|
||||
updateMetrics(GetConfig())
|
||||
|
||||
third, _ := UnifiedResourceSnapshot()
|
||||
if len(third) == 0 {
|
||||
t.Fatal("expected mock resources after tick")
|
||||
}
|
||||
if &first[0] == &third[0] {
|
||||
t.Fatal("expected a rebuilt snapshot after the data version advanced")
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,16 @@ var (
|
||||
mockConfig = DefaultConfig
|
||||
enabled atomic.Bool
|
||||
fixtureRevision atomic.Uint64
|
||||
updateEveryNS atomic.Int64
|
||||
updateTicker *time.Ticker
|
||||
stopUpdatesCh chan struct{}
|
||||
updateLoopWg sync.WaitGroup
|
||||
// fixtureDataVersion advances on EVERY observable mock-graph change:
|
||||
// metric ticks as well as the structural changes that bump
|
||||
// fixtureRevision. Caches of snapshots derived from the graph key on
|
||||
// this; fixtureRevision stays structural-only so the expensive seeded
|
||||
// trend history can be reused across monitor restarts within a process.
|
||||
fixtureDataVersion atomic.Uint64
|
||||
updateEveryNS atomic.Int64
|
||||
updateTicker *time.Ticker
|
||||
stopUpdatesCh chan struct{}
|
||||
updateLoopWg sync.WaitGroup
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -170,6 +176,7 @@ func enableMockMode(config MockConfig, fromInit bool) {
|
||||
mockConfig = config
|
||||
mockGraph = buildFixtureGraph(config, now)
|
||||
fixtureRevision.Add(1)
|
||||
fixtureDataVersion.Add(1)
|
||||
enabled.Store(true)
|
||||
dataMu.Unlock()
|
||||
startUpdateLoop()
|
||||
@@ -205,6 +212,7 @@ func disableMockMode() {
|
||||
dataMu.Lock()
|
||||
mockGraph = emptyFixtureGraph()
|
||||
fixtureRevision.Add(1)
|
||||
fixtureDataVersion.Add(1)
|
||||
dataMu.Unlock()
|
||||
|
||||
log.Info().Msg("mock mode disabled")
|
||||
@@ -270,6 +278,15 @@ func updateMetrics(cfg MockConfig) {
|
||||
defer dataMu.Unlock()
|
||||
|
||||
mockGraph.UpdateMetrics(cfg, time.Now())
|
||||
fixtureDataVersion.Add(1)
|
||||
}
|
||||
|
||||
// FixtureDataVersion returns a token that advances on every observable
|
||||
// mock-graph change (metric ticks and structural changes alike). Derived
|
||||
// snapshots cached against this token are current for as long as it is
|
||||
// unchanged.
|
||||
func FixtureDataVersion() uint64 {
|
||||
return fixtureDataVersion.Load()
|
||||
}
|
||||
|
||||
// GetConfig returns the current mock configuration.
|
||||
@@ -478,6 +495,7 @@ func SetMockConfig(cfg MockConfig) {
|
||||
if configChanged && enabled.Load() {
|
||||
mockGraph = buildFixtureGraph(normalized, time.Now())
|
||||
fixtureRevision.Add(1)
|
||||
fixtureDataVersion.Add(1)
|
||||
}
|
||||
dataMu.Unlock()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package mock
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
||||
@@ -159,12 +160,46 @@ func SupplementalOwnedSources() []unifiedresources.DataSource {
|
||||
}
|
||||
}
|
||||
|
||||
// unifiedSnapshotMemo caches the package-level UnifiedResourceSnapshot result
|
||||
// per FixtureDataVersion. Building the snapshot constructs a full throwaway
|
||||
// registry (ingest-clone in, List-clone out) — before this memo, every
|
||||
// consumer paid that on every read, which dominated the demo's allocation
|
||||
// profile.
|
||||
var unifiedSnapshotMemo struct {
|
||||
mu sync.Mutex
|
||||
valid bool
|
||||
version uint64
|
||||
resources []unifiedresources.Resource
|
||||
freshness time.Time
|
||||
}
|
||||
|
||||
// UnifiedResourceSnapshot returns the current mock world as unified
|
||||
// resources. The returned slice and the nested data of its elements are
|
||||
// shared between callers — treat them as read-only. Every current consumer
|
||||
// either ingests them into a registry (which deep-clones) or copies the slice
|
||||
// before writing top-level fields.
|
||||
func UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
||||
if !IsMockEnabled() {
|
||||
return nil, time.Time{}
|
||||
}
|
||||
|
||||
return CurrentFixtureGraph().UnifiedResourceSnapshot()
|
||||
unifiedSnapshotMemo.mu.Lock()
|
||||
defer unifiedSnapshotMemo.mu.Unlock()
|
||||
|
||||
// Read the version before fetching the graph: a tick that lands between
|
||||
// the two can only make the cached data newer than its token, which
|
||||
// forces a harmless rebuild on the next call — never a stale serve.
|
||||
version := FixtureDataVersion()
|
||||
if unifiedSnapshotMemo.valid && unifiedSnapshotMemo.version == version {
|
||||
return unifiedSnapshotMemo.resources, unifiedSnapshotMemo.freshness
|
||||
}
|
||||
|
||||
resources, freshness := CurrentFixtureGraph().UnifiedResourceSnapshot()
|
||||
unifiedSnapshotMemo.valid = true
|
||||
unifiedSnapshotMemo.version = version
|
||||
unifiedSnapshotMemo.resources = resources
|
||||
unifiedSnapshotMemo.freshness = freshness
|
||||
return resources, freshness
|
||||
}
|
||||
|
||||
func (g FixtureGraph) UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||
)
|
||||
|
||||
func TestCurrentUnifiedStateViewCachedBetweenMockTicks(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
mustSetMockEnabled(t, true)
|
||||
t.Cleanup(func() { mustSetMockEnabled(t, previous) })
|
||||
|
||||
m := &Monitor{}
|
||||
|
||||
first := m.currentUnifiedStateView()
|
||||
if first.readState == nil {
|
||||
t.Fatal("expected a read state from the mock branch")
|
||||
}
|
||||
second := m.currentUnifiedStateView()
|
||||
if first.readState != second.readState {
|
||||
t.Fatal("expected the cached view while the fixture data version is unchanged")
|
||||
}
|
||||
if len(first.resources) == 0 || len(second.resources) != len(first.resources) {
|
||||
t.Fatalf("expected identical resource sets, got %d vs %d", len(first.resources), len(second.resources))
|
||||
}
|
||||
|
||||
// Toggling mock mode rebuilds the fixture graph and advances the data
|
||||
// version, which must invalidate the cached view.
|
||||
mustSetMockEnabled(t, false)
|
||||
mustSetMockEnabled(t, true)
|
||||
|
||||
third := m.currentUnifiedStateView()
|
||||
if third.readState == nil {
|
||||
t.Fatal("expected a read state after the fixture graph rebuilt")
|
||||
}
|
||||
if third.readState == first.readState {
|
||||
t.Fatal("expected a rebuilt view after the fixture data version advanced")
|
||||
}
|
||||
}
|
||||
@@ -1055,6 +1055,10 @@ type Monitor struct {
|
||||
config *config.Config
|
||||
state *models.State
|
||||
orgID string // Organization ID for tenant isolation (empty = default/legacy)
|
||||
mockUnifiedViewMu sync.Mutex
|
||||
mockUnifiedView monitorUnifiedStateView
|
||||
mockUnifiedViewVersion uint64
|
||||
mockUnifiedViewValid bool
|
||||
pveClients map[string]PVEClientInterface
|
||||
pbsClients map[string]*pbs.Client
|
||||
pmgClients map[string]*pmg.Client
|
||||
@@ -4577,9 +4581,30 @@ func (m *Monitor) currentUnifiedStateView() monitorUnifiedStateView {
|
||||
}
|
||||
|
||||
if mock.IsMockEnabled() {
|
||||
// Read the version before the snapshot so a tick landing in between
|
||||
// caches newer data under an older token (harmless rebuild next
|
||||
// call) rather than ever serving stale data under a newer one.
|
||||
version := mock.FixtureDataVersion()
|
||||
m.mockUnifiedViewMu.Lock()
|
||||
if m.mockUnifiedViewValid && m.mockUnifiedViewVersion == version {
|
||||
view := m.mockUnifiedView
|
||||
m.mockUnifiedViewMu.Unlock()
|
||||
return view
|
||||
}
|
||||
m.mockUnifiedViewMu.Unlock()
|
||||
|
||||
resources, freshness := mock.UnifiedResourceSnapshot()
|
||||
if len(resources) > 0 || !freshness.IsZero() {
|
||||
return monitorUnifiedStateViewFromResources(resources, freshness)
|
||||
// Consumers share this view between ticks, mirroring the
|
||||
// sharing semantics the persistent-store ReadState path has
|
||||
// always had in real mode: views are read-only.
|
||||
view := monitorUnifiedStateViewFromResources(resources, freshness)
|
||||
m.mockUnifiedViewMu.Lock()
|
||||
m.mockUnifiedView = view
|
||||
m.mockUnifiedViewVersion = version
|
||||
m.mockUnifiedViewValid = true
|
||||
m.mockUnifiedViewMu.Unlock()
|
||||
return view
|
||||
}
|
||||
return monitorUnifiedStateViewFromSnapshot(m.GetState())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user