Files
wehub-resource-sync 26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

138 lines
4.0 KiB
Go

package telemetry
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"time"
)
// maxReadSurfaceEntries bounds the persisted gate state so an unbounded set of
// surface keys cannot grow the state file forever. Oldest entries are pruned.
const maxReadSurfaceEntries = 64
// ReadSurfaceGate bounds telemetry volume for high-frequency read-only
// commands (axi-status, axi-home, status, runs). It emits when the observed
// state fingerprint changed since the last emit, and otherwise at most once
// per heartbeat interval. State persists in a small JSON file so agent
// polling loops - a fresh CLI process per poll - stay bounded across process
// boundaries. Any state-file failure fails open (emit), degrading to the
// pre-gate behavior instead of dropping meaningful events.
type ReadSurfaceGate struct {
path string
interval time.Duration
now func() time.Time
}
// NewReadSurfaceGate creates a gate persisting state at path. now is
// injectable for deterministic tests; nil means time.Now.
func NewReadSurfaceGate(path string, interval time.Duration, now func() time.Time) *ReadSurfaceGate {
if now == nil {
now = time.Now
}
return &ReadSurfaceGate{path: path, interval: interval, now: now}
}
type readSurfaceEntry struct {
Fingerprint string `json:"fingerprint"`
LastEmitUnix int64 `json:"last_emit_unix"`
}
type readSurfaceState struct {
Surfaces map[string]readSurfaceEntry `json:"surfaces"`
}
// ShouldEmit reports whether the surface's event should be emitted now, and
// records the emit when it returns true. fingerprint is a low-cardinality
// summary of the observed state; any change emits immediately, an unchanged
// fingerprint emits at most once per interval.
func (g *ReadSurfaceGate) ShouldEmit(surface, fingerprint string) bool {
if g == nil || g.path == "" {
return true
}
lock, err := acquireReadSurfaceLock(g.path + ".lock")
if err != nil {
return true
}
defer lock.Close()
now := g.now()
state := g.load()
entry, seen := state.Surfaces[surface]
if seen && entry.Fingerprint == fingerprint && now.Sub(time.Unix(entry.LastEmitUnix, 0)) < g.interval {
return false
}
state.Surfaces[surface] = readSurfaceEntry{Fingerprint: fingerprint, LastEmitUnix: now.Unix()}
pruneReadSurfaceState(&state)
g.save(state)
return true
}
func (g *ReadSurfaceGate) load() readSurfaceState {
data, err := os.ReadFile(g.path)
if err == nil {
if state, parseErr := parseReadSurfaceState(data); parseErr == nil {
return state
}
}
return readSurfaceState{Surfaces: map[string]readSurfaceEntry{}}
}
func parseReadSurfaceState(data []byte) (readSurfaceState, error) {
var state readSurfaceState
if err := json.Unmarshal(data, &state); err != nil {
return readSurfaceState{}, err
}
if state.Surfaces == nil {
state.Surfaces = map[string]readSurfaceEntry{}
}
return state, nil
}
func pruneReadSurfaceState(state *readSurfaceState) {
if len(state.Surfaces) <= maxReadSurfaceEntries {
return
}
type keyed struct {
key string
entry readSurfaceEntry
}
entries := make([]keyed, 0, len(state.Surfaces))
for key, entry := range state.Surfaces {
entries = append(entries, keyed{key: key, entry: entry})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].entry.LastEmitUnix > entries[j].entry.LastEmitUnix
})
pruned := make(map[string]readSurfaceEntry, maxReadSurfaceEntries)
for _, e := range entries[:maxReadSurfaceEntries] {
pruned[e.key] = e.entry
}
state.Surfaces = pruned
}
// save writes atomically via rename so a concurrent reader never sees a
// partial file. Failures are ignored: the worst case is an extra emit.
func (g *ReadSurfaceGate) save(state readSurfaceState) {
data, err := json.Marshal(state)
if err != nil {
return
}
tmp, err := os.CreateTemp(filepath.Dir(g.path), ".telemetry-gate-*")
if err != nil {
return
}
tmpPath := tmp.Name()
_, writeErr := tmp.Write(data)
closeErr := tmp.Close()
if writeErr != nil || closeErr != nil {
_ = os.Remove(tmpPath)
return
}
if err := os.Rename(tmpPath, g.path); err != nil {
_ = os.Remove(tmpPath)
}
}