node_heartbeat_job.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. package job
  2. import (
  3. "context"
  4. "strconv"
  5. "sync"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
  9. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  10. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  11. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  12. "github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
  13. )
  14. const (
  15. nodeHeartbeatConcurrency = 32
  16. nodeHeartbeatRequestTimeout = 4 * time.Second
  17. )
  18. type NodeHeartbeatJob struct {
  19. nodeService service.NodeService
  20. running sync.Mutex
  21. }
  22. func NewNodeHeartbeatJob() *NodeHeartbeatJob {
  23. return &NodeHeartbeatJob{}
  24. }
  25. func (j *NodeHeartbeatJob) Run() {
  26. if !j.running.TryLock() {
  27. return
  28. }
  29. defer j.running.Unlock()
  30. nodes, err := j.nodeService.GetAll()
  31. if err != nil {
  32. logger.Warning("node heartbeat: load nodes failed:", err)
  33. return
  34. }
  35. j.nodeService.RetainEnabledNodeDescendants(nodes)
  36. if len(nodes) == 0 {
  37. return
  38. }
  39. sem := make(chan struct{}, nodeHeartbeatConcurrency)
  40. var wg sync.WaitGroup
  41. for _, n := range nodes {
  42. if !n.Enable {
  43. continue
  44. }
  45. wg.Add(1)
  46. sem <- struct{}{}
  47. n := n
  48. common.GoRecover("node-heartbeat:"+n.Name, func() {
  49. defer wg.Done()
  50. defer func() { <-sem }()
  51. j.probeOne(n)
  52. })
  53. }
  54. wg.Wait()
  55. if !websocket.HasClients() {
  56. return
  57. }
  58. updated, err := j.nodeService.GetNodeTreeView()
  59. if err != nil {
  60. logger.Warning("node heartbeat: load nodes for broadcast failed:", err)
  61. return
  62. }
  63. websocket.BroadcastNodes(updated)
  64. }
  65. func (j *NodeHeartbeatJob) probeOne(n *model.Node) {
  66. ctx, cancel := context.WithTimeout(context.Background(), nodeHeartbeatRequestTimeout)
  67. defer cancel()
  68. prevStatus := n.Status
  69. patch, err := j.nodeService.Probe(ctx, n)
  70. if err != nil {
  71. patch.Status = "offline"
  72. } else {
  73. patch.Status = "online"
  74. }
  75. if updErr := j.nodeService.UpdateHeartbeat(n.Id, patch); updErr != nil {
  76. logger.Warning("node heartbeat: update node", n.Id, "failed:", updErr)
  77. }
  78. publishNodeTransition(n, prevStatus, patch)
  79. // Learn the nodes this node manages so the panel can surface them as
  80. // transitive sub-nodes (#4983). Fresh context — the probe budget above may
  81. // be spent. Drop them when the node is unreachable.
  82. if patch.Status == "online" {
  83. dctx, dcancel := context.WithTimeout(context.Background(), nodeHeartbeatRequestTimeout)
  84. j.nodeService.RefreshDescendants(dctx, n)
  85. dcancel()
  86. } else {
  87. j.nodeService.ClearDescendants(n.Id)
  88. }
  89. }
  90. // publishNodeTransition emits node.down / node.up only on a genuine state change.
  91. // An "unknown"/empty previous status (fresh start) is treated as not-online, so a
  92. // node coming up for the first time fires node.up but never a spurious node.down.
  93. func publishNodeTransition(n *model.Node, prevStatus string, patch service.HeartbeatPatch) {
  94. if EventBus == nil {
  95. return
  96. }
  97. var eventType eventbus.EventType
  98. switch {
  99. case prevStatus == "online" && patch.Status == "offline":
  100. eventType = eventbus.EventNodeDown
  101. case prevStatus != "online" && patch.Status == "online":
  102. eventType = eventbus.EventNodeUp
  103. default:
  104. return
  105. }
  106. source := n.Name
  107. if source == "" {
  108. source = "node-" + strconv.Itoa(n.Id)
  109. }
  110. EventBus.Publish(eventbus.Event{
  111. Type: eventType,
  112. Source: source,
  113. Data: &eventbus.NodeHealthData{
  114. NodeId: n.Id,
  115. LatencyMs: patch.LatencyMs,
  116. CpuPct: patch.CpuPct,
  117. MemPct: patch.MemPct,
  118. XrayState: patch.XrayState,
  119. XrayError: patch.XrayError,
  120. },
  121. })
  122. }