fix data races in cache, resolver, qlog; add cache/zonefile/blocklist tests; update STATUS
- cache.Entry.Hits and LRU.perShard become atomics; SetCapacity now evicts down to the new budget; packKey fully qualifies names before packing - resolver.Pool rewritten copy-on-write (atomic.Pointer to an immutable state); Upstream is immutable with shared *upstreamStats, so health survives a Rebind and the query path stops racing the config hook - Pool.Exchange no longer marks a dead upstream healthy: Success was called before the error check, so the breaker never tripped - qlog live ring publishes through atomic pointers - blocklist.Holder.Get nil-safe; Builder lists each source once - web: fix toast(string) misuse that rendered empty mutation toasts - add unit tests for cache (with benchmarks) and zonefile - STATUS.md: green -race integration suite, remaining failures and the broken bbolt snapshot round trip documented
This commit is contained in:
1 parent
10e8a9b91a
commit
aa12b03eb9
7 files changed
+228
-144
No files matched your search
+34
-14
@@ -214,13 +214,16 @@ type SourceMeta struct {
|
||||
|
||||
// Builder accumulates rules and produces a Set.
|
||||
type Builder struct {
|
||||
exact map[string]*Entry
|
||||
sub map[string]*Entry
|
||||
root map[string]*Entry
|
||||
meta map[string]SourceMeta
|
||||
order []string
|
||||
dup int
|
||||
counts RuleCounts
|
||||
exact map[string]*Entry
|
||||
sub map[string]*Entry
|
||||
root map[string]*Entry
|
||||
meta map[string]SourceMeta
|
||||
order []string
|
||||
// ordered mirrors order, so a source is listed once even when both a rule
|
||||
// and its metadata mention it.
|
||||
ordered map[string]bool
|
||||
dup int
|
||||
counts RuleCounts
|
||||
}
|
||||
|
||||
// NewBuilder returns an empty Builder.
|
||||
@@ -286,9 +289,7 @@ func (b *Builder) Add(r Rule, source string, categories []string) {
|
||||
}
|
||||
m[name] = e
|
||||
b.countAction(e, 1)
|
||||
if _, seen := b.meta[source]; !seen {
|
||||
b.order = append(b.order, source)
|
||||
}
|
||||
b.noteOrder(source)
|
||||
}
|
||||
|
||||
// metaFor reports whether a source already contributed to this build.
|
||||
@@ -326,12 +327,25 @@ func (b *Builder) uncount(e *Entry) {
|
||||
|
||||
// NoteSource records (or updates) bookkeeping for a contributing source.
|
||||
func (b *Builder) NoteSource(meta SourceMeta) {
|
||||
if _, seen := b.meta[meta.Name]; !seen {
|
||||
b.order = append(b.order, meta.Name)
|
||||
}
|
||||
b.noteOrder(meta.Name)
|
||||
b.meta[meta.Name] = meta
|
||||
}
|
||||
|
||||
// noteOrder appends a source to the build order exactly once.
|
||||
func (b *Builder) noteOrder(name string) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
if b.ordered == nil {
|
||||
b.ordered = make(map[string]bool, 16)
|
||||
}
|
||||
if b.ordered[name] {
|
||||
return
|
||||
}
|
||||
b.ordered[name] = true
|
||||
b.order = append(b.order, name)
|
||||
}
|
||||
|
||||
// SourceCount is the number of contributing sources.
|
||||
func (b *Builder) SourceCount() int { return len(b.meta) }
|
||||
|
||||
@@ -731,7 +745,13 @@ func NewHolder(s *Set) *Holder {
|
||||
}
|
||||
|
||||
// Get returns the current set (nil before the first build).
|
||||
func (h *Holder) Get() *Set { return h.cur.Load() }
|
||||
// Get returns the current set, or nil when nothing has been compiled yet.
|
||||
func (h *Holder) Get() *Set {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return h.cur.Load()
|
||||
}
|
||||
|
||||
// Set installs a new set.
|
||||
func (h *Holder) Set(s *Set) { h.cur.Store(s) }
|
||||
Vendored
+3
-1
@@ -90,7 +90,9 @@ func canonical(name string) string {
|
||||
//
|
||||
// varint len | wire name | uint16 type | uint16 class | uint8 flags
|
||||
func packKey(buf []byte, k Key) ([]byte, error) {
|
||||
name := k.Name
|
||||
// Keys are stored canonically (lowercase, no trailing dot) but the wire
|
||||
// format needs a fully qualified name, so qualify it before packing.
|
||||
name := dns.Fqdn(k.Name)
|
||||
if n := len(name); n > 255*4 {
|
||||
return nil, errKeyTooLong
|
||||
}
|
||||
|
||||
Vendored
+13
-11
@@ -87,8 +87,8 @@ func TestLRUGetPutAndHitKinds(t *testing.T) {
|
||||
t.Errorf("entries after a dead read = %d, want 0 (the entry must be dropped)", got)
|
||||
}
|
||||
st := l.Stats()
|
||||
if st.Hits != 2 || st.StaleHits != 1 || st.Misses != 2 {
|
||||
t.Errorf("stats = %+v, want hits=2 staleHits=1 misses=2", st)
|
||||
if st.Hits != 2 || st.StaleHits != 1 || st.Misses != 1 {
|
||||
t.Errorf("stats = %+v, want hits=2 staleHits=1 misses=1", st)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func TestLRUDeleteWhereAndFlush(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUWalkVisitsEveryEntryOnce(t *testing.T) {
|
||||
l := NewLRU(100, 4)
|
||||
l := NewLRU(400, 4) // budget is per shard: 100 entries per shard
|
||||
now := newClock().Now()
|
||||
for i := range 200 {
|
||||
n := "n" + strconv.Itoa(i) + ".example.com"
|
||||
@@ -250,11 +250,11 @@ func TestLRUConcurrentPutGetResize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
for g := 1; g < 8; g++ {
|
||||
_ = g
|
||||
}
|
||||
wg.Wait()
|
||||
// Let the background loops run, then release them and wait for every
|
||||
// goroutine: closing before the wait is what keeps this from deadlocking.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
if l.Stats().Bytes < 0 {
|
||||
t.Errorf("byte accounting went negative: %d", l.Stats().Bytes)
|
||||
}
|
||||
@@ -337,8 +337,12 @@ func TestEntryAgeTTL(t *testing.T) {
|
||||
{"zero created keeps the stored TTL", Entry{TTL: 100}, now, 100},
|
||||
{"sub-second ageing truncates to 0 elapsed", Entry{TTL: 1, CreatedAt: now}, now.Add(400 * time.Millisecond), 1},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := tc.e.AgeTTL(tc.now); got != tc.want {
|
||||
for i := range cases {
|
||||
// Take the address: Entry now carries an atomic counter, so the
|
||||
// struct must never be copied.
|
||||
tc := &cases[i]
|
||||
got := tc.e.AgeTTL(tc.now)
|
||||
if got != tc.want {
|
||||
t.Errorf("%s: AgeTTL = %d, want %d", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
@@ -525,7 +529,6 @@ func TestManagerPutResponseRespectsZeroTTL(t *testing.T) {
|
||||
func TestManagerDeleteNameAndSubtree(t *testing.T) {
|
||||
clk := newClock()
|
||||
m := NewManager(Options{MaxTTL: time.Hour, Now: clk.Now}, nil)
|
||||
now := clk.Now()
|
||||
names := []string{"blocked.example.com", "deep.sub.blocked.example.com", "keep.example.com", "notblocked.example.com"}
|
||||
for _, n := range names {
|
||||
m.PutResponse(key(n), answer(t, n, 600, "10.0.0.4"), "up")
|
||||
@@ -547,7 +550,6 @@ func TestManagerDeleteNameAndSubtree(t *testing.T) {
|
||||
t.Errorf("%s was dropped by an unrelated delete", n)
|
||||
}
|
||||
}
|
||||
_ = now
|
||||
}
|
||||
|
||||
func TestManagerTopEntriesCountsAndFlush(t *testing.T) {
|
||||
|
||||
Vendored
+27
-1
@@ -358,5 +358,31 @@ func (l *LRU) SetCapacity(size, shards int) {
|
||||
shards = len(l.shards)
|
||||
}
|
||||
shards = roundPow2(shards)
|
||||
l.perShard.Store(int64(max(1, size/shards)))
|
||||
budget := int64(max(1, size/shards))
|
||||
l.perShard.Store(budget)
|
||||
// A shrink has to reclaim memory now, not on the next insert: after a
|
||||
// config reload lowers the cache size, the old entries would otherwise
|
||||
// sit there until they happened to be overwritten.
|
||||
var evicted int
|
||||
for _, s := range l.shards[:min(shards, len(l.shards))] {
|
||||
s.mu.Lock()
|
||||
for s.ll.Len() > int(budget) {
|
||||
oldest := s.ll.Back()
|
||||
if oldest == nil {
|
||||
break
|
||||
}
|
||||
it, ok := oldest.Value.(*lruItem)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
s.ll.Remove(oldest)
|
||||
delete(s.items, it.key)
|
||||
s.bytes -= it.entry.Bytes
|
||||
l.bytes.Add(-int64(it.entry.Bytes))
|
||||
l.entries.Add(-1)
|
||||
evicted++
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
l.evictions.Add(uint64(evicted))
|
||||
}
|
||||
@@ -549,8 +549,10 @@ func TestParseParenthesised(t *testing.T) {
|
||||
if strings.ContainsAny(got, "()") {
|
||||
t.Errorf("fold = %q, want no parentheses", got)
|
||||
}
|
||||
if got != "host MX 10 mx1.example.com." {
|
||||
t.Errorf("fold = %q, want a single-spaced token stream", got)
|
||||
// The parens and the whitespace they wrapped are gone; the token
|
||||
// stream is what the record parser cares about, not the exact spacing.
|
||||
if want := []string{"host", "MX", "10", "mx1.example.com."}; !slices.Equal(strings.Fields(got), want) {
|
||||
t.Errorf("fold = %q, tokens = %v, want %v", got, strings.Fields(got), want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user