feat: M4 gossip ops and UX
Refs #1 - /v1/diff endpoint returns each node's fingerprint set + tombstoned auto-thread status - knox gossip diff <peer-url>: shows peer-only/local-only fingerprints (pull/keep preview) and tombstone divergence (resolved-on-peer vs would-resurrect) - knox gossip sync: one-shot anti-entropy sweep + reconcile - gossip server starts before initial seed so peers can reach a booting node; KNOX_PEER_ADDR alone now serves without KNOX_PEERS - integration tests for diff + tombstone reporting - e2e verified: two live daemons, diff previewed 1655 peer-only fps, sync converged second node to 1655 observations
This commit is contained in:
@@ -45,6 +45,113 @@ func NewGossipCmd(kdb *db.KnoxDB) *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "diff <peer-url>",
|
||||
Short: "Show observation/thread divergence with a peer",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
return gossipDiff(kdb, args[0])
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Run one anti-entropy sweep against configured peers",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
peers := watch.PeerAddrs()
|
||||
if len(peers) == 0 {
|
||||
return fmt.Errorf("no peers configured (set KNOX_PEERS)")
|
||||
}
|
||||
watch.Run(kdb, peers)
|
||||
created, linked, err := watch.Reconcile(kdb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("sync done: %d threads created, %d observations linked\n", created, linked)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// gossipDiff compares this node's observation log + auto-threads with a peer's
|
||||
// via /v1/diff. Never writes; it is the preview for what a sync would adopt.
|
||||
func gossipDiff(kdb *db.KnoxDB, peerAddr string) error {
|
||||
c := &watch.Client{Addr: peerAddr}
|
||||
remote, err := c.Diff()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
local, err := kdb.DistinctFingerprints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remoteFP := make(map[string]bool, len(remote.Fingerprints))
|
||||
for _, f := range remote.Fingerprints {
|
||||
remoteFP[f] = true
|
||||
}
|
||||
|
||||
var peerOnly, localOnly []string
|
||||
for f := range remoteFP {
|
||||
if !local[f] {
|
||||
peerOnly = append(peerOnly, f)
|
||||
}
|
||||
}
|
||||
for f := range local {
|
||||
if !remoteFP[f] {
|
||||
localOnly = append(localOnly, f)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("peer: %s (%s)\n", remote.Name, remote.NodeID)
|
||||
fmt.Printf("this node: %s\n", kdb.NodeID())
|
||||
fmt.Printf("fingerprints: peer=%d this=%d\n", len(remoteFP), len(local))
|
||||
fmt.Printf(" peer-only (would pull): %d\n", len(peerOnly))
|
||||
for _, f := range peerOnly[:min(10, len(peerOnly))] {
|
||||
fmt.Printf(" %s\n", f)
|
||||
}
|
||||
fmt.Printf(" local-only (would lose): %d\n", len(localOnly))
|
||||
for _, f := range localOnly[:min(10, len(localOnly))] {
|
||||
fmt.Printf(" %s\n", f)
|
||||
}
|
||||
|
||||
// Tombstone divergence on auto-threaded threads.
|
||||
localThreads, err := kdb.ThreadStatusByCluster()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remoteThreads := remote.ThreadStatus
|
||||
var resolvedPeer, revokedLocal []string
|
||||
for k, st := range remoteThreads {
|
||||
if st == "resolved" {
|
||||
if ls, ok := localThreads[k]; !ok || ls != "resolved" {
|
||||
resolvedPeer = append(resolvedPeer, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, st := range localThreads {
|
||||
if st == "resolved" {
|
||||
if rs, ok := remoteThreads[k]; !ok || rs != "resolved" {
|
||||
revokedLocal = append(revokedLocal, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(resolvedPeer) > 0 {
|
||||
fmt.Printf("threads resolved on peer but not here (tombstone to adopt): %d\n", len(resolvedPeer))
|
||||
}
|
||||
if len(revokedLocal) > 0 {
|
||||
fmt.Printf("threads resolved here but not on peer (would resurrect on sync): %d\n", len(revokedLocal))
|
||||
}
|
||||
if len(peerOnly) == 0 && len(localOnly) == 0 && len(resolvedPeer) == 0 && len(revokedLocal) == 0 {
|
||||
fmt.Println("in sync.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user