a73x

internal/server/registry/registry.go

Ref:   Size: 7.9 KiB   History

// Package registry holds volatile actual state in memory. A server restart
// loses nothing meaningful: agents reconnect and re-report (spec).
package registry

import (
	"bytes"
	"slices"
	"sync"
	"time"
)

// OnlineWindow is how long a host may go without a report before it is marked
// offline. It is COUPLED to the agent's report cadence
// (syncclient.DefaultTickInterval, 10s): the invariant
// OnlineWindow >= 3*DefaultTickInterval keeps a healthy host from flapping
// Online/Offline on a couple of jittered/missed reports. Lowering this (or
// raising the agent tick) without preserving that margin re-introduces flapping;
// the two live in different packages, so an invariant test in syncclient guards
// the relationship.
const OnlineWindow = 30 * time.Second

// StaleWindow is the "degrading" threshold: a host whose last report is older
// than this — but still within OnlineWindow — is Online yet Stale, the early
// warning that sync is flapping or falling behind before it drops fully offline.
const StaleWindow = OnlineWindow / 2

type Capacity struct{ VCPUs, MemMB, DiskGB int64 }

// Metrics is live, measured host utilization (never persisted). It rides each
// report into the registry and is dropped when the host goes offline. Distinct
// from Capacity (host totals) and from allocation (control-plane bookkeeping).
type Metrics struct {
	UptimeS                   int64
	MemUsedMB, MemAvailableMB int64
	Load1, Load5, Load15      float64
	DiskUsedGB, DiskFreeGB    int64
}

type VMStatus struct {
	VMID, PowerState, Phase, IP, LastError string
	// StatusDetail is what the host is doing about this VM right now, in the
	// host's own words. Empty is the normal state of a settled VM, and is also
	// what an agent too old to say anything leaves behind.
	StatusDetail string
}

type QuarantinedVM struct {
	VMID, Name    string
	VMSpecJSON    []byte
	DestroyAtUnix int64
}

// ExposureStatus is one exposure's live state as its host's agent reports it:
// "active" once the host listener is bound, "failed" with the OS error
// otherwise. Never persisted — like Metrics, it lives only while the host is
// connected, and an exposure with no row here has simply not been reported on
// yet.
type ExposureStatus struct {
	ID, State, Reason string
	// Sessions is what the port has carried, or nil when the agent serving it
	// does not count — an agent older than the counters. Nil is deliberately
	// distinguishable from a zeroed struct: one is "nobody said", the other is
	// "nothing has happened", and only the second is a fact worth showing.
	Sessions *ExposureSessions
}

// ExposureSessions is one published port's traffic as its host counts it:
// Active right now, Refused and Dropped cumulatively since that agent started.
// See ExposureSessions in the proto for why the two totals are not gauges.
type ExposureSessions struct{ Active, Refused, Dropped int64 }

// VolumeStatus is what a host found on disk for one volume id: whether the
// file is there, and how big it is. Never persisted — like ExposureStatus it
// lives only while the host is connected. It is also the only evidence a
// tombstoned volume's row may be reaped on, so Present=false is a statement
// the agent makes, never an absence the server infers.
type VolumeStatus struct {
	VolumeID string
	Present  bool
	SizeGB   int64
}

type Report struct {
	VMs            []VMStatus
	Quarantined    []QuarantinedVM
	Exposures      []ExposureStatus
	Volumes        []VolumeStatus
	Capacity       Capacity
	Metrics        Metrics
	FenceViolation bool
	LastSeenEpoch  uint64
}

type HostState struct {
	Report
	LastSeen time.Time
	// Sessions counts agent (re)connects since server start. A steadily rising
	// value with a live LastSeen means the agent is churning/flapping its QUIC
	// session even though it looks online; a flat value means a stable link.
	Sessions int
	// AgentVersion is the agent binary's stamped version, set from each Hello
	// (like Sessions, owned by the connect path, preserved across reports).
	// Empty until a version-reporting agent connects.
	AgentVersion string
	// HostNetworks are the named guest networks this host's agent was
	// configured with (--host-network), as advertised in each Hello. Owned by
	// the connect path exactly like AgentVersion — no report carries them, so
	// UpdateReport preserves them — and empty for a host that has not spoken
	// since server start, for a Mac agent, and for any agent that predates the
	// field. Create-time admission reads this set.
	HostNetworks []string
	// The following are derived on each Get from LastSeen and the clock; they
	// are not stored.
	Online bool
	// Stale trips before Online clears (last report older than StaleWindow): the
	// at-a-glance "sync is degrading" signal.
	Stale bool
	// SinceLastSeen is the age of the last report at read time. Zero when the
	// host has never reported (LastSeen unset).
	SinceLastSeen time.Duration
}

type Registry struct {
	mu  sync.RWMutex
	m   map[string]HostState
	now func() time.Time
}

func New(now func() time.Time) *Registry {
	return &Registry{m: map[string]HostState{}, now: now}
}

func (r *Registry) UpdateReport(hostID string, rep Report) {
	r.mu.Lock()
	defer r.mu.Unlock()
	// Preserve the session counter across reports: UpdateReport replaces the
	// whole HostState, and Sessions is owned by RecordConnect, not the report.
	sessions := r.m[hostID].Sessions
	agentVersion := r.m[hostID].AgentVersion
	hostNetworks := r.m[hostID].HostNetworks
	r.m[hostID] = HostState{Report: rep, LastSeen: r.now(), Sessions: sessions,
		AgentVersion: agentVersion, HostNetworks: hostNetworks}
}

// RecordConnect increments the host's session counter, marking one agent
// (re)connect. It preserves any existing report/LastSeen so a reconnect that
// arrives before the first fresh report does not blank live state.
func (r *Registry) RecordConnect(hostID string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	st := r.m[hostID]
	st.Sessions++
	r.m[hostID] = st
}

// SetAgentVersion records the host's agent binary version from its Hello.
// Preserves existing report state, like RecordConnect.
func (r *Registry) SetAgentVersion(hostID, v string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	st := r.m[hostID]
	st.AgentVersion = v
	r.m[hostID] = st
}

// SetHostNetworks records the named guest networks a host advertised in its
// Hello. Connect-path-owned like AgentVersion: reports never carry it, so
// UpdateReport preserves it. Each Hello replaces the set — an agent restarted
// without its --host-network flags advertises none, and the fleet must see
// that rather than the networks it used to serve.
//
// Stores networks by reference, same convention as the Report slices: the
// caller must not retain or mutate the slice after passing it in. Get clones
// on the way out, so a reader can never observe or corrupt this copy.
func (r *Registry) SetHostNetworks(hostID string, networks []string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	st := r.m[hostID]
	st.HostNetworks = networks
	r.m[hostID] = st
}

func (r *Registry) Get(hostID string) (HostState, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	st, ok := r.m[hostID]
	if !ok {
		return st, false
	}
	// Derive liveness from the last report's age. A host that has connected but
	// never reported (LastSeen unset) is neither online nor meaningfully "stale
	// for N seconds", so leave SinceLastSeen zero and report it offline.
	if !st.LastSeen.IsZero() {
		elapsed := r.now().Sub(st.LastSeen)
		st.SinceLastSeen = elapsed
		st.Online = elapsed < OnlineWindow
		st.Stale = elapsed >= StaleWindow
	}
	// Deep-copy slices so callers cannot corrupt registry state.
	st.Report.VMs = slices.Clone(st.Report.VMs)
	st.Report.Exposures = slices.Clone(st.Report.Exposures)
	st.Report.Volumes = slices.Clone(st.Report.Volumes)
	quarantined := slices.Clone(st.Report.Quarantined)
	for i := range quarantined {
		quarantined[i].VMSpecJSON = bytes.Clone(quarantined[i].VMSpecJSON)
	}
	st.Report.Quarantined = quarantined
	st.HostNetworks = slices.Clone(st.HostNetworks)
	return st, true
}