Pulse/internal/monitoring/platform_poller_shared.go
rcourtman b855b21c7f Dedupe monitoring pollers behind shared provider, lifecycle, and seeding helpers
Clears the six dupl pairs in internal/monitoring:

- poll_providers.go: PVE/PBS/PMG listInstances / describeInstances /
  connectionStatus closures collapse into generic sortedClientNames,
  describeProviderInstances, and providerConnectionStatuses helpers; the
  PBS/PMG adapters are now built by newPrefixedPollProvider from a
  prefixedPollProviderSpec (prefix drives status keys, health keys, and
  the fallback instance name).
- truenas_poller.go / vmware_poller.go: the Start scheduling loop
  (double-start guard, stopped-channel handshake, sync+poll cadence)
  moves to startPollerLoop, and the active-connection config policy
  (enabled instances keyed by trimmed connection ID, defaults applied)
  moves to loadActiveInstanceConfigs, both in the new
  platform_poller_shared.go.
- monitor_polling_vm.go / monitor_polling_containers.go: traditional
  polling now records guest series via the existing canonical
  recordGuestMetric helper (io/network sentinels preserve the historical
  cpu/memory/disk-only behavior on this path); the source-shape
  guardrails in canonical_guardrails_test.go and
  memory_source_catalog_test.go pin the new delegation.
- mock_metrics_history.go: native and TrueNAS disk seeding share one
  seedDiskTelemetry closure.

Proof: new TestTrueNASPollerDoubleStartKeepsSingleLoop pins the shared
lifecycle loop; new TestSeedMockMetricsHistory_DiskTelemetryParityAcrossNativeAndTrueNAS
pins four-series parity for both disk sources. Contract Extension Points
name the shared wiring layer and poller scaffold.

Full internal/monitoring test suite passes.
2026-06-10 09:19:51 +01:00

115 lines
2.6 KiB
Go

package monitoring
import (
"context"
"strings"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
// startPollerLoop runs the scheduling scaffold shared by the periodic
// platform pollers (TrueNAS, VMware): guard against double-start under mu,
// then sync + poll immediately and again on every nextWaitDuration tick until
// the context is cancelled. cancelSlot and stoppedSlot point at the poller's
// lifecycle fields so Stop keeps working against the same state.
func startPollerLoop(
ctx context.Context,
mu *sync.Mutex,
cancelSlot *context.CancelFunc,
stoppedSlot *chan struct{},
syncConnections func(),
pollAll func(context.Context),
nextWaitDuration func(time.Time) time.Duration,
) {
if ctx == nil {
ctx = context.Background()
}
mu.Lock()
if *cancelSlot != nil {
mu.Unlock()
return
}
runCtx, cancel := context.WithCancel(ctx)
*cancelSlot = cancel
*stoppedSlot = make(chan struct{})
stopped := *stoppedSlot
mu.Unlock()
go func() {
defer close(stopped)
defer func() {
mu.Lock()
if *stoppedSlot == stopped {
*cancelSlot = nil
}
mu.Unlock()
}()
syncConnections()
pollAll(runCtx)
for {
wait := nextWaitDuration(time.Now())
timer := time.NewTimer(wait)
select {
case <-runCtx.Done():
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
return
case <-timer.C:
syncConnections()
pollAll(runCtx)
}
}
}()
}
// loadActiveInstanceConfigs resolves the active platform connection configs
// for an org: prefer the poller's cached map, otherwise load from per-org
// persistence and keep enabled instances keyed by trimmed connection ID with
// defaults applied. Shared by the TrueNAS and VMware pollers so the "what
// counts as an active connection" policy stays single-sourced.
func loadActiveInstanceConfigs[T any](
cached map[string]T,
multiTenant *config.MultiTenantPersistence,
orgID string,
load func(*config.ConfigPersistence) ([]T, error),
applyDefaults func(*T),
enabled func(T) bool,
connID func(T) string,
) map[string]T {
if len(cached) > 0 || multiTenant == nil {
return cached
}
persistence, err := multiTenant.GetPersistence(orgID)
if err != nil || persistence == nil {
return cached
}
instances, err := load(persistence)
if err != nil {
return cached
}
active := make(map[string]T)
for i := range instances {
instance := instances[i]
applyDefaults(&instance)
if !enabled(instance) {
continue
}
id := strings.TrimSpace(connID(instance))
if id == "" {
continue
}
active[id] = instance
}
return active
}