B4: probe task store, panel API, heartbeat ingestion, agent prober
- probe package: probe_ping_task/probe_ping_node tables; task CRUD with hub save semantics (id high-water, base-diff assignment, 64-per-agent crowding refusal, auto_join cap); filing with assignment-gated insert; PingHistory weighted-median chart read over minute+hour tiers; auto-join on first Record (ensureRows). - controller: /panel/api/agent/probe-tasks* management endpoints (node id space, hub validation chain verbatim); series=ping now serves real rows from probe.PingHistory; heartbeat ingests probeResults (missing latency skipped, batch capped); config response carries probe_tasks (always present, empty = clear). - agent: per-task probe loops (immediate first probe, ticker, 900ms resolve/dial budgets, <=3 addresses, resolve overrun = no sample), result buffer drained on heartbeat and requeued on failure; task set applied before the config-hash early return.
This commit is contained in:
@@ -511,6 +511,23 @@ func (a *AgentController) confirmAgentUpdate(id uint, reportedVersion string) {
|
||||
}).Error
|
||||
}
|
||||
|
||||
// heartbeatProbeResult is one latency sample an agent piggybacks on
|
||||
// its heartbeat (item 7 B4). LatencyMs is a pointer: an absent field
|
||||
// means "no sample" (a resolution overrun produces none) and the row
|
||||
// is skipped, while a present negative value is a genuine recorded
|
||||
// miss — the two must never collapse into each other.
|
||||
type heartbeatProbeResult struct {
|
||||
TaskID int64 `json:"taskId"`
|
||||
LatencyMs *int64 `json:"latencyMs"`
|
||||
}
|
||||
|
||||
// maxProbeResultsPerHeartbeat caps one heartbeat's probe batch. The
|
||||
// agent's buffer holds far less than this in practice (64 tasks × 60 s
|
||||
// at the fastest legal interval is under a thousand samples); the cap
|
||||
// exists so a buggy or hostile reporter cannot make one request file
|
||||
// an unbounded batch.
|
||||
const maxProbeResultsPerHeartbeat = 2048
|
||||
|
||||
func (a *AgentController) heartbeat(c *gin.Context) {
|
||||
agent, ok := a.findAgent(bearerToken(c))
|
||||
if !ok {
|
||||
@@ -525,6 +542,7 @@ func (a *AgentController) heartbeat(c *gin.Context) {
|
||||
UpdateStatus *heartbeatUpdateStatus `json:"updateStatus"`
|
||||
ProbeFacts *heartbeatProbeFacts `json:"probeFacts"`
|
||||
ProbeMetrics *heartbeatProbeMetrics `json:"probeMetrics"`
|
||||
ProbeResults []heartbeatProbeResult `json:"probeResults"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
// The last-seen stamp as it stood before this heartbeat: the probe
|
||||
@@ -542,6 +560,23 @@ func (a *AgentController) heartbeat(c *gin.Context) {
|
||||
// probe package logs and swallows its own failures.
|
||||
probe.Record(int64(agent.Id), probeSample(req.ProbeMetrics), prevLastSeen)
|
||||
}
|
||||
if len(req.ProbeResults) > 0 {
|
||||
// Item 7 B4: file the latency batch, stamped at arrival (hub
|
||||
// semantics). Results ride the heartbeat rather than the
|
||||
// traffic report on purpose: probing must keep working while
|
||||
// xray is down, and the traffic report early-returns then.
|
||||
results := make([]probe.PingResult, 0, len(req.ProbeResults))
|
||||
for _, r := range req.ProbeResults {
|
||||
if r.TaskID <= 0 || r.LatencyMs == nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, probe.PingResult{TaskID: r.TaskID, LatencyMs: *r.LatencyMs})
|
||||
if len(results) >= maxProbeResultsPerHeartbeat {
|
||||
break
|
||||
}
|
||||
}
|
||||
probe.InsertPings(int64(agent.Id), results)
|
||||
}
|
||||
if req.Telemetry != nil {
|
||||
a.updateAgentTelemetry(uint(agent.Id), req.Telemetry)
|
||||
if req.Telemetry.AgentVersion != nil && *req.Telemetry.AgentVersion != "" {
|
||||
@@ -581,6 +616,12 @@ func (a *AgentController) getConfig(c *gin.Context) {
|
||||
payload := gin.H{
|
||||
"config_hash": hex.EncodeToString(sum[:]),
|
||||
"xray_config": json.RawMessage(raw),
|
||||
// Item 7 B4: the agent's probe task set rides the existing
|
||||
// config poll (no new channel). Always present from this
|
||||
// master version — an empty list means "run no probes", while
|
||||
// an absent field (older master) leaves the agent's current
|
||||
// set untouched.
|
||||
"probe_tasks": probe.PingTasksFor(int64(agent.Id)),
|
||||
}
|
||||
// Item 20: a pending one-click update intent rides along on the config
|
||||
// poll (no new channel — the agent already polls this endpoint).
|
||||
|
||||
Reference in New Issue
Block a user