fix: harden gossip, HLC restarts, watcher races, MCP args, pagination
- gossip: validate push batches (4 MiB / 1000-row caps); reject rows claiming the local node id (vector-poisoning), empty ids, negative HCLs - gossip: reconcile derived state after pulls (pulls only append to the observation log, so entry-count comparison could never trigger it) - hlc: seek clock from persisted MAX(hcl) at Open so a restart with a regressed wall clock cannot reissue values (locator/cursor safety) - db: serialize writers via BEGIN IMMEDIATE DSN, single conn per pool, and a per-KnoxDB mutex around RecordObservation's dedup - watch: atomic ticker guards (was a cross-goroutine data race), trailing-edge per-path debounce, recursive directory watches, rename re-ingest, remove cancels pending ingests - mcp: strict argument validation (no silent clamping), thread existence checks before writes, nil-safe golden-thread tool - cli: --page 0 no longer panics; query/recent pagination actually pages - tests: hlc SeekTo monotonicity, concurrent dedup race, push validation, batch caps, idempotency on observation counts
This commit is contained in:
@@ -98,10 +98,16 @@ func nextCursor(rows []db.GossipObservation) int64 {
|
||||
return rows[len(rows)-1].HCL
|
||||
}
|
||||
|
||||
// maxBatchRows bounds the number of observations a peer may push in one POST.
|
||||
// Pull already pages at 500 rows, so any larger batch is at best redundant and
|
||||
// at worst a flood; capping keeps memory and insert work bounded.
|
||||
const maxBatchRows = 1000
|
||||
|
||||
func (n *Node) handleBatch(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 4<<20) // 4 MiB
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
http.Error(w, "batch too large or unreadable", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var rows []db.GossipObservation
|
||||
@@ -109,6 +115,10 @@ func (n *Node) handleBatch(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(rows) > maxBatchRows {
|
||||
http.Error(w, fmt.Sprintf("batch too large: %d rows (max %d)", len(rows), maxBatchRows), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
inserted, err := n.Kdb.PushObservations(rows)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -273,7 +283,7 @@ func (c *Client) Diff() (*DiffSummary, error) {
|
||||
// node pulls/pushes directly with every other node it learns about.
|
||||
//
|
||||
// m, when non-nil, receives gossip event counters (nil for one-shot CLI runs).
|
||||
func Run(kdb *db.KnoxDB, m *metrics.Metrics, static []string) {
|
||||
func Run(kdb *db.KnoxDB, m *metrics.Metrics, static []string) int {
|
||||
myID := kdb.NodeID()
|
||||
|
||||
// Seed the work queue with static config plus persisted discoveries.
|
||||
@@ -284,6 +294,7 @@ func Run(kdb *db.KnoxDB, m *metrics.Metrics, static []string) {
|
||||
|
||||
seen := make(map[string]bool) // addr → handled (also suppresses self)
|
||||
queue := 0
|
||||
pulledTotal := 0
|
||||
for queue < len(work) {
|
||||
addr := strings.TrimSpace(work[queue])
|
||||
queue++
|
||||
@@ -338,6 +349,7 @@ func Run(kdb *db.KnoxDB, m *metrics.Metrics, static []string) {
|
||||
pulled += n
|
||||
}
|
||||
}
|
||||
pulledTotal += pulled
|
||||
if m != nil {
|
||||
m.IncrementPull(pulled)
|
||||
}
|
||||
@@ -356,6 +368,7 @@ func Run(kdb *db.KnoxDB, m *metrics.Metrics, static []string) {
|
||||
log.Printf("[gossip] synced with %s (%s): in sync (pushed %d)", p.NodeID, addr, pushed)
|
||||
}
|
||||
}
|
||||
return pulledTotal
|
||||
}
|
||||
|
||||
func kdbVectorGet(kdb *db.KnoxDB, nodeID string) int64 {
|
||||
|
||||
Reference in New Issue
Block a user