|
@@ -2,7 +2,10 @@ package job
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "sort"
|
|
|
"strconv"
|
|
"strconv"
|
|
|
|
|
+ "strings"
|
|
|
"sync"
|
|
"sync"
|
|
|
"time"
|
|
"time"
|
|
|
|
|
|
|
@@ -17,6 +20,10 @@ import (
|
|
|
const (
|
|
const (
|
|
|
nodeHeartbeatConcurrency = 32
|
|
nodeHeartbeatConcurrency = 32
|
|
|
nodeHeartbeatRequestTimeout = 4 * time.Second
|
|
nodeHeartbeatRequestTimeout = 4 * time.Second
|
|
|
|
|
+ // Past this many same-direction transitions in one tick, one summary event goes out:
|
|
|
|
|
+ // per-node events overflow the notifier queues and every chat's rate limit.
|
|
|
|
|
+ nodeTransitionBurst = 5
|
|
|
|
|
+ nodeTransitionBurstNames = 10
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
type NodeHeartbeatJob struct {
|
|
type NodeHeartbeatJob struct {
|
|
@@ -46,6 +53,8 @@ func (j *NodeHeartbeatJob) Run() {
|
|
|
|
|
|
|
|
sem := make(chan struct{}, nodeHeartbeatConcurrency)
|
|
sem := make(chan struct{}, nodeHeartbeatConcurrency)
|
|
|
var wg sync.WaitGroup
|
|
var wg sync.WaitGroup
|
|
|
|
|
+ var transitionsMu sync.Mutex
|
|
|
|
|
+ var transitions []eventbus.Event
|
|
|
for _, n := range nodes {
|
|
for _, n := range nodes {
|
|
|
if !n.Enable {
|
|
if !n.Enable {
|
|
|
continue
|
|
continue
|
|
@@ -56,10 +65,15 @@ func (j *NodeHeartbeatJob) Run() {
|
|
|
common.GoRecover("node-heartbeat:"+n.Name, func() {
|
|
common.GoRecover("node-heartbeat:"+n.Name, func() {
|
|
|
defer wg.Done()
|
|
defer wg.Done()
|
|
|
defer func() { <-sem }()
|
|
defer func() { <-sem }()
|
|
|
- j.probeOne(n)
|
|
|
|
|
|
|
+ if event := j.probeOne(n); event != nil {
|
|
|
|
|
+ transitionsMu.Lock()
|
|
|
|
|
+ transitions = append(transitions, *event)
|
|
|
|
|
+ transitionsMu.Unlock()
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
wg.Wait()
|
|
wg.Wait()
|
|
|
|
|
+ publishNodeTransitions(transitions)
|
|
|
|
|
|
|
|
if !websocket.HasClients() {
|
|
if !websocket.HasClients() {
|
|
|
return
|
|
return
|
|
@@ -72,7 +86,7 @@ func (j *NodeHeartbeatJob) Run() {
|
|
|
websocket.BroadcastNodes(updated)
|
|
websocket.BroadcastNodes(updated)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (j *NodeHeartbeatJob) probeOne(n *model.Node) {
|
|
|
|
|
|
|
+func (j *NodeHeartbeatJob) probeOne(n *model.Node) *eventbus.Event {
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), nodeHeartbeatRequestTimeout)
|
|
ctx, cancel := context.WithTimeout(context.Background(), nodeHeartbeatRequestTimeout)
|
|
|
defer cancel()
|
|
defer cancel()
|
|
|
prevStatus := n.Status
|
|
prevStatus := n.Status
|
|
@@ -85,7 +99,6 @@ func (j *NodeHeartbeatJob) probeOne(n *model.Node) {
|
|
|
if updErr := j.nodeService.UpdateHeartbeat(n.Id, patch); updErr != nil {
|
|
if updErr := j.nodeService.UpdateHeartbeat(n.Id, patch); updErr != nil {
|
|
|
logger.Warning("node heartbeat: update node", n.Id, "failed:", updErr)
|
|
logger.Warning("node heartbeat: update node", n.Id, "failed:", updErr)
|
|
|
}
|
|
}
|
|
|
- publishNodeTransition(n, prevStatus, patch)
|
|
|
|
|
// Learn the nodes this node manages so the panel can surface them as
|
|
// Learn the nodes this node manages so the panel can surface them as
|
|
|
// transitive sub-nodes (#4983). Fresh context — the probe budget above may
|
|
// transitive sub-nodes (#4983). Fresh context — the probe budget above may
|
|
|
// be spent. Drop them when the node is unreachable.
|
|
// be spent. Drop them when the node is unreachable.
|
|
@@ -96,15 +109,12 @@ func (j *NodeHeartbeatJob) probeOne(n *model.Node) {
|
|
|
} else {
|
|
} else {
|
|
|
j.nodeService.ClearDescendants(n.Id)
|
|
j.nodeService.ClearDescendants(n.Id)
|
|
|
}
|
|
}
|
|
|
|
|
+ return nodeTransitionEvent(n, prevStatus, patch)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// publishNodeTransition emits node.down / node.up only on a genuine state change.
|
|
|
|
|
-// An "unknown"/empty previous status (fresh start) is treated as not-online, so a
|
|
|
|
|
-// node coming up for the first time fires node.up but never a spurious node.down.
|
|
|
|
|
-func publishNodeTransition(n *model.Node, prevStatus string, patch service.HeartbeatPatch) {
|
|
|
|
|
- if EventBus == nil {
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// nodeTransitionEvent is node.down / node.up on a genuine state change only; an unknown
|
|
|
|
|
+// previous status (fresh start) counts as not-online, so it never yields node.down.
|
|
|
|
|
+func nodeTransitionEvent(n *model.Node, prevStatus string, patch service.HeartbeatPatch) *eventbus.Event {
|
|
|
var eventType eventbus.EventType
|
|
var eventType eventbus.EventType
|
|
|
switch {
|
|
switch {
|
|
|
case prevStatus == "online" && patch.Status == "offline":
|
|
case prevStatus == "online" && patch.Status == "offline":
|
|
@@ -112,13 +122,13 @@ func publishNodeTransition(n *model.Node, prevStatus string, patch service.Heart
|
|
|
case prevStatus != "online" && patch.Status == "online":
|
|
case prevStatus != "online" && patch.Status == "online":
|
|
|
eventType = eventbus.EventNodeUp
|
|
eventType = eventbus.EventNodeUp
|
|
|
default:
|
|
default:
|
|
|
- return
|
|
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
source := n.Name
|
|
source := n.Name
|
|
|
if source == "" {
|
|
if source == "" {
|
|
|
source = "node-" + strconv.Itoa(n.Id)
|
|
source = "node-" + strconv.Itoa(n.Id)
|
|
|
}
|
|
}
|
|
|
- EventBus.Publish(eventbus.Event{
|
|
|
|
|
|
|
+ return &eventbus.Event{
|
|
|
Type: eventType,
|
|
Type: eventType,
|
|
|
Source: source,
|
|
Source: source,
|
|
|
Data: &eventbus.NodeHealthData{
|
|
Data: &eventbus.NodeHealthData{
|
|
@@ -129,5 +139,34 @@ func publishNodeTransition(n *model.Node, prevStatus string, patch service.Heart
|
|
|
XrayState: patch.XrayState,
|
|
XrayState: patch.XrayState,
|
|
|
XrayError: patch.XrayError,
|
|
XrayError: patch.XrayError,
|
|
|
},
|
|
},
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// publishNodeTransitions sends one tick's transitions, folding a same-direction burst
|
|
|
|
|
+// (a master-side blip flips every node at once) into one event naming the nodes.
|
|
|
|
|
+func publishNodeTransitions(events []eventbus.Event) {
|
|
|
|
|
+ if EventBus == nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ namesByType := make(map[eventbus.EventType][]string)
|
|
|
|
|
+ for _, e := range events {
|
|
|
|
|
+ namesByType[e.Type] = append(namesByType[e.Type], e.Source)
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, e := range events {
|
|
|
|
|
+ if len(namesByType[e.Type]) <= nodeTransitionBurst {
|
|
|
|
|
+ EventBus.Publish(e)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, eventType := range []eventbus.EventType{eventbus.EventNodeDown, eventbus.EventNodeUp} {
|
|
|
|
|
+ names := namesByType[eventType]
|
|
|
|
|
+ if len(names) <= nodeTransitionBurst {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ sort.Strings(names)
|
|
|
|
|
+ source := strings.Join(names[:min(len(names), nodeTransitionBurstNames)], ", ")
|
|
|
|
|
+ if extra := len(names) - nodeTransitionBurstNames; extra > 0 {
|
|
|
|
|
+ source += fmt.Sprintf(" (+%d)", extra)
|
|
|
|
|
+ }
|
|
|
|
|
+ EventBus.Publish(eventbus.Event{Type: eventType, Source: source})
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|