Files
pulse/internal/monitoring/identity_flap_tracker.go
T
rcourtman 245177e531 Restore backend lint to green: real dedup + errcheck idiom fixes
golangci-lint had accumulated 12 findings since 5abb2d8f4. All fixed with
real dedup (no nolint suppressions) and the repo's existing errcheck idioms:

- dupl internal/monitoring: docker/host identity-conflict trackers were
  structural clones; extracted a shared identityFlapTracker core with a
  domain-neutral identityConflict result. Per-domain files now hold only
  the window const and the model translation. Tracker-behavior tests
  consolidated into identity_flap_tracker_test.go; Monitor-level
  translation and Apply*Report integration tests remain per domain.
- dupl internal/api/router.go: VM/container workload chart loops shared a
  16-line live-fallback block; extracted guestChartSeriesWithLiveFallback
  over a guestLiveMetricsView interface both views satisfy.
- dupl internal/storagehealth/risk.go: SMART attribute copying extracted
  into applySMARTAttributes shared by both assessors (same
  *models.SMARTAttributes type on both inputs).
- errcheck pkg/audit/sqlite_logger.go: three defer tx.Rollback() sites
  now use the repo-wide defer func() { _ = tx.Rollback() }() idiom.
- errcheck telemetry/notifications tests: send() errors now fail the
  test; queue.Stop() uses the package's _ = idiom.

Full test suites pass for all six touched packages.

Contract-Neutral: lint-hygiene restoration: dupl dedup (identical logic extracted to shared helpers) and errcheck idiom fixes; no public-contract or behavioral delta
2026-08-03 01:01:20 +01:00

142 lines
4.2 KiB
Go

package monitoring
import (
"sort"
"strings"
"time"
)
// identityFlapTracker is the shared revisit-detection core behind the
// Docker-host and host-agent identity conflict trackers. It watches the
// stream of (hostname, secondary) identity pairs folded into a single
// resource identity and detects when two distinct machines are behind it.
//
// The signal is a *revisit*: a value switches away from the previous one and
// back to one already seen inside the window. A genuine rename transitions
// exactly once and never revisits the old value, so it does not trip the
// detector; clones that share an identity alternate every report cycle and
// trip it immediately. The window also controls how long a detected conflict
// stays visible after the flapping stops.
//
// What the secondary field means (machine ID, report IP) is the adapter's
// business; this core only tracks values.
type identityFlapTracker struct {
window time.Duration
hostnames map[string]time.Time
secondaries map[string]time.Time
lastHostname string
lastSecondary string
conflictSince time.Time
conflictLastSeen time.Time
}
// identityConflict is the tracker's domain-neutral verdict; adapters
// translate it into their platform's conflict model.
type identityConflict struct {
hostnames []string
// secondaries is nil unless the secondary values themselves diverge; when
// clones share one machine ID / report IP the hostname list already
// carries the story.
secondaries []string
firstSeen time.Time
lastSeen time.Time
}
func newIdentityFlapTracker(window time.Duration) *identityFlapTracker {
return &identityFlapTracker{
window: window,
hostnames: make(map[string]time.Time),
secondaries: make(map[string]time.Time),
}
}
// observe records one report's identity fields and returns the active
// conflict, or nil when the identity looks healthy.
func (t *identityFlapTracker) observe(hostname, secondary string, now time.Time) *identityConflict {
hostname = strings.TrimSpace(hostname)
secondary = strings.TrimSpace(secondary)
pruneOlderThan(t.hostnames, now.Add(-t.window))
pruneOlderThan(t.secondaries, now.Add(-t.window))
revisit := false
if hostname != "" {
if _, seen := t.hostnames[hostname]; seen && t.lastHostname != "" && t.lastHostname != hostname {
revisit = true
}
t.hostnames[hostname] = now
t.lastHostname = hostname
}
if secondary != "" {
if _, seen := t.secondaries[secondary]; seen && t.lastSecondary != "" && t.lastSecondary != secondary {
revisit = true
}
t.secondaries[secondary] = now
t.lastSecondary = secondary
}
if revisit {
if t.conflictSince.IsZero() || now.Sub(t.conflictLastSeen) > t.window {
t.conflictSince = now
}
t.conflictLastSeen = now
}
if t.conflictLastSeen.IsZero() || now.Sub(t.conflictLastSeen) > t.window {
t.conflictSince = time.Time{}
t.conflictLastSeen = time.Time{}
return nil
}
conflict := &identityConflict{
hostnames: sortedKeys(t.hostnames),
firstSeen: t.conflictSince,
lastSeen: t.conflictLastSeen,
}
if len(t.secondaries) > 1 {
conflict.secondaries = sortedKeys(t.secondaries)
}
return conflict
}
// observeIdentityFlap feeds one report's identity fields into the flap
// tracker for the resolved identifier, lazily creating the tracker map and
// entry, and returns the active conflict, if any. Callers must not hold m.mu.
func (m *Monitor) observeIdentityFlap(trackers *map[string]*identityFlapTracker, window time.Duration, identifier, hostname, secondary string, now time.Time) *identityConflict {
if strings.TrimSpace(identifier) == "" {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
if *trackers == nil {
*trackers = make(map[string]*identityFlapTracker)
}
tracker, ok := (*trackers)[identifier]
if !ok {
tracker = newIdentityFlapTracker(window)
(*trackers)[identifier] = tracker
}
return tracker.observe(hostname, secondary, now)
}
func pruneOlderThan(entries map[string]time.Time, cutoff time.Time) {
for key, seenAt := range entries {
if seenAt.Before(cutoff) {
delete(entries, key)
}
}
}
func sortedKeys(entries map[string]time.Time) []string {
keys := make([]string, 0, len(entries))
for key := range entries {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}