-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsharded_cache.go
More file actions
122 lines (109 loc) · 2.68 KB
/
Copy pathsharded_cache.go
File metadata and controls
122 lines (109 loc) · 2.68 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
package uax
import (
"hash/maphash"
"sync"
)
// ShardedCache distributes cached entries across multiple independent LRU shards
// to reduce lock contention under high concurrency.
type ShardedCache struct {
parser *Parser
shards []cacheShard
shardCount uint64
seed maphash.Seed
}
type cacheShard struct {
mu sync.RWMutex
entries map[string]*lruEntry
lru lruList
maxSize int
hits int64
misses int64
}
// NewShardedCache creates a sharded cache with the given number of shards
// and per-shard capacity. Total capacity = shards * perShardSize.
func NewShardedCache(p *Parser, shards, perShardSize int) *ShardedCache {
if shards <= 0 {
shards = 16
}
if perShardSize <= 0 {
perShardSize = 256
}
sc := &ShardedCache{
parser: p,
shards: make([]cacheShard, shards),
shardCount: uint64(shards),
seed: maphash.MakeSeed(),
}
for i := range sc.shards {
sc.shards[i].entries = make(map[string]*lruEntry, perShardSize)
sc.shards[i].maxSize = perShardSize
}
return sc
}
// ParseString parses a UA string with sharded caching.
func (sc *ShardedCache) ParseString(ua string) Result {
shard := &sc.shards[sc.shardIndex(ua)]
return shard.getOrParse(ua, sc.parser)
}
// Parse parses an Input. Only caches if no Client Hints (CH makes each request unique).
func (sc *ShardedCache) Parse(input Input) Result {
if input.HasClientHints() {
return sc.parser.Parse(input)
}
return sc.ParseString(input.UAString)
}
// Stats returns aggregated cache statistics across all shards.
func (sc *ShardedCache) Stats() CacheStats {
var s CacheStats
for i := range sc.shards {
sc.shards[i].mu.RLock()
s.Size += len(sc.shards[i].entries)
s.Hits += sc.shards[i].hits
s.Misses += sc.shards[i].misses
sc.shards[i].mu.RUnlock()
}
return s
}
func (sc *ShardedCache) shardIndex(key string) uint64 {
var h maphash.Hash
h.SetSeed(sc.seed)
h.WriteString(key)
return h.Sum64() % sc.shardCount
}
func (s *cacheShard) getOrParse(ua string, p *Parser) Result {
// Fast path: read lock
s.mu.RLock()
if e, ok := s.entries[ua]; ok {
s.mu.RUnlock()
s.mu.Lock()
// Re-validate: entry may have been evicted between RUnlock and Lock
if _, still := s.entries[ua]; still {
s.lru.promote(e)
s.hits++
}
s.mu.Unlock()
return e.result
}
s.mu.RUnlock()
// Miss: parse
result := p.ParseString(ua)
s.mu.Lock()
// Double-check
if e, ok := s.entries[ua]; ok {
s.lru.promote(e)
s.hits++
s.mu.Unlock()
return e.result
}
s.misses++
e := &lruEntry{key: ua, result: result}
s.entries[ua] = e
s.lru.pushFront(e)
for len(s.entries) > s.maxSize {
if tail := s.lru.evictTail(); tail != nil {
delete(s.entries, tail.key)
}
}
s.mu.Unlock()
return result
}