-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache.go
More file actions
144 lines (126 loc) · 3.17 KB
/
Copy pathcache.go
File metadata and controls
144 lines (126 loc) · 3.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package horosvec
import "sync"
// cachedNode holds graph node data in the LRU cache.
type cachedNode struct {
nodeID int64
extID []byte // external ID for result mapping
neighbors []int64
vec []float32 // raw vector for exact L2 during search
code []byte // RaBitQ 1-bit code
sqNorm float64
l1Norm float64
// doubly-linked list pointers
prev, next *cachedNode
}
// nodeCache is a thread-safe LRU cache for graph nodes.
type nodeCache struct {
mu sync.RWMutex
capacity int
items map[int64]*cachedNode
// doubly-linked list: head = most recently used, tail = least recently used
head, tail *cachedNode
}
// newNodeCache creates an LRU cache with the given capacity.
func newNodeCache(capacity int) *nodeCache {
return &nodeCache{
capacity: capacity,
items: make(map[int64]*cachedNode, capacity),
}
}
// get retrieves a node from the cache and promotes it in the LRU.
// Returns nil if not found. Uses a single write lock to avoid TOCTOU
// between lookup and promotion.
func (c *nodeCache) get(nodeID int64) *cachedNode {
c.mu.Lock()
node, ok := c.items[nodeID]
if !ok {
c.mu.Unlock()
return nil
}
c.moveToHead(node)
c.mu.Unlock()
return node
}
// getReadOnly retrieves a node without updating LRU order.
// Safe for concurrent readers — only takes a read lock.
// Use on hot search paths where LRU promotion is not needed
// (cache is typically warm and no eviction occurs during search).
func (c *nodeCache) getReadOnly(nodeID int64) *cachedNode {
c.mu.RLock()
node := c.items[nodeID]
c.mu.RUnlock()
return node
}
// put adds or updates a node in the cache.
// Lock covers put+eviction loop. If capacity very small, evictTail chain may interact with concurrent get() promotion window.
func (c *nodeCache) put(node *cachedNode) {
c.mu.Lock()
defer c.mu.Unlock()
if existing, ok := c.items[node.nodeID]; ok {
// Update existing
existing.extID = node.extID
existing.neighbors = node.neighbors
existing.vec = node.vec
existing.code = node.code
existing.sqNorm = node.sqNorm
existing.l1Norm = node.l1Norm
c.moveToHead(existing)
return
}
// Add new
c.items[node.nodeID] = node
c.addToHead(node)
// Evict if over capacity
for len(c.items) > c.capacity {
c.evictTail()
}
}
// clear removes all entries from the cache.
func (c *nodeCache) clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = make(map[int64]*cachedNode, c.capacity)
c.head = nil
c.tail = nil
}
// --- internal linked list operations (must hold mu) ---
func (c *nodeCache) addToHead(node *cachedNode) {
node.prev = nil
node.next = c.head
if c.head != nil {
c.head.prev = node
}
c.head = node
if c.tail == nil {
c.tail = node
}
}
func (c *nodeCache) removeNode(node *cachedNode) {
if node.prev != nil {
node.prev.next = node.next
} else {
c.head = node.next
}
if node.next != nil {
node.next.prev = node.prev
} else {
c.tail = node.prev
}
node.prev = nil
node.next = nil
}
func (c *nodeCache) moveToHead(node *cachedNode) {
if c.head == node {
return
}
c.removeNode(node)
c.addToHead(node)
}
func (c *nodeCache) evictTail() {
if c.tail == nil {
return
}
node := c.tail
c.removeNode(node)
delete(c.items, node.nodeID)
}