client/dns/mgmt: singleflight on-demand resolves

resolveOnDemand deduplicated the cache write but not the upstream
lookup — a burst of concurrent queries for a freshly-learned pool-root
subdomain (A + AAAA from a single getaddrinfo, or multiple peer
workers dialing the same foreign relay) each hit the bypass resolver
independently.

Wrap the lookupRecords call in refreshGroup.Do with an 'ondemand:'-
prefixed key so concurrent callers collapse into one request; the
prefix namespaces this key off scheduleRefresh's keyspace so the two
paths don't collide.

Error and empty-result paths preserved; the cache write still runs
under the existing TOCTOU-safe exists check + mutex so a winning
refresh isn't clobbered.
This commit is contained in:
Zoltán Papp
2026-04-24 22:36:18 +02:00
parent c89c30bb28
commit b0b52b6774

View File

@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -232,16 +233,26 @@ func (m *Resolver) resolveOnDemand(w dns.ResponseWriter, r *dns.Msg, question dn
return
}
ctx, cancel := context.WithTimeout(context.Background(), dnsTimeout)
defer cancel()
records, err := m.lookupRecords(ctx, d, question)
// Collapse concurrent on-demand lookups for the same (name, qtype) into
// a single upstream query via singleflight. A burst of parallel queries
// for a freshly-learned pool-root subdomain (e.g. multiple peer workers
// dialing the same foreign relay, or A + AAAA racing each other) would
// otherwise each hit the bypass resolver independently. The prefix
// namespaces this key off scheduleRefresh's keyspace so the two paths
// can coexist without collisions.
key := "ondemand:" + question.Name + ":" + strconv.Itoa(int(question.Qtype))
result, err, _ := m.refreshGroup.Do(key, func() (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), dnsTimeout)
defer cancel()
return m.lookupRecords(ctx, d, question)
})
if err != nil {
log.Debugf("on-demand resolve %s type=%s: %v",
d.SafeString(), dns.TypeToString[question.Qtype], err)
m.continueToNext(w, r)
return
}
records, _ := result.([]dns.RR)
if len(records) == 0 {
m.continueToNext(w, r)
return