feat: M1 determinism fixes for gossip
Refs #1 - F1: canonical fingerprints (git identity by remote URL, log by basename, obsidian by relative vault path) so identical facts get identical ids across machines - F2: node_id (persisted in settings) + Hybrid Logical Clock in all observation "when" columns; removed time.Now() fact-time fallbacks - F3: stable TF-IDF tie-break sort (score desc, term asc) - F4: observations carry (node_id, hcl) locator; dedup ordered by hcl
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// Package hlc implements a Hybrid Logical Clock for ordering observations.
|
||||
//
|
||||
// A node's HLC is monotonic even when wall clocks jump (NTP correction, suspend).
|
||||
// Values are packed into an int64: high bits = wall-clock milliseconds, low bits
|
||||
// = per-millisecond sequence. Lexicographic comparison of the packed value is a
|
||||
// causal order (states: causally-related events have distinct values; concurrent
|
||||
// events never collide because the sequence bumps on any wall-clock stall).
|
||||
package hlc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// seqBits is the number of low bits reserved for the per-millisecond sequence,
|
||||
// giving 2^22 ≈ 4.2M slots per ms — far beyond ingest rates.
|
||||
const seqBits = 22
|
||||
const seqMask = int64(1)<<seqBits - 1
|
||||
const wallShift = seqBits
|
||||
|
||||
// Clock is a single-writer HLC. It is safe for concurrent use.
|
||||
type Clock struct {
|
||||
mu sync.Mutex
|
||||
wallMS int64 // last observed wall-clock millis
|
||||
seq int64 // sequence within the current wallMillis bucket
|
||||
}
|
||||
|
||||
func New() *Clock { return &Clock{} }
|
||||
|
||||
// Now returns the next monotonic HLC value and the wall-clock time embedded in
|
||||
// it. The returned time is the HLC's wall component — never ahead of the local
|
||||
// clock beyond the current call and never rewinding across calls.
|
||||
func (c *Clock) Now() (int64, time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
w := time.Now().UnixMilli()
|
||||
if w > c.wallMS {
|
||||
c.wallMS = w
|
||||
c.seq = 0
|
||||
} else {
|
||||
// Wall clock stalled or went backwards (NTP): keep wallMS but bump seq
|
||||
// so the value is still strictly increasing.
|
||||
if c.seq >= seqMask {
|
||||
// Extremely unlikely (4.2M events in one ms); jump the wall lazily.
|
||||
c.wallMS++
|
||||
c.seq = 0
|
||||
} else {
|
||||
c.seq++
|
||||
}
|
||||
}
|
||||
|
||||
v := c.wallMS<<wallShift | c.seq
|
||||
return v, time.UnixMilli(c.wallMS).UTC()
|
||||
}
|
||||
|
||||
// WallTime extracts the wall-clock component embedded in a packed HLC value.
|
||||
func WallTime(v int64) time.Time {
|
||||
return time.UnixMilli(v >> wallShift).UTC()
|
||||
}
|
||||
Reference in New Issue
Block a user