|
@@ -20,51 +20,50 @@ func assertSameSet(t *testing.T, label string, got, want []string) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestGetOnlineClientsByNodeScopesPerNode pins the fix for issue #4809: a
|
|
|
|
|
-// client online on one node must not be reported online on any other node.
|
|
|
|
|
-func TestGetOnlineClientsByNodeScopesPerNode(t *testing.T) {
|
|
|
|
|
|
|
+// TestMergedNodeTreesScopesPerGuid pins #4983/#4809: each node's clients stay
|
|
|
|
|
+// under that node's GUID, so a client on one node is never attributed to
|
|
|
|
|
+// another — and a sub-node's clients (reported under their own GUID inside a
|
|
|
|
|
+// parent's tree) compose upward without collapsing onto the parent.
|
|
|
|
|
+func TestMergedNodeTreesScopesPerGuid(t *testing.T) {
|
|
|
p := newOnlineTestProcess()
|
|
p := newOnlineTestProcess()
|
|
|
- p.RefreshLocalOnline([]string{"user1"}, nil, 1000, 20000)
|
|
|
|
|
- p.SetNodeOnlineClients(3, []string{"user1", "user2"})
|
|
|
|
|
- p.SetNodeOnlineClients(5, []string{"user3"})
|
|
|
|
|
-
|
|
|
|
|
- byNode := p.GetOnlineClientsByNode()
|
|
|
|
|
-
|
|
|
|
|
- assertSameSet(t, "local (key 0)", byNode[localNodeKey], []string{"user1"})
|
|
|
|
|
- assertSameSet(t, "node 3", byNode[3], []string{"user1", "user2"})
|
|
|
|
|
- assertSameSet(t, "node 5", byNode[5], []string{"user3"})
|
|
|
|
|
-
|
|
|
|
|
- if slices.Contains(byNode[5], "user1") {
|
|
|
|
|
- t.Errorf("user1 leaked onto node 5: %v", byNode[5])
|
|
|
|
|
- }
|
|
|
|
|
- if slices.Contains(byNode[localNodeKey], "user3") || slices.Contains(byNode[3], "user3") {
|
|
|
|
|
- t.Errorf("user3 leaked off node 5: local=%v node3=%v", byNode[localNodeKey], byNode[3])
|
|
|
|
|
|
|
+ // Node A (direct) reports its own clients plus sub-node B's tree.
|
|
|
|
|
+ p.SetNodeOnlineTree(1, map[string][]string{
|
|
|
|
|
+ "guid-a": {"user1", "user2"},
|
|
|
|
|
+ "guid-b": {"user3"}, // B is behind A; still keyed by B's own GUID
|
|
|
|
|
+ })
|
|
|
|
|
+ p.SetNodeOnlineTree(2, map[string][]string{
|
|
|
|
|
+ "guid-c": {"user4"},
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ merged := p.GetMergedNodeTrees()
|
|
|
|
|
+ assertSameSet(t, "guid-a", merged["guid-a"], []string{"user1", "user2"})
|
|
|
|
|
+ assertSameSet(t, "guid-b", merged["guid-b"], []string{"user3"})
|
|
|
|
|
+ assertSameSet(t, "guid-c", merged["guid-c"], []string{"user4"})
|
|
|
|
|
+
|
|
|
|
|
+ if slices.Contains(merged["guid-a"], "user3") {
|
|
|
|
|
+ t.Errorf("user3 (on sub-node B) leaked onto node A: %v", merged["guid-a"])
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestGetOnlineClientsByNodeOmitsEmptyGroups keeps the payload small: a node
|
|
|
|
|
-// with no online clients (e.g. just cleared) must not appear as an empty key.
|
|
|
|
|
-func TestGetOnlineClientsByNodeOmitsEmptyGroups(t *testing.T) {
|
|
|
|
|
|
|
+// TestMergedNodeTreesOmitsEmpty keeps the payload small: empty GUID sets don't
|
|
|
|
|
+// appear as keys.
|
|
|
|
|
+func TestMergedNodeTreesOmitsEmpty(t *testing.T) {
|
|
|
p := newOnlineTestProcess()
|
|
p := newOnlineTestProcess()
|
|
|
- p.SetNodeOnlineClients(3, []string{"user1"})
|
|
|
|
|
- p.SetNodeOnlineClients(7, []string{})
|
|
|
|
|
-
|
|
|
|
|
- byNode := p.GetOnlineClientsByNode()
|
|
|
|
|
-
|
|
|
|
|
- if _, ok := byNode[7]; ok {
|
|
|
|
|
- t.Errorf("node 7 has no online clients but is present: %v", byNode)
|
|
|
|
|
- }
|
|
|
|
|
- if _, ok := byNode[localNodeKey]; ok {
|
|
|
|
|
- t.Errorf("no local clients online but key 0 is present: %v", byNode)
|
|
|
|
|
|
|
+ p.SetNodeOnlineTree(1, map[string][]string{
|
|
|
|
|
+ "guid-a": {"user1"},
|
|
|
|
|
+ "guid-x": {},
|
|
|
|
|
+ })
|
|
|
|
|
+ if _, ok := p.GetMergedNodeTrees()["guid-x"]; ok {
|
|
|
|
|
+ t.Errorf("empty GUID set should be omitted: %v", p.GetMergedNodeTrees())
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestGetOnlineClientsUnionDedupes confirms the flat union (used by the
|
|
|
|
|
-// client-centric / total-count views) still merges every node and dedupes.
|
|
|
|
|
|
|
+// TestGetOnlineClientsUnionDedupes confirms the flat union (client-centric /
|
|
|
|
|
+// total-count views) merges local + every node and dedupes.
|
|
|
func TestGetOnlineClientsUnionDedupes(t *testing.T) {
|
|
func TestGetOnlineClientsUnionDedupes(t *testing.T) {
|
|
|
p := newOnlineTestProcess()
|
|
p := newOnlineTestProcess()
|
|
|
p.RefreshLocalOnline([]string{"user1"}, nil, 1000, 20000)
|
|
p.RefreshLocalOnline([]string{"user1"}, nil, 1000, 20000)
|
|
|
- p.SetNodeOnlineClients(3, []string{"user1", "user2"})
|
|
|
|
|
|
|
+ p.SetNodeOnlineTree(1, map[string][]string{"guid-a": {"user1", "user2"}})
|
|
|
|
|
|
|
|
assertSameSet(t, "union", p.GetOnlineClients(), []string{"user1", "user2"})
|
|
assertSameSet(t, "union", p.GetOnlineClients(), []string{"user1", "user2"})
|
|
|
}
|
|
}
|
|
@@ -77,18 +76,18 @@ func TestRefreshLocalOnlineGraceWindow(t *testing.T) {
|
|
|
const grace = 20000
|
|
const grace = 20000
|
|
|
|
|
|
|
|
p.RefreshLocalOnline([]string{"user1"}, nil, 1000, grace)
|
|
p.RefreshLocalOnline([]string{"user1"}, nil, 1000, grace)
|
|
|
- if got := p.GetOnlineClientsByNode()[localNodeKey]; !slices.Contains(got, "user1") {
|
|
|
|
|
|
|
+ if got := p.GetLocalOnlineClients(); !slices.Contains(got, "user1") {
|
|
|
t.Fatalf("user1 should be online right after activity, got %v", got)
|
|
t.Fatalf("user1 should be online right after activity, got %v", got)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
p.RefreshLocalOnline([]string{"user2"}, nil, 11000, grace)
|
|
p.RefreshLocalOnline([]string{"user2"}, nil, 11000, grace)
|
|
|
- got := p.GetOnlineClientsByNode()[localNodeKey]
|
|
|
|
|
|
|
+ got := p.GetLocalOnlineClients()
|
|
|
if !slices.Contains(got, "user1") || !slices.Contains(got, "user2") {
|
|
if !slices.Contains(got, "user1") || !slices.Contains(got, "user2") {
|
|
|
t.Fatalf("both within grace window, got %v", got)
|
|
t.Fatalf("both within grace window, got %v", got)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
p.RefreshLocalOnline(nil, nil, 22000, grace)
|
|
p.RefreshLocalOnline(nil, nil, 22000, grace)
|
|
|
- got = p.GetOnlineClientsByNode()[localNodeKey]
|
|
|
|
|
|
|
+ got = p.GetLocalOnlineClients()
|
|
|
if slices.Contains(got, "user1") {
|
|
if slices.Contains(got, "user1") {
|
|
|
t.Errorf("user1 (idle 21s, past grace) should have aged out, got %v", got)
|
|
t.Errorf("user1 (idle 21s, past grace) should have aged out, got %v", got)
|
|
|
}
|
|
}
|
|
@@ -97,40 +96,36 @@ func TestRefreshLocalOnlineGraceWindow(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestGetActiveInboundsByNodeTracksGraceWindow pins the fix for issue #4859: a
|
|
|
|
|
-// multi-inbound client must only count as online on inbounds that actually
|
|
|
|
|
-// carried traffic. The active-inbound signal honours the same grace window as
|
|
|
|
|
-// the online-email signal, and only this panel's tags report under key 0.
|
|
|
|
|
-func TestGetActiveInboundsByNodeTracksGraceWindow(t *testing.T) {
|
|
|
|
|
|
|
+// TestGetLocalActiveInboundsTracksGraceWindow pins #4859: a multi-inbound
|
|
|
|
|
+// client only counts online on inbounds that actually carried traffic, and the
|
|
|
|
|
+// active-inbound signal honours the same grace window as the online signal.
|
|
|
|
|
+func TestGetLocalActiveInboundsTracksGraceWindow(t *testing.T) {
|
|
|
p := newOnlineTestProcess()
|
|
p := newOnlineTestProcess()
|
|
|
const grace = 20000
|
|
const grace = 20000
|
|
|
|
|
|
|
|
p.RefreshLocalOnline([]string{"alice"}, []string{"inbound-a"}, 1000, grace)
|
|
p.RefreshLocalOnline([]string{"alice"}, []string{"inbound-a"}, 1000, grace)
|
|
|
- got := p.GetActiveInboundsByNode()[localNodeKey]
|
|
|
|
|
- assertSameSet(t, "active after first poll", got, []string{"inbound-a"})
|
|
|
|
|
|
|
+ assertSameSet(t, "active after first poll", p.GetLocalActiveInbounds(), []string{"inbound-a"})
|
|
|
|
|
|
|
|
p.RefreshLocalOnline([]string{"alice"}, []string{"inbound-b"}, 11000, grace)
|
|
p.RefreshLocalOnline([]string{"alice"}, []string{"inbound-b"}, 11000, grace)
|
|
|
- got = p.GetActiveInboundsByNode()[localNodeKey]
|
|
|
|
|
- assertSameSet(t, "both within grace", got, []string{"inbound-a", "inbound-b"})
|
|
|
|
|
|
|
+ assertSameSet(t, "both within grace", p.GetLocalActiveInbounds(), []string{"inbound-a", "inbound-b"})
|
|
|
|
|
|
|
|
p.RefreshLocalOnline(nil, nil, 22000, grace)
|
|
p.RefreshLocalOnline(nil, nil, 22000, grace)
|
|
|
- got = p.GetActiveInboundsByNode()[localNodeKey]
|
|
|
|
|
- assertSameSet(t, "inbound-a (idle 21s, past grace) aged out, inbound-b kept", got, []string{"inbound-b"})
|
|
|
|
|
|
|
+ assertSameSet(t, "inbound-a (idle 21s) aged out, inbound-b kept", p.GetLocalActiveInbounds(), []string{"inbound-b"})
|
|
|
|
|
|
|
|
p.RefreshLocalOnline(nil, nil, 40000, grace)
|
|
p.RefreshLocalOnline(nil, nil, 40000, grace)
|
|
|
- if _, ok := p.GetActiveInboundsByNode()[localNodeKey]; ok {
|
|
|
|
|
- t.Errorf("all inbounds idle past grace, key 0 should be absent: %v", p.GetActiveInboundsByNode())
|
|
|
|
|
|
|
+ if got := p.GetLocalActiveInbounds(); len(got) != 0 {
|
|
|
|
|
+ t.Errorf("all inbounds idle past grace, want empty, got %v", got)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestClearNodeOnlineClientsDropsNode mirrors a failed node probe: the node's
|
|
// TestClearNodeOnlineClientsDropsNode mirrors a failed node probe: the node's
|
|
|
-// clients must disappear from the per-node map immediately.
|
|
|
|
|
|
|
+// whole subtree contribution disappears immediately.
|
|
|
func TestClearNodeOnlineClientsDropsNode(t *testing.T) {
|
|
func TestClearNodeOnlineClientsDropsNode(t *testing.T) {
|
|
|
p := newOnlineTestProcess()
|
|
p := newOnlineTestProcess()
|
|
|
- p.SetNodeOnlineClients(3, []string{"user1"})
|
|
|
|
|
|
|
+ p.SetNodeOnlineTree(3, map[string][]string{"guid-a": {"user1"}})
|
|
|
p.ClearNodeOnlineClients(3)
|
|
p.ClearNodeOnlineClients(3)
|
|
|
|
|
|
|
|
- if _, ok := p.GetOnlineClientsByNode()[3]; ok {
|
|
|
|
|
- t.Errorf("node 3 should be absent after ClearNodeOnlineClients")
|
|
|
|
|
|
|
+ if _, ok := p.GetMergedNodeTrees()["guid-a"]; ok {
|
|
|
|
|
+ t.Errorf("node 3's subtree should be absent after ClearNodeOnlineClients")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|