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:
@@ -142,6 +142,12 @@ func Open(path string) (*KnoxDB, error) {
|
||||
return nil, fmt.Errorf("create locator index: %w", err)
|
||||
}
|
||||
|
||||
// Thread idempotency index must come after the cluster_key migration (a
|
||||
// fresh DB has the column from Schema; an existing DB gets it above).
|
||||
if _, err := db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_threads_cluster ON threads(cluster_key) WHERE cluster_key IS NOT NULL AND cluster_key != ''`); err != nil {
|
||||
return nil, fmt.Errorf("create cluster_key index: %w", err)
|
||||
}
|
||||
|
||||
return kdb, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,18 @@ func (k *KnoxDB) UpsertPeer(peerID, addr, name string, maxHCL int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// MergePeer records a peer discovered indirectly (via another peer's ping).
|
||||
// Unlike UpsertPeer it never clobbers the cursor — a freshly learned address
|
||||
// has no known knowledge yet; the anti-entropy pull will set it on contact.
|
||||
func (k *KnoxDB) MergePeer(peerID, addr, name string) error {
|
||||
_, err := k.db.Exec(
|
||||
`INSERT INTO peers (peer_id, addr, name) VALUES (?, ?, ?)
|
||||
ON CONFLICT(peer_id) DO UPDATE SET addr=?, name=?`,
|
||||
peerID, addr, name, addr, name,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListPeers returns known peers ordered by first-seen.
|
||||
func (k *KnoxDB) ListPeers() ([]Peer, error) {
|
||||
rows, err := k.db.Query(`SELECT peer_id, COALESCE(addr,''), COALESCE(name,''), COALESCE(last_handshake,''), COALESCE(cursor,0), COALESCE(created_at,'') FROM peers ORDER BY created_at`)
|
||||
@@ -181,6 +193,61 @@ type Peer struct {
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
// PeerInfo is the shareable (non-secret) subset of a peer that /v1/ping
|
||||
// advertises so other nodes can discover the swarm.
|
||||
type PeerInfo struct {
|
||||
PeerID string `json:"peer_id"`
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ShareablePeers returns the peers this node knows about, for dissemination in
|
||||
// ping responses. Self and peers without an address are excluded.
|
||||
func (k *KnoxDB) ShareablePeers() ([]PeerInfo, error) {
|
||||
rows, err := k.db.Query(
|
||||
`SELECT peer_id, COALESCE(addr,''), COALESCE(name,'') FROM peers WHERE addr<>'' AND peer_id<>? ORDER BY peer_id`,
|
||||
k.NodeID(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []PeerInfo
|
||||
for rows.Next() {
|
||||
var p PeerInfo
|
||||
if err := rows.Scan(&p.PeerID, &p.Addr, &p.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// MaxHCLForNode returns the maximum HCL this node holds for a given origin
|
||||
// node, or 0 if none.
|
||||
func (k *KnoxDB) MaxHCLForNode(nodeID string) int64 {
|
||||
var m int64
|
||||
if err := k.db.QueryRow(`SELECT MAX(hcl) FROM observations WHERE node_id=?`, nodeID).Scan(&m); err != nil {
|
||||
return 0
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// SwarmPeerAddrs returns the addresses of all known peers (shareable set). It
|
||||
// is what the sweep iterates after a bootstrap join.
|
||||
func (k *KnoxDB) SwarmPeerAddrs() ([]string, error) {
|
||||
peers, err := k.ShareablePeers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
addrs = append(addrs, p.Addr)
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// DistinctFingerprints returns the set of all observed fingerprints — the
|
||||
// ground-truth index of what this node knows. Used by gossip diff.
|
||||
func (k *KnoxDB) DistinctFingerprints() (map[string]bool, error) {
|
||||
|
||||
@@ -105,8 +105,6 @@ CREATE TABLE IF NOT EXISTS threads (
|
||||
cluster_key TEXT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_threads_cluster ON threads(cluster_key) WHERE cluster_key IS NOT NULL AND cluster_key != '';
|
||||
|
||||
-- SETTINGS: key-value store for runtime configuration
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
|
||||
Reference in New Issue
Block a user