mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-16 16:20:59 +00:00
feat(inbounds): improve multi-node online attribution (#6164)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user