@marcel91200 / caddy Mirror · read-only / commits / f1bc314f
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 03:30 UTC

reverseproxy: make the shared DNS discovery cache resolver-aware (#8252)

Co-authored-by: Zen Dodd <mail@steadytao.com>

tannevaled committed Oct 31, 2026 at 22:23 UTC f1bc314fbc5075e1c9b4672c4692e8e96d20dbbd
4 files changed +180 -39
modules/caddyhttp/reverseproxy/upstreams.go
+43 -3
@@ -7,6 +7,9 @@ import (
7 weakrand "math/rand/v2"
8 "net"
9 "net/http"
10 + "slices"
11 + "strconv"
12 + "strings"
13 "time"
14
15 "go.uber.org/zap"
@@ -76,6 +79,11 @@ type SRVUpstreams struct {
79
80 resolver *net.Resolver
81
82 + // resolverKey identifies resolver for the shared DNS cache, so two SRV
83 + // sources that look up the same record with different resolvers do not
84 + // share a cache entry. Empty for the default resolver.
85 + resolverKey string
86 +
87 logger *zap.Logger
88 }
89
@@ -114,6 +122,7 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error {
122 if su.resolver == nil {
123 su.resolver = net.DefaultResolver
124 }
125 + su.resolverKey = resolverCacheKey(su.Resolver)
126
127 return nil
128 }
@@ -124,7 +133,7 @@ func (su *SRVUpstreams) ResetCache(r *http.Request) error {
133 return nil
134 }
135 _, service, proto, name := su.expandedAddr(r)
127 - network.ResetSRV(service, proto, name)
136 + network.ResetSRV(su.resolverKey, service, proto, name)
137 return nil
138 }
139
@@ -132,7 +141,7 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
141 _, service, proto, name := su.expandedAddr(r)
142
143 targets, err := network.SRV(r.Context(), su.resolver.LookupSRV,
135 - service, proto, name,
144 + su.resolverKey, service, proto, name,
145 time.Duration(su.Refresh), time.Duration(su.GracePeriod), su.logger)
146 if err != nil {
147 return nil, err
@@ -231,6 +240,10 @@ type AUpstreams struct {
240
241 resolver *net.Resolver
242
243 + // resolverKey identifies resolver for the shared DNS cache; see the same
244 + // field on SRVUpstreams. Empty for the default resolver.
245 + resolverKey string
246 +
247 logger *zap.Logger
248 }
249
@@ -272,6 +285,7 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error {
285 if au.resolver == nil {
286 au.resolver = net.DefaultResolver
287 }
288 + au.resolverKey = resolverCacheKey(au.Resolver)
289
290 return nil
291 }
@@ -284,7 +298,7 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
298 port := repl.ReplaceAll(au.Port, "")
299
300 targets, err := network.A(r.Context(), au.resolver.LookupIP,
287 - ipVersion, name, port, time.Duration(au.Refresh), au.logger)
301 + au.resolverKey, ipVersion, name, port, time.Duration(au.Refresh), au.logger)
302 if err != nil {
303 return nil, err
304 }
@@ -405,6 +419,32 @@ func (u *UpstreamResolver) ParseAddresses() error {
419 return nil
420 }
421
422 +// resolverCacheKey returns a stable identity for the configured resolver, used
423 +// to key the shared DNS cache so that two upstream sources looking up the same
424 +// name with different resolvers do not share a cache entry. The default
425 +// resolver (no Resolver configured) yields "". Addresses are sorted so that the
426 +// same set in a different order maps to the same key.
427 +//
428 +// Each address is length-prefixed ("<len>:<addr>") rather than joined with a
429 +// separator, so the encoding is unambiguous even when an address itself
430 +// contains the separator — e.g. a Unix-socket resolver path with a comma.
431 +// Without this, {"a,b"} and {"a", "b"} would collide, defeating the very
432 +// isolation this key provides.
433 +func resolverCacheKey(r *UpstreamResolver) string {
434 + if r == nil || len(r.Addresses) == 0 {
435 + return ""
436 + }
437 + addrs := slices.Clone(r.Addresses)
438 + slices.Sort(addrs)
439 + var b strings.Builder
440 + for _, a := range addrs {
441 + b.WriteString(strconv.Itoa(len(a)))
442 + b.WriteByte(':')
443 + b.WriteString(a)
444 + }
445 + return b.String()
446 +}
447 +
448 // Interface guards
449 var (
450 _ caddy.Provisioner = (*SRVUpstreams)(nil)
modules/caddyhttp/reverseproxy/upstreams_test.go
+37
@@ -53,3 +53,40 @@ func TestResolveIpVersion(t *testing.T) {
53 }
54 }
55 }
56 +
57 +func TestResolverCacheKey(t *testing.T) {
58 + key := func(addrs ...string) string {
59 + if addrs == nil {
60 + return resolverCacheKey(nil)
61 + }
62 + return resolverCacheKey(&UpstreamResolver{Addresses: addrs})
63 + }
64 +
65 + // The default resolver (nil or empty) yields the empty key, so default-resolver
66 + // sources keep sharing the cache.
67 + if got := key(); got != "" {
68 + t.Errorf("nil resolver: got %q, want \"\"", got)
69 + }
70 + if got := resolverCacheKey(&UpstreamResolver{}); got != "" {
71 + t.Errorf("empty resolver: got %q, want \"\"", got)
72 + }
73 +
74 + // Order does not matter: the same set maps to the same key.
75 + if a, b := key("1.1.1.1", "8.8.8.8"), key("8.8.8.8", "1.1.1.1"); a != b {
76 + t.Errorf("order should not matter: %q != %q", a, b)
77 + }
78 +
79 + // Different sets must not collide, even when an address contains the
80 + // delimiter that a naive join would use. {"a,b"} and {"a","b"} are distinct
81 + // resolver configurations and must get distinct keys.
82 + colliding := [][2][]string{
83 + {{"a,b"}, {"a", "b"}},
84 + {{"unix//run/x,y.sock"}, {"unix//run/x", "y.sock"}},
85 + {{"10:1"}, {"1", "0:1"}}, // guards the length-prefix encoding itself
86 + }
87 + for _, c := range colliding {
88 + if k1, k2 := key(c[0]...), key(c[1]...); k1 == k2 {
89 + t.Errorf("distinct address sets %q and %q share key %q", c[0], c[1], k1)
90 + }
91 + }
92 +}
modules/internal/network/dynamicupstreams.go
+16 -10
@@ -49,6 +49,9 @@ type SRVLookupFunc func(ctx context.Context, service, proto, name string) (cname
49
50 // SRV resolves the given SRV record into targets, caching the result for
51 // refresh. lookup is the resolver to use (typically (*net.Resolver).LookupSRV).
52 +// resolverKey identifies that resolver, so results looked up with different
53 +// resolvers for the same record are cached separately; pass "" for the default
54 +// resolver.
55 //
56 // If the lookup fails but returns some records (e.g. a few invalid names were
57 // filtered out), those records are still used. If it fails with no records and
@@ -56,8 +59,8 @@ type SRVLookupFunc func(ctx context.Context, service, proto, name string) (cname
59 // past its refresh instead of returning an error. With no cached result, grace
60 // also suppresses the error and returns no targets, preserving the reverse
61 // proxy's existing behavior.
59 -func SRV(ctx context.Context, lookup SRVLookupFunc, service, proto, name string, refresh, grace time.Duration, logger *zap.Logger) ([]Target, error) {
60 - key := srvKey(service, proto, name)
62 +func SRV(ctx context.Context, lookup SRVLookupFunc, resolverKey, service, proto, name string, refresh, grace time.Duration, logger *zap.Logger) ([]Target, error) {
63 + key := srvKey(resolverKey, service, proto, name)
64
65 // fast path: a fresh cached result under a read lock
66 srvMu.RLock()
@@ -126,10 +129,11 @@ func SRV(ctx context.Context, lookup SRVLookupFunc, service, proto, name string,
129 return targets, nil
130 }
131
129 -// ResetSRV removes the cached result for a single SRV record.
130 -func ResetSRV(service, proto, name string) {
132 +// ResetSRV removes the cached result for a single SRV record resolved with the
133 +// given resolver (see [SRV] for resolverKey).
134 +func ResetSRV(resolverKey, service, proto, name string) {
135 srvMu.Lock()
132 - delete(srvCache, srvKey(service, proto, name))
136 + delete(srvCache, srvKey(resolverKey, service, proto, name))
137 srvMu.Unlock()
138 }
139
@@ -140,8 +144,8 @@ func ResetAllSRV() {
144 srvMu.Unlock()
145 }
146
143 -func srvKey(service, proto, name string) string {
144 - return service + "\x00" + proto + "\x00" + name
147 +func srvKey(resolverKey, service, proto, name string) string {
148 + return resolverKey + "\x00" + service + "\x00" + proto + "\x00" + name
149 }
150
151 const maxCacheEntries = 100
@@ -164,9 +168,11 @@ type IPLookupFunc func(ctx context.Context, network, host string) ([]net.IP, err
168 // A resolves name's A/AAAA records into targets (one per address, all using the
169 // given port), caching the result for refresh. network selects the IP versions
170 // ("ip", "ip4" or "ip6"). lookup is the resolver to use (typically
167 -// (*net.Resolver).LookupIP).
168 -func A(ctx context.Context, lookup IPLookupFunc, network, name, port string, refresh time.Duration, logger *zap.Logger) ([]Target, error) {
169 - key := name + "\x00" + port + "\x00" + network
171 +// (*net.Resolver).LookupIP). resolverKey identifies that resolver, so results
172 +// looked up with different resolvers for the same name are cached separately;
173 +// pass "" for the default resolver.
174 +func A(ctx context.Context, lookup IPLookupFunc, resolverKey, network, name, port string, refresh time.Duration, logger *zap.Logger) ([]Target, error) {
175 + key := resolverKey + "\x00" + name + "\x00" + port + "\x00" + network
176
177 aMu.RLock()
178 cached := aCache[key]
modules/internal/network/dynamicupstreams_test.go
+84 -26
@@ -38,7 +38,7 @@ func TestSRVResolvesAndCaches(t *testing.T) {
38 }, nil
39 }
40
41 - targets, err := SRV(context.Background(), lookup, "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop())
41 + targets, err := SRV(context.Background(), lookup, "", "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop())
42 if err != nil {
43 t.Fatalf("SRV: %v", err)
44 }
@@ -50,7 +50,7 @@ func TestSRVResolvesAndCaches(t *testing.T) {
50 }
51
52 // second call within refresh must be served from cache (no extra lookup)
53 - if _, err := SRV(context.Background(), lookup, "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
53 + if _, err := SRV(context.Background(), lookup, "", "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
54 t.Fatalf("SRV (cached): %v", err)
55 }
56 if calls != 1 {
@@ -62,7 +62,7 @@ func TestSRVErrorWithoutGrace(t *testing.T) {
62 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
63 return "", nil, errors.New("dns boom")
64 }
65 - if _, err := SRV(context.Background(), lookup, "svc-err", "tcp", "x", time.Minute, 0, zap.NewNop()); err == nil {
65 + if _, err := SRV(context.Background(), lookup, "", "svc-err", "tcp", "x", time.Minute, 0, zap.NewNop()); err == nil {
66 t.Fatal("expected an error when lookup fails and nothing is cached")
67 }
68 }
@@ -72,7 +72,7 @@ func TestSRVGracePeriodServesStale(t *testing.T) {
72 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
73 }
74 // tiny refresh so the entry is immediately stale on the next call
75 - if _, err := SRV(context.Background(), ok, "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zap.NewNop()); err != nil {
75 + if _, err := SRV(context.Background(), ok, "", "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zap.NewNop()); err != nil {
76 t.Fatalf("seeding cache: %v", err)
77 }
78
@@ -80,7 +80,7 @@ func TestSRVGracePeriodServesStale(t *testing.T) {
80 return "", nil, errors.New("dns boom")
81 }
82 // a level-enabled logger so the "using previously cached" error log fires
83 - targets, err := SRV(context.Background(), fail, "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zaptest.NewLogger(t))
83 + targets, err := SRV(context.Background(), fail, "", "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zaptest.NewLogger(t))
84 if err != nil {
85 t.Fatalf("grace period should suppress the error: %v", err)
86 }
@@ -95,11 +95,11 @@ func TestResetSRV(t *testing.T) {
95 calls++
96 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
97 }
98 - if _, err := SRV(context.Background(), lookup, "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
98 + if _, err := SRV(context.Background(), lookup, "", "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
99 t.Fatal(err)
100 }
101 - ResetSRV("svc-reset", "tcp", "x")
102 - if _, err := SRV(context.Background(), lookup, "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
101 + ResetSRV("", "svc-reset", "tcp", "x")
102 + if _, err := SRV(context.Background(), lookup, "", "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
103 t.Fatal(err)
104 }
105 if calls != 2 {
@@ -114,7 +114,7 @@ func TestAResolvesAndCaches(t *testing.T) {
114 return []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2")}, nil
115 }
116
117 - targets, err := A(context.Background(), lookup, "ip", "db.a-test", "5432", time.Minute, zap.NewNop())
117 + targets, err := A(context.Background(), lookup, "", "ip", "db.a-test", "5432", time.Minute, zap.NewNop())
118 if err != nil {
119 t.Fatalf("A: %v", err)
120 }
@@ -125,7 +125,7 @@ func TestAResolvesAndCaches(t *testing.T) {
125 t.Errorf("unexpected first target: %+v", targets[0])
126 }
127
128 - if _, err := A(context.Background(), lookup, "ip", "db.a-test", "5432", time.Minute, zap.NewNop()); err != nil {
128 + if _, err := A(context.Background(), lookup, "", "ip", "db.a-test", "5432", time.Minute, zap.NewNop()); err != nil {
129 t.Fatalf("A (cached): %v", err)
130 }
131 if calls != 1 {
@@ -137,7 +137,7 @@ func TestAError(t *testing.T) {
137 lookup := func(context.Context, string, string) ([]net.IP, error) {
138 return nil, errors.New("dns boom")
139 }
140 - if _, err := A(context.Background(), lookup, "ip", "db.a-err", "5432", time.Minute, zap.NewNop()); err == nil {
140 + if _, err := A(context.Background(), lookup, "", "ip", "db.a-err", "5432", time.Minute, zap.NewNop()); err == nil {
141 t.Fatal("expected an error when the A lookup fails")
142 }
143 }
@@ -151,7 +151,7 @@ func TestSRVFilteredRecords(t *testing.T) {
151 errors.New("some SRV names were filtered out")
152 }
153 // a level-enabled logger so the "SRV records filtered" warning fires
154 - targets, err := SRV(context.Background(), lookup, "svc-filtered", "tcp", "x", time.Minute, 0, zaptest.NewLogger(t))
154 + targets, err := SRV(context.Background(), lookup, "", "svc-filtered", "tcp", "x", time.Minute, 0, zaptest.NewLogger(t))
155 if err != nil {
156 t.Fatalf("usable records must be returned despite the partial error: %v", err)
157 }
@@ -165,7 +165,7 @@ func TestResetAllSRV(t *testing.T) {
165 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
166 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
167 }
168 - if _, err := SRV(context.Background(), lookup, "svc-reset-all", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
168 + if _, err := SRV(context.Background(), lookup, "", "svc-reset-all", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
169 t.Fatalf("SRV: %v", err)
170 }
171 srvMu.RLock()
@@ -197,7 +197,7 @@ func TestSRVCacheBound(t *testing.T) {
197 }
198 for i := 0; i < maxCacheEntries+5; i++ {
199 name := fmt.Sprintf("svc-bound-%d", i)
200 - if _, err := SRV(context.Background(), lookup, name, "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
200 + if _, err := SRV(context.Background(), lookup, "", name, "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
201 t.Fatalf("SRV[%d]: %v", i, err)
202 }
203 }
@@ -209,6 +209,64 @@ func TestSRVCacheBound(t *testing.T) {
209 }
210 }
211
212 +// TestSRVResolverKeyIsolatesCache verifies that two lookups for the same SRV
213 +// record but with different resolver keys do not share a cache entry.
214 +func TestSRVResolverKeyIsolatesCache(t *testing.T) {
215 + ResetAllSRV()
216 + t.Cleanup(ResetAllSRV)
217 +
218 + lookupA := func(context.Context, string, string, string) (string, []*net.SRV, error) {
219 + return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
220 + }
221 + lookupB := func(context.Context, string, string, string) (string, []*net.SRV, error) {
222 + return "", []*net.SRV{{Target: "b.example.", Port: 2}}, nil
223 + }
224 +
225 + ta, err := SRV(context.Background(), lookupA, "resolver-a", "svc", "tcp", "x", time.Minute, 0, zap.NewNop())
226 + if err != nil {
227 + t.Fatalf("resolver-a: %v", err)
228 + }
229 + tb, err := SRV(context.Background(), lookupB, "resolver-b", "svc", "tcp", "x", time.Minute, 0, zap.NewNop())
230 + if err != nil {
231 + t.Fatalf("resolver-b: %v", err)
232 + }
233 + if len(ta) != 1 || ta[0].Host != "a.example." {
234 + t.Fatalf("resolver-a targets = %+v, want a.example.", ta)
235 + }
236 + if len(tb) != 1 || tb[0].Host != "b.example." {
237 + t.Fatalf("resolver-b targets = %+v, want b.example. (resolver-a's entry leaked across resolvers)", tb)
238 + }
239 +}
240 +
241 +// TestAResolverKeyIsolatesCache is the A-cache equivalent of the above.
242 +func TestAResolverKeyIsolatesCache(t *testing.T) {
243 + aMu.Lock()
244 + aCache = make(map[string]cacheEntry)
245 + aMu.Unlock()
246 +
247 + lookupA := func(context.Context, string, string) ([]net.IP, error) {
248 + return []net.IP{net.ParseIP("10.0.0.1")}, nil
249 + }
250 + lookupB := func(context.Context, string, string) ([]net.IP, error) {
251 + return []net.IP{net.ParseIP("10.0.0.2")}, nil
252 + }
253 +
254 + ta, err := A(context.Background(), lookupA, "resolver-a", "ip", "db.iso", "5432", time.Minute, zap.NewNop())
255 + if err != nil {
256 + t.Fatalf("resolver-a: %v", err)
257 + }
258 + tb, err := A(context.Background(), lookupB, "resolver-b", "ip", "db.iso", "5432", time.Minute, zap.NewNop())
259 + if err != nil {
260 + t.Fatalf("resolver-b: %v", err)
261 + }
262 + if len(ta) != 1 || ta[0].Host != "10.0.0.1" {
263 + t.Fatalf("resolver-a targets = %+v, want 10.0.0.1", ta)
264 + }
265 + if len(tb) != 1 || tb[0].Host != "10.0.0.2" {
266 + t.Fatalf("resolver-b targets = %+v, want 10.0.0.2 (resolver-a's entry leaked across resolvers)", tb)
267 + }
268 +}
269 +
270 // TestACacheBound verifies the same bounding behavior for the A cache.
271 func TestACacheBound(t *testing.T) {
272 aMu.Lock()
@@ -220,7 +278,7 @@ func TestACacheBound(t *testing.T) {
278 }
279 for i := 0; i < maxCacheEntries+5; i++ {
280 name := fmt.Sprintf("db.bound-%d", i)
223 - if _, err := A(context.Background(), lookup, "ip", name, "5432", time.Minute, zap.NewNop()); err != nil {
281 + if _, err := A(context.Background(), lookup, "", "ip", name, "5432", time.Minute, zap.NewNop()); err != nil {
282 t.Fatalf("A[%d]: %v", i, err)
283 }
284 }
@@ -264,7 +322,7 @@ func TestSRVConcurrentRefreshDeduplicates(t *testing.T) {
322 done := make(chan struct{}, 2)
323 // G1: takes the write lock and blocks inside lookup.
324 go func() {
267 - _, _ = SRV(context.Background(), lookup, "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
325 + _, _ = SRV(context.Background(), lookup, "", "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
326 done <- struct{}{}
327 }()
328 <-inLookup // G1 now holds the write lock; cache is still empty
@@ -275,7 +333,7 @@ func TestSRVConcurrentRefreshDeduplicates(t *testing.T) {
333 // performs no lookup. Ordering here is enforced by the channels below, not
334 // by timing.
335 go func() {
278 - _, _ = SRV(context.Background(), lookup, "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
336 + _, _ = SRV(context.Background(), lookup, "", "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
337 done <- struct{}{}
338 }()
339
@@ -317,7 +375,7 @@ func TestAConcurrentRefreshDeduplicates(t *testing.T) {
375
376 done := make(chan struct{}, 2)
377 go func() {
320 - _, _ = A(context.Background(), lookup, "ip", "db.conc", "5432", time.Minute, zap.NewNop())
378 + _, _ = A(context.Background(), lookup, "", "ip", "db.conc", "5432", time.Minute, zap.NewNop())
379 done <- struct{}{}
380 }()
381 <-inLookup
@@ -325,7 +383,7 @@ func TestAConcurrentRefreshDeduplicates(t *testing.T) {
383 // Second caller for the same key; ordering is enforced by the channels,
384 // not by timing (see the SRV test above).
385 go func() {
328 - _, _ = A(context.Background(), lookup, "ip", "db.conc", "5432", time.Minute, zap.NewNop())
386 + _, _ = A(context.Background(), lookup, "", "ip", "db.conc", "5432", time.Minute, zap.NewNop())
387 done <- struct{}{}
388 }()
389
@@ -342,12 +400,12 @@ func TestAConcurrentRefreshDeduplicates(t *testing.T) {
400 }
401
402 func TestSRVGracePeriodWithoutCachedResult(t *testing.T) {
345 - ResetSRV("svc-cold-grace", "tcp", "x")
346 - t.Cleanup(func() { ResetSRV("svc-cold-grace", "tcp", "x") })
403 + ResetSRV("", "svc-cold-grace", "tcp", "x")
404 + t.Cleanup(func() { ResetSRV("", "svc-cold-grace", "tcp", "x") })
405 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
406 return "", nil, errors.New("dns boom")
407 }
350 - targets, err := SRV(context.Background(), lookup, "svc-cold-grace", "tcp", "x", time.Minute, time.Minute, zap.NewNop())
408 + targets, err := SRV(context.Background(), lookup, "", "svc-cold-grace", "tcp", "x", time.Minute, time.Minute, zap.NewNop())
409 if err != nil || len(targets) != 0 {
410 t.Fatalf("cold-cache failure with grace: targets = %v, err = %v; want no targets and no error", targets, err)
411 }
@@ -355,15 +413,15 @@ func TestSRVGracePeriodWithoutCachedResult(t *testing.T) {
413
414 func TestDiscoveryLoggingOnlyOnRefresh(t *testing.T) {
415 t.Run("SRV", func(t *testing.T) {
358 - ResetSRV("svc-log", "tcp", "x")
359 - t.Cleanup(func() { ResetSRV("svc-log", "tcp", "x") })
416 + ResetSRV("", "svc-log", "tcp", "x")
417 + t.Cleanup(func() { ResetSRV("", "svc-log", "tcp", "x") })
418 core, logs := observer.New(zap.DebugLevel)
419 logger := zap.New(core)
420 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
421 return "", []*net.SRV{{Target: "a.example.", Port: 5432, Priority: 1, Weight: 10}}, nil
422 }
423 for range 2 {
366 - if _, err := SRV(context.Background(), lookup, "svc-log", "tcp", "x", time.Minute, 0, logger); err != nil {
424 + if _, err := SRV(context.Background(), lookup, "", "svc-log", "tcp", "x", time.Minute, 0, logger); err != nil {
425 t.Fatal(err)
426 }
427 }
@@ -389,7 +447,7 @@ func TestDiscoveryLoggingOnlyOnRefresh(t *testing.T) {
447 return []net.IP{net.ParseIP("10.0.0.1")}, nil
448 }
449 for range 2 {
392 - if _, err := A(context.Background(), lookup, "ip", "db.log", "5432", time.Minute, logger); err != nil {
450 + if _, err := A(context.Background(), lookup, "", "ip", "db.log", "5432", time.Minute, logger); err != nil {
451 t.Fatal(err)
452 }
453 }