feat: M2 composite locator + idle reconcile
Refs #1 - observations: hcl backfilled from local rowid; (node_id, hcl) UNIQUE locator index becomes the gossip merge key - threads: cluster_key column + partial UNIQUE index; CreateThreadCluster is idempotent, threader folds into exact cluster_key before heuristic - AutoLinkThreadObservations dedup re-expressed on hcl DESC - new `knox reconcile [--dry-run]` rebuilds entries from the observation log and re-links threads idempotently (entry count, thread cluster_key verified bit-identical from log-only DB)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/david/knox/internal/db"
|
||||
"github.com/david/knox/internal/watch"
|
||||
)
|
||||
|
||||
// NewReconcileCmd rebuilds all derived state (entries cache, threads) from the
|
||||
// append-only observation log. With --dry-run it reports what would change
|
||||
// without writing.
|
||||
func NewReconcileCmd(kdb *db.KnoxDB) *cobra.Command {
|
||||
var dryRun bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "reconcile",
|
||||
Short: "Rebuild entries and threads from the observation log",
|
||||
Long: `Reconcile makes derived state converge on the append-only observation log.
|
||||
|
||||
Entries are dropped and rebuilt via deterministic SQL aggregation; the
|
||||
auto-threader then re-links/create threads idempotently by cluster_key. After a
|
||||
gossip pull, reconcile brings a node's materialized view in line with whatever
|
||||
observations it now holds.`,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
if dryRun {
|
||||
return dryRunReconcile(kdb)
|
||||
}
|
||||
return reconcile(kdb)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "report drift without writing")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func reconcile(kdb *db.KnoxDB) error {
|
||||
before, after, err := kdb.RebuildEntriesFromObservations()
|
||||
if err != nil {
|
||||
return fmt.Errorf("rebuild entries: %w", err)
|
||||
}
|
||||
threader := watch.NewAutoThreader(kdb)
|
||||
created, linked, err := threader.AutoThread()
|
||||
if err != nil {
|
||||
return fmt.Errorf("auto-thread: %w", err)
|
||||
}
|
||||
fmt.Printf("reconciled: entries %d -> %d, %d threads created, %d observations linked\n", before, after, created, linked)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dryRunReconcile(kdb *db.KnoxDB) error {
|
||||
// Ground truth from the log (computed in a throwaway way via a count of
|
||||
// what rebuild would produce) vs the current materialized cache.
|
||||
current, _ := kdb.EntryCount()
|
||||
logCount := kdb.ObservationEntryEstimate()
|
||||
|
||||
threader := watch.NewAutoThreader(kdb)
|
||||
threader.DryRun = true
|
||||
created, linked, _ := threader.AutoThread()
|
||||
|
||||
fmt.Printf("drift: %d entries current, %d from log (%+d), %d threads would be created, %d obs would be linked\n",
|
||||
current, logCount, logCount-current, created, linked)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user