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
}