2026-09-22 13:13:10 +02:00
|
|
|
package dns
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type cacheEntry[V any] struct {
|
|
|
|
|
val V
|
|
|
|
|
exp time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shard[V any] struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
m map[string]cacheEntry[V]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TTLCache is a sharded, memory-bounded, in-memory TTL cache. It is used for
|
|
|
|
|
// positive DNS results, negative DNS results, CNAME targets and hostname dedup.
|
|
|
|
|
//
|
|
|
|
|
// ponytail: once a shard is full, an arbitrary entry is evicted rather than the
|
|
|
|
|
// least-recently-used one. Swap in an LRU only if the hit rate actually matters.
|
|
|
|
|
type TTLCache[V any] struct {
|
|
|
|
|
shards []shard[V]
|
|
|
|
|
mask uint64
|
|
|
|
|
maxShard int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewTTLCache[V any](maxEntries, shards int) *TTLCache[V] {
|
|
|
|
|
if shards < 1 {
|
|
|
|
|
shards = 1
|
|
|
|
|
}
|
|
|
|
|
n := 1
|
|
|
|
|
for n < shards {
|
|
|
|
|
n <<= 1
|
|
|
|
|
}
|
|
|
|
|
maxShard := maxEntries / n
|
|
|
|
|
if maxShard < 1 {
|
|
|
|
|
maxShard = 1
|
|
|
|
|
}
|
|
|
|
|
c := &TTLCache[V]{mask: uint64(n - 1), maxShard: maxShard}
|
|
|
|
|
c.shards = make([]shard[V], n)
|
|
|
|
|
for i := range c.shards {
|
2026-09-22 13:33:05 +02:00
|
|
|
// Deliberately no capacity hint: pre-sizing every shard to maxShard
|
|
|
|
|
// allocates the full theoretical capacity (hundreds of MB across the
|
|
|
|
|
// caches) up front. Let the maps grow on demand; maxShard still bounds
|
|
|
|
|
// them via eviction.
|
|
|
|
|
c.shards[i].m = make(map[string]cacheEntry[V])
|
2026-09-22 13:13:10 +02:00
|
|
|
}
|
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hashString(s string) uint64 {
|
|
|
|
|
const prime = 1099511628211
|
|
|
|
|
h := uint64(14695981039346656037)
|
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
|
|
h ^= uint64(s[i])
|
|
|
|
|
h *= prime
|
|
|
|
|
}
|
|
|
|
|
return h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *TTLCache[V]) Get(key string) (V, bool) {
|
|
|
|
|
s := &c.shards[hashString(key)&c.mask]
|
|
|
|
|
s.mu.Lock()
|
|
|
|
|
e, ok := s.m[key]
|
|
|
|
|
if ok && time.Now().Before(e.exp) {
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
return e.val, true
|
|
|
|
|
}
|
|
|
|
|
if ok {
|
|
|
|
|
delete(s.m, key)
|
|
|
|
|
}
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
var zero V
|
|
|
|
|
return zero, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *TTLCache[V]) Set(key string, val V, ttl time.Duration) {
|
|
|
|
|
if ttl <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s := &c.shards[hashString(key)&c.mask]
|
|
|
|
|
s.mu.Lock()
|
|
|
|
|
if len(s.m) >= c.maxShard {
|
|
|
|
|
for k := range s.m {
|
|
|
|
|
delete(s.m, k)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.m[key] = cacheEntry[V]{val: val, exp: time.Now().Add(ttl)}
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *TTLCache[V]) Len() int {
|
|
|
|
|
total := 0
|
|
|
|
|
for i := range c.shards {
|
|
|
|
|
c.shards[i].mu.Lock()
|
|
|
|
|
total += len(c.shards[i].m)
|
|
|
|
|
c.shards[i].mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
return total
|
|
|
|
|
}
|