Files
3x-ui-agent/internal/web/service/probe/probe.go
T
Muse e03ceaa41f 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.
2026-10-22 21:57:23 +00:00

239 lines
6.4 KiB
Go

package probe
import (
"strconv"
"strings"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
)
// RetentionDays reads the history retention window (days) from the
// panel settings (key retention_days, as in hub). Default 7 — the
// window the user settled on — clamped to [1, 365] (hub's cap).
func RetentionDays() int64 {
var row model.Setting
err := database.GetDB().Where("key = ?", "retention_days").First(&row).Error
if err != nil {
return DetailDays
}
n, perr := strconv.ParseInt(strings.TrimSpace(row.Value), 10, 64)
if perr != nil {
return DetailDays
}
if n < 1 {
return 1
}
if n > 365 {
return 365
}
return n
}
// Record files one heartbeat's probe sample into the default store.
// prevLastSeenMs is the agent row's last-seen stamp from before this
// heartbeat (milliseconds, as stored on agent_nodes). Best-effort: any
// failure is logged and swallowed so a heartbeat never fails over
// history.
func Record(agentID int64, smp Sample, prevLastSeenMs int64) {
st := Default()
if st == nil {
return
}
if err := st.Record(agentID, smp, time.Now(), prevLastSeenMs/1000); err != nil {
logger.Warningf("probe: record agent %d: %v", agentID, err)
}
}
// Maintain runs one housekeeping pass against the default store:
// roll_up first, then prune — prune runs even when roll_up fails, as
// it keeps minute rows until their hour is folded and skipping it
// would leave the database growing (hub housekeeping discipline).
func Maintain() {
st := Default()
if st == nil {
return
}
keep := RetentionDays()
if folded, err := st.RollUp(time.Now().Unix(), keep); err != nil {
logger.Warningf("probe: roll_up failed: %v", err)
} else if folded > 1 {
logger.Infof("probe: folded %d hours of history into the hourly tier", folded)
}
if _, err := st.Prune(keep); err != nil {
logger.Warningf("probe: prune failed: %v", err)
}
}
// BookHeld books every node's held, not-yet-booked reading on the
// default store; called as the master shuts down.
func BookHeld() {
if st := Default(); st != nil {
st.BookHeld()
}
}
// TrafficSnapshot is the B3 read port: every agent's gated ledger.
func TrafficSnapshot() (map[int64]Traffic, error) {
st := Default()
if st == nil {
return map[int64]Traffic{}, nil
}
return st.AllTraffic()
}
// MetaSnapshot is the B3 read port for display metadata: every agent's
// probe_meta row, keyed by agent id. Agents without a row are absent;
// callers substitute DefaultMeta.
func MetaSnapshot() (map[int64]Meta, error) {
st := Default()
if st == nil {
return map[int64]Meta{}, nil
}
return st.AllMeta()
}
// MetaFor reads one agent's metadata row from the default store.
// ok is false when the agent never reported (no row ensured yet).
func MetaFor(agentID int64) (Meta, bool, error) {
st := Default()
if st == nil {
return Meta{}, false, nil
}
return st.Meta(agentID)
}
// SaveMeta persists one agent's metadata row (B3 edit surface).
func SaveMeta(m Meta) error {
st := Default()
if st == nil {
return nil
}
return st.UpsertMeta(m)
}
// MetricHistory is the B3 read port for one agent's history window.
// With no store the window is simply empty.
func MetricHistory(agentID int64, sp Span) ([]MetricRow, error) {
st := Default()
if st == nil {
return []MetricRow{}, nil
}
return st.MetricHistory(agentID, sp)
}
// DeleteAgentData drops a removed agent's probe rows (B3 cleanup point
// called from agent deletion). Best-effort: failures are logged and
// swallowed, as a stale probe row must never block a panel operation.
func DeleteAgentData(agentID int64) {
st := Default()
if st == nil {
return
}
if err := st.DeleteAgentData(agentID); err != nil {
logger.Warningf("probe: delete agent %d data: %v", agentID, err)
}
}
// ---- B4: probe tasks, results and the latency read ----
// PingTasks lists every probe task with its assignments. With no
// store the list is empty.
func PingTasks() ([]PingTask, error) {
st := Default()
if st == nil {
return []PingTask{}, nil
}
return st.PingTasks()
}
// SavePingTask creates or replaces one probe task (see Store).
func SavePingTask(t PingTask, base *[]int64) (int64, error) {
st := Default()
if st == nil {
return 0, nil
}
return st.SavePingTask(t, base)
}
// DeletePingTask removes one probe task and its filed history.
func DeletePingTask(id int64) error {
st := Default()
if st == nil {
return nil
}
return st.DeletePingTask(id)
}
// SetNodePingTasks applies one agent's assignment difference.
func SetNodePingTasks(agentID int64, tasks, base []int64) error {
st := Default()
if st == nil {
return nil
}
return st.SetNodePingTasks(agentID, tasks, base)
}
// ReorderPingTasks rewrites the panel order of the task list.
func ReorderPingTasks(ids []int64) error {
st := Default()
if st == nil {
return nil
}
return st.ReorderPingTasks(ids)
}
// PingTasksFor reads the task list pushed to one agent on its config
// poll. Best-effort like the rest of the ingest side: a store failure
// reads as "no tasks" and is logged, so a broken probe.db never
// breaks config delivery — the agent keeps its current list until a
// poll succeeds.
func PingTasksFor(agentID int64) []PingTaskRef {
st := Default()
if st == nil {
return []PingTaskRef{}
}
refs, err := st.PingTasksFor(agentID)
if err != nil {
logger.Warningf("probe: ping tasks for agent %d: %v", agentID, err)
return []PingTaskRef{}
}
return refs
}
// PingTaskNames reads one agent's probe chart labels.
func PingTaskNames(agentID int64) (map[string]string, error) {
st := Default()
if st == nil {
return map[string]string{}, nil
}
return st.PingTaskNames(agentID)
}
// InsertPings files one agent's probe results at the arrival stamp.
// Best-effort (heartbeat path): failures are logged and swallowed.
func InsertPings(agentID int64, results []PingResult) {
if len(results) == 0 {
return
}
st := Default()
if st == nil {
return
}
if err := st.InsertPings(agentID, results, time.Now().Unix()); err != nil {
logger.Warningf("probe: insert pings agent %d: %v", agentID, err)
}
}
// PingHistory is the B4 read port for one agent's latency window.
// With no store the window is simply empty.
func PingHistory(agentID int64, sp Span) ([]PingRow, map[string]float64, error) {
st := Default()
if st == nil {
return []PingRow{}, map[string]float64{}, nil
}
return st.PingHistory(agentID, sp)
}