feat: swarm membership discovery via peer-list gossip
Refs #1 - /v1/ping now advertises the node's known peers (peer_id, addr, name) - Run sweeps static KNOX_PEERS + persisted discovered peers, enqueueing newly-learned addresses for direct sweeps (membership-only relay; no observation relay) - db: ShareablePeers, SwarmPeerAddrs, MergePeer (cursor-preserving discovery upsert), MaxHCLForNode - integration test: a node configured with a single seed discovers and pulls from other swarm members without direct configuration
This commit is contained in:
@@ -84,8 +84,77 @@ func TestGossipConvergence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGossipDiff ensures /v1/diff reports per-node observation fingerprints and
|
||||
// tombstoned thread divergence.
|
||||
// TestGossipSwarmDiscovery: C only knows A. A knows B. When C sweeps A, it must
|
||||
// learn about B through A's ping, enqueue B, and pull B's observations — with no
|
||||
// direct configuration of B (no relay of data, only membership).
|
||||
func TestGossipSwarmDiscovery(t *testing.T) {
|
||||
a := tmpKnoxDB(t)
|
||||
b := tmpKnoxDB(t)
|
||||
c := tmpKnoxDB(t)
|
||||
|
||||
seedObs(a, "AAA")
|
||||
seedObs(b, "BBB")
|
||||
seedObs(c, "CCC")
|
||||
|
||||
nodeA := &Node{Kdb: a, Name: "A"}
|
||||
sa := httptest.NewServer(nodeA.Handler())
|
||||
defer sa.Close()
|
||||
nodeB := &Node{Kdb: b, Name: "B"}
|
||||
sb := httptest.NewServer(nodeB.Handler())
|
||||
defer sb.Close()
|
||||
nodeC := &Node{Kdb: c, Name: "C"}
|
||||
sc := httptest.NewServer(nodeC.Handler())
|
||||
defer sc.Close()
|
||||
|
||||
// A discovers B (A pings B) so A can advertise B to the swarm.
|
||||
Run(a, []string{sb.URL})
|
||||
|
||||
// C only knows A. A single sweep should surface B (membership in ping)
|
||||
// and pull B's observations directly.
|
||||
Run(c, []string{sa.URL})
|
||||
|
||||
// C must know B and hold all three origin logs.
|
||||
peers, err := c.ListPeers()
|
||||
if err != nil {
|
||||
t.Fatalf("list peers: %v", err)
|
||||
}
|
||||
foundB := false
|
||||
for _, p := range peers {
|
||||
if p.PeerID == b.NodeID() {
|
||||
foundB = true
|
||||
}
|
||||
}
|
||||
if !foundB {
|
||||
t.Fatalf("C did not discover B via A's membership list; peers=%v", peers)
|
||||
}
|
||||
|
||||
vec, err := c.KnowledgeVector()
|
||||
if err != nil {
|
||||
t.Fatalf("c vector: %v", err)
|
||||
}
|
||||
if len(vec) != 3 {
|
||||
t.Errorf("C should hold 3 origin logs (A, B, C), got %v", vec)
|
||||
}
|
||||
|
||||
// C's copy of B's log must match B's own max hcl.
|
||||
bmax := c.MaxHCLForNode(b.NodeID())
|
||||
bm, err := bNodeMax(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bmax != bm {
|
||||
t.Errorf("C max hcl for B=%d, B reports %d", bmax, bm)
|
||||
}
|
||||
}
|
||||
|
||||
// bNodeMax is a helper reading the max hcl B holds for its own node.
|
||||
func bNodeMax(b *db.KnoxDB) (int64, error) {
|
||||
rows, err := b.KnowledgeVector()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return rows[b.NodeID()], nil
|
||||
}
|
||||
func TestGossipDiff(t *testing.T) {
|
||||
a := tmpKnoxDB(t)
|
||||
b := tmpKnoxDB(t)
|
||||
|
||||
Reference in New Issue
Block a user