feat(inbounds): improve multi-node online attribution (#6164)

This commit is contained in:
isultanov99
2026-08-15 17:40:35 +02:00
committed by GitHub
parent 2d669fa4b7
commit be70535b94
5 changed files with 399 additions and 21 deletions
+39
View File
@@ -118,16 +118,55 @@ func TestGetLocalActiveInboundsTracksGraceWindow(t *testing.T) {
}
}
func TestMergedActiveInboundTreesScopesPerGuid(t *testing.T) {
p := newOnlineTestProcess()
p.SetNodeActiveInboundTree(1, map[string][]string{
"guid-a": {"in-a", "in-a"},
"guid-b": {"in-b"},
})
p.SetNodeActiveInboundTree(2, map[string][]string{
"guid-a": {"in-c"},
"guid-c": {},
})
merged := p.GetMergedActiveInboundTrees()
assertSameSet(t, "guid-a", merged["guid-a"], []string{"in-a", "in-c"})
assertSameSet(t, "guid-b", merged["guid-b"], []string{"in-b"})
if _, ok := merged["guid-c"]; ok {
t.Errorf("empty active-inbound GUID set should be omitted: %v", merged)
}
}
// TestClearNodeOnlineClientsDropsNode mirrors a failed node probe: the node's
// whole subtree contribution disappears immediately.
func TestClearNodeOnlineClientsDropsNode(t *testing.T) {
p := newOnlineTestProcess()
p.SetNodeOnlineTree(3, map[string][]string{"guid-a": {"user1"}})
p.SetNodeActiveInboundTree(3, map[string][]string{"guid-a": {"in-a"}})
p.ClearNodeOnlineClients(3)
if _, ok := p.GetMergedNodeTrees()["guid-a"]; ok {
t.Errorf("node 3's subtree should be absent after ClearNodeOnlineClients")
}
if _, ok := p.GetMergedActiveInboundTrees()["guid-a"]; ok {
t.Errorf("node 3's active-inbound subtree should be absent after ClearNodeOnlineClients")
}
}
func TestSetNodeTreesEmptyInputDropsNode(t *testing.T) {
p := newOnlineTestProcess()
p.SetNodeOnlineTree(3, map[string][]string{"guid-a": {"user1"}})
p.SetNodeActiveInboundTree(3, map[string][]string{"guid-a": {"in-a"}})
p.SetNodeOnlineTree(3, nil)
p.SetNodeActiveInboundTree(3, nil)
if _, ok := p.GetMergedNodeTrees()["guid-a"]; ok {
t.Errorf("empty online tree should remove node 3's subtree")
}
if _, ok := p.GetMergedActiveInboundTrees()["guid-a"]; ok {
t.Errorf("empty active-inbound tree should remove node 3's subtree")
}
}
// TestOnlineAPISupportTriState pins the lazy capability probe contract: a new
+66 -5
View File
@@ -176,7 +176,12 @@ type process struct {
// mutex guards this map, onlineClients, and localLastOnline above so the
// online getters never see a torn read.
nodeOnlineTrees map[int]map[string][]string
onlineMu sync.RWMutex
// nodeActiveInboundTrees mirrors nodeOnlineTrees for active inbound tags:
// each direct node reports a GUID-keyed subtree of inbound tags that carried
// traffic within its own grace window. The inbounds page combines this with
// nodeOnlineTrees so a multi-inbound client is not shown on an idle inbound.
nodeActiveInboundTrees map[int]map[string][]string
onlineMu sync.RWMutex
// onlineAPISupport caches whether the running core implements the
// online-stats RPCs (GetUsersStats). A new process is created on every
@@ -398,10 +403,9 @@ func (p *Process) GetMergedNodeTrees() map[string][]string {
}
// GetLocalActiveInbounds returns a copy of THIS panel's inbound tags that
// carried traffic within the grace window. Only the local xray reports
// per-inbound activity; remote-node snapshots don't carry it, so the service
// layer keys these under the panel's own GUID and a node missing from the
// active-inbounds map means "don't gate" (fall back to the email-only signal).
// carried traffic within the grace window. The service layer keys these under
// the panel's own GUID before merging them with remote-node active-inbound
// subtrees.
func (p *Process) GetLocalActiveInbounds() []string {
p.onlineMu.RLock()
defer p.onlineMu.RUnlock()
@@ -413,6 +417,43 @@ func (p *Process) GetLocalActiveInbounds() []string {
return out
}
// GetMergedActiveInboundTrees returns the union of every direct node's reported
// active-inbound subtree, keyed by the panelGuid of the node that physically
// hosts each inbound. Duplicate tags reported through multiple paths are
// deduped per GUID.
func (p *Process) GetMergedActiveInboundTrees() map[string][]string {
p.onlineMu.RLock()
defer p.onlineMu.RUnlock()
if len(p.nodeActiveInboundTrees) == 0 {
return map[string][]string{}
}
out := make(map[string][]string)
seen := make(map[string]map[string]struct{})
for _, tree := range p.nodeActiveInboundTrees {
for guid, tags := range tree {
if guid == "" || len(tags) == 0 {
continue
}
dedup := seen[guid]
if dedup == nil {
dedup = make(map[string]struct{}, len(tags))
seen[guid] = dedup
}
for _, tag := range tags {
if tag == "" {
continue
}
if _, ok := dedup[tag]; ok {
continue
}
dedup[tag] = struct{}{}
out[guid] = append(out[guid], tag)
}
}
}
return out
}
// RefreshLocalOnline records that each email in activeEmails and each tag in
// activeInboundTags had local xray traffic at now, then rebuilds onlineClients
// and localActiveInbounds from every entry seen within graceMs, pruning older
@@ -462,12 +503,31 @@ func (p *Process) RefreshLocalOnline(activeEmails, activeInboundTags []string, n
func (p *Process) SetNodeOnlineTree(nodeID int, tree map[string][]string) {
p.onlineMu.Lock()
defer p.onlineMu.Unlock()
if len(tree) == 0 {
delete(p.nodeOnlineTrees, nodeID)
return
}
if p.nodeOnlineTrees == nil {
p.nodeOnlineTrees = map[int]map[string][]string{}
}
p.nodeOnlineTrees[nodeID] = tree
}
// SetNodeActiveInboundTree records the GUID-keyed active-inbound subtree one
// direct remote node reported. Replaces any previous entry for that node.
func (p *Process) SetNodeActiveInboundTree(nodeID int, tree map[string][]string) {
p.onlineMu.Lock()
defer p.onlineMu.Unlock()
if len(tree) == 0 {
delete(p.nodeActiveInboundTrees, nodeID)
return
}
if p.nodeActiveInboundTrees == nil {
p.nodeActiveInboundTrees = map[int]map[string][]string{}
}
p.nodeActiveInboundTrees[nodeID] = tree
}
// ClearNodeOnlineClients drops a direct node's whole subtree contribution.
// Called when a probe fails so a downed node — and everything behind it — doesn't
// keep its clients listed as "online" until the next successful probe.
@@ -475,6 +535,7 @@ func (p *Process) ClearNodeOnlineClients(nodeID int) {
p.onlineMu.Lock()
defer p.onlineMu.Unlock()
delete(p.nodeOnlineTrees, nodeID)
delete(p.nodeActiveInboundTrees, nodeID)
}
// GetUptime returns the uptime of the Xray process in seconds.