Author SHA1 Message Date
Muse db6f666ff7 probe: fast report stamps last-seen; snapshot TTL 900ms (V2.0.3)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
2026-10-23 18:38:56 +08:00
3 changed files with 32 additions and 14 deletions
+15 -6
View File
@@ -626,11 +626,16 @@ func probeStatIface(agentID int64) string {
} }
// probeReport ingests one fast report (V2.0.1): a probe metrics // probeReport ingests one fast report (V2.0.1): a probe metrics
// block and nothing else. It deliberately does NOT touch the // block and nothing else. Since V2.0.3 it also touches the agent's
// agent's last-seen stamp — liveness stays the 60 s heartbeat's // last-seen stamp, like every other report channel: the hub's
// signal, and the traffic ledger's unseen-boot rule keeps measuring // themes gate each batch on the node's last_seen being newer than
// reboots against heartbeat sightings exactly as before. Agents on // the last one they applied, so a metrics-only channel that never
// older masters never see this endpoint (404) and simply keep // advanced the stamp froze the public cards between the 10 s
// traffic reports. Liveness now means "any report from the agent"
// — all channels come from the same process — and the traffic
// ledger's unseen-boot rule is unaffected: Record still receives
// the pre-touch stamp, so gaps measure exactly as before. Agents
// on older masters never see this endpoint (404) and simply keep
// reporting metrics on the heartbeat; masters accept metrics on // reporting metrics on the heartbeat; masters accept metrics on
// either channel. // either channel.
func (a *AgentController) probeReport(c *gin.Context) { func (a *AgentController) probeReport(c *gin.Context) {
@@ -644,9 +649,13 @@ func (a *AgentController) probeReport(c *gin.Context) {
ProbeMetrics *heartbeatProbeMetrics `json:"probeMetrics"` ProbeMetrics *heartbeatProbeMetrics `json:"probeMetrics"`
} }
_ = c.ShouldBindJSON(&req) _ = c.ShouldBindJSON(&req)
// The last-seen stamp as it stood before this report (see the
// heartbeat handler): captured before touchAgent overwrites it.
prevLastSeen := agent.LastSeen
a.touchAgent(uint(agent.Id), "")
if req.ProbeMetrics != nil { if req.ProbeMetrics != nil {
a.updateAgentProbe(uint(agent.Id), nil, req.ProbeMetrics) a.updateAgentProbe(uint(agent.Id), nil, req.ProbeMetrics)
probe.Record(int64(agent.Id), probeSample(req.ProbeMetrics), agent.LastSeen) probe.Record(int64(agent.Id), probeSample(req.ProbeMetrics), prevLastSeen)
} }
jsonObj(c, gin.H{"message": "ok"}, nil) jsonObj(c, gin.H{"message": "ok"}, nil)
} }
@@ -18,9 +18,10 @@ import (
) )
// V2.0.1 probe batch, master side: the fast-report endpoint ingests // V2.0.1 probe batch, master side: the fast-report endpoint ingests
// metrics without touching liveness (last-seen stays the // metrics, and the config poll carries the cadence + the node's
// heartbeat's), and the config poll carries the cadence + the // counted-interface pin. Since V2.0.3 the fast report also stamps
// node's counted-interface pin. // last-seen (the hub's themes gate batches on its freshness); it
// originally did not — see the probeReport handler comment.
func TestAgentFastReportAndConfigKeys(t *testing.T) { func TestAgentFastReportAndConfigKeys(t *testing.T) {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
dbDir := t.TempDir() dbDir := t.TempDir()
@@ -71,6 +72,12 @@ func TestAgentFastReportAndConfigKeys(t *testing.T) {
metrics := `{"probeMetrics":{"bootId":"boot-f/0123456789abcdef","uptime":60,` + metrics := `{"probeMetrics":{"bootId":"boot-f/0123456789abcdef","uptime":60,` +
`"memTotal":1000,"memUsed":400,"netRxTotal":10,"netTxTotal":20}}` `"memTotal":1000,"memUsed":400,"netRxTotal":10,"netTxTotal":20}}`
// Age the stamp first so the fast report's touch is observable
// even within the same millisecond as the heartbeat's.
stale := seenAfterHeartbeat - 120_000
if err := db.Model(&model.AgentNode{}).Where("id = ?", agent.Id).Update("last_seen", stale).Error; err != nil {
t.Fatalf("age last-seen: %v", err)
}
if code, body := call(http.MethodPost, "/panel/api/agent/probe-report", metrics); code != http.StatusOK { if code, body := call(http.MethodPost, "/panel/api/agent/probe-report", metrics); code != http.StatusOK {
t.Fatalf("probe-report: %d (%s)", code, body) t.Fatalf("probe-report: %d (%s)", code, body)
} }
@@ -78,8 +85,8 @@ func TestAgentFastReportAndConfigKeys(t *testing.T) {
if row.ProbeMemUsed != 400 || row.ProbeAt == 0 { if row.ProbeMemUsed != 400 || row.ProbeAt == 0 {
t.Errorf("fast report metrics not stored: %+v", row) t.Errorf("fast report metrics not stored: %+v", row)
} }
if row.LastSeen != seenAfterHeartbeat { if row.LastSeen <= stale {
t.Errorf("fast report moved last-seen: %d -> %d (liveness is the heartbeat's signal)", seenAfterHeartbeat, row.LastSeen) t.Errorf("fast report did not stamp last-seen: %d -> %d (V2.0.3: themes gate on it)", stale, row.LastSeen)
} }
// Unauthorized callers are refused. // Unauthorized callers are refused.
+5 -3
View File
@@ -29,9 +29,11 @@ import (
// 150 s absorbs one lost beat plus jitter. // 150 s absorbs one lost beat plus jitter.
const ProbeOnlineThresholdMs = 150_000 const ProbeOnlineThresholdMs = 150_000
// ProbeSnapshotTTL mirrors the hub's 1900 ms frame reuse: just under // ProbeSnapshotTTL mirrors the hub's frame reuse, shortened in
// the 2 s push interval, so a frame is built at most once per tick. // V2.0.3 from the hub's 1900 ms: just under the fastest (1 s) push
const ProbeSnapshotTTL = 1900 * time.Millisecond // cadence, so a 1 s stream still gets a fresh frame every tick
// instead of repeating the previous one half the time.
const ProbeSnapshotTTL = 900 * time.Millisecond
// ---- public-surface settings (main settings KV, hub key names) ---- // ---- public-surface settings (main settings KV, hub key names) ----