feat: M3 peer gossip protocol
Refs #1 - peers table (peer_id, addr, cursor, last_handshake) - watch serves HTTP API: GET /v1/ping (knowledge vector), GET /v1/log?node=&after= (cursor-paged pull), POST /v1/obs/batch - anti-entropy sweep (Run): ping, pull what we lack, push own obs; echo suppressed by node_id ownership; reconcile-on-pull - config via KNOX_PEERS / KNOX_PEER_ADDR; knox gossip status - db: GossipObservation wire type, PushObservations, ObservationsAfter, KnowledgeVector, peer upsert/list - integration tests: bidirectional convergence + idempotency
This commit is contained in:
+57
-1
@@ -2,6 +2,7 @@ package watch
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -12,7 +13,10 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
const browserInterval = 5 * time.Minute
|
||||
const (
|
||||
browserInterval = 5 * time.Minute
|
||||
gossipInterval = 1 * time.Minute
|
||||
)
|
||||
|
||||
type Watcher struct {
|
||||
knoxDB *db.KnoxDB
|
||||
@@ -53,15 +57,31 @@ func (w *Watcher) Start() error {
|
||||
log.Printf("[knox] obsidian vault: %s", w.vault)
|
||||
}
|
||||
|
||||
// Gossip: serve our observation log to peers and periodically converge.
|
||||
gossipAddr := ListenAddr()
|
||||
if peers := PeerAddrs(); len(peers) > 0 {
|
||||
node := &Node{Kdb: w.knoxDB, Name: "knox", Addr: gossipAddr}
|
||||
srv := &http.Server{Addr: gossipAddr, Handler: node.Handler()}
|
||||
go func() {
|
||||
log.Printf("[knox] gossip listening on %s", gossipAddr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[knox] gossip server: %v", err)
|
||||
}
|
||||
}()
|
||||
log.Printf("[knox] gossip peers: %v", peers)
|
||||
}
|
||||
|
||||
debounceMap := make(map[string]time.Time)
|
||||
browserTicker := time.NewTicker(browserInterval)
|
||||
giteaTicker := time.NewTicker(10 * time.Minute)
|
||||
gitTicker := time.NewTicker(10 * time.Minute)
|
||||
threadTicker := time.NewTicker(10 * time.Minute)
|
||||
gossipTicker := time.NewTicker(gossipInterval)
|
||||
browserRunning := false
|
||||
giteaRunning := false
|
||||
gitRunning := false
|
||||
threadRunning := false
|
||||
gossipRunning := false
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -119,6 +139,15 @@ func (w *Watcher) Start() error {
|
||||
defer func() { threadRunning = false }()
|
||||
w.autoThread()
|
||||
}()
|
||||
case <-gossipTicker.C:
|
||||
if gossipRunning {
|
||||
continue
|
||||
}
|
||||
gossipRunning = true
|
||||
go func() {
|
||||
defer func() { gossipRunning = false }()
|
||||
w.syncGossip()
|
||||
}()
|
||||
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
@@ -404,6 +433,33 @@ func (w *Watcher) autoThread() {
|
||||
}
|
||||
}
|
||||
|
||||
// syncGossip runs one anti-entropy sweep against configured peers and rebuilds
|
||||
// derived state if anything new arrived.
|
||||
func (w *Watcher) syncGossip() {
|
||||
peers := PeerAddrs()
|
||||
if len(peers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
before := 0
|
||||
if n, err := w.knoxDB.EntryCount(); err == nil {
|
||||
before = n
|
||||
}
|
||||
|
||||
Run(w.knoxDB, peers)
|
||||
|
||||
// If new observations arrived, reconcile to pick up entries/threads they
|
||||
// imply (deterministic log → derived rebuild).
|
||||
if after, err := w.knoxDB.EntryCount(); err == nil && after > before {
|
||||
created, linked, err := Reconcile(w.knoxDB)
|
||||
if err != nil {
|
||||
log.Printf("[knox] gossip reconcile: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[knox] gossip reconcile done: %d created, %d linked", created, linked)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) isRelevantEvent(event fsnotify.Event) bool {
|
||||
if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user