@marcel91200 / caddy Mirror · read-only
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, 01:00 UTC

master
go 721 lines 24.5 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package network
16
17 import (
18 "context"
19 "errors"
20 "fmt"
21 "net"
22 "sync"
23 "sync/atomic"
24 "testing"
25 "time"
26
27 "go.uber.org/zap"
28 "go.uber.org/zap/zaptest"
29 "go.uber.org/zap/zaptest/observer"
30 )
31
32 func TestSRVResolvesAndCaches(t *testing.T) {
33 calls := 0
34 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
35 calls++
36 return "", []*net.SRV{
37 {Target: "a.example.", Port: 5432, Priority: 1, Weight: 10},
38 {Target: "b.example.", Port: 5433, Priority: 1, Weight: 20},
39 }, nil
40 }
41
42 targets, err := SRV(context.Background(), lookup, "", "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop())
43 if err != nil {
44 t.Fatalf("SRV: %v", err)
45 }
46 if len(targets) != 2 {
47 t.Fatalf("targets = %d, want 2", len(targets))
48 }
49 if targets[0].Host != "a.example." || targets[0].Port != "5432" || targets[0].Weight != 10 {
50 t.Errorf("unexpected first target: %+v", targets[0])
51 }
52
53 // second call within refresh must be served from cache (no extra lookup)
54 if _, err := SRV(context.Background(), lookup, "", "svc-cache", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
55 t.Fatalf("SRV (cached): %v", err)
56 }
57 if calls != 1 {
58 t.Errorf("lookup calls = %d, want 1 (cached)", calls)
59 }
60 }
61
62 func TestSRVErrorWithoutGrace(t *testing.T) {
63 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
64 return "", nil, errors.New("dns boom")
65 }
66 if _, err := SRV(context.Background(), lookup, "", "svc-err", "tcp", "x", time.Minute, 0, zap.NewNop()); err == nil {
67 t.Fatal("expected an error when lookup fails and nothing is cached")
68 }
69 }
70
71 func TestSRVGracePeriodServesStale(t *testing.T) {
72 ok := func(context.Context, string, string, string) (string, []*net.SRV, error) {
73 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
74 }
75 // tiny refresh so the entry is immediately stale on the next call
76 if _, err := SRV(context.Background(), ok, "", "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zap.NewNop()); err != nil {
77 t.Fatalf("seeding cache: %v", err)
78 }
79
80 fail := func(context.Context, string, string, string) (string, []*net.SRV, error) {
81 return "", nil, errors.New("dns boom")
82 }
83 // a level-enabled logger so the "using previously cached" error log fires
84 targets, err := SRV(context.Background(), fail, "", "svc-grace", "tcp", "x", time.Nanosecond, time.Hour, zaptest.NewLogger(t))
85 if err != nil {
86 t.Fatalf("grace period should suppress the error: %v", err)
87 }
88 if len(targets) != 1 {
89 t.Errorf("expected the stale cached target to be served, got %d", len(targets))
90 }
91 }
92
93 func TestResetSRV(t *testing.T) {
94 calls := 0
95 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
96 calls++
97 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
98 }
99 if _, err := SRV(context.Background(), lookup, "", "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
100 t.Fatal(err)
101 }
102 ResetSRV("", "svc-reset", "tcp", "x")
103 if _, err := SRV(context.Background(), lookup, "", "svc-reset", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
104 t.Fatal(err)
105 }
106 if calls != 2 {
107 t.Errorf("lookup calls = %d, want 2 (cache was reset between calls)", calls)
108 }
109 }
110
111 func TestAResolvesAndCaches(t *testing.T) {
112 calls := 0
113 lookup := func(context.Context, string, string) ([]net.IP, error) {
114 calls++
115 return []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2")}, nil
116 }
117
118 targets, err := A(context.Background(), lookup, "", "ip", "db.a-test", "5432", time.Minute, zap.NewNop())
119 if err != nil {
120 t.Fatalf("A: %v", err)
121 }
122 if len(targets) != 2 {
123 t.Fatalf("targets = %d, want 2", len(targets))
124 }
125 if targets[0].Host != "10.0.0.1" || targets[0].Port != "5432" {
126 t.Errorf("unexpected first target: %+v", targets[0])
127 }
128
129 if _, err := A(context.Background(), lookup, "", "ip", "db.a-test", "5432", time.Minute, zap.NewNop()); err != nil {
130 t.Fatalf("A (cached): %v", err)
131 }
132 if calls != 1 {
133 t.Errorf("lookup calls = %d, want 1 (cached)", calls)
134 }
135 }
136
137 func TestAError(t *testing.T) {
138 lookup := func(context.Context, string, string) ([]net.IP, error) {
139 return nil, errors.New("dns boom")
140 }
141 if _, err := A(context.Background(), lookup, "", "ip", "db.a-err", "5432", time.Minute, zap.NewNop()); err == nil {
142 t.Fatal("expected an error when the A lookup fails")
143 }
144 }
145
146 // TestSRVFilteredRecords covers the LookupSRV semantics where invalid names are
147 // filtered out and an error is returned alongside the usable remainder: the
148 // usable records must still be returned (the error is downgraded to a warning).
149 func TestSRVFilteredRecords(t *testing.T) {
150 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
151 return "", []*net.SRV{{Target: "ok.example.", Port: 5432, Priority: 1, Weight: 10}},
152 errors.New("some SRV names were filtered out")
153 }
154 // a level-enabled logger so the "SRV records filtered" warning fires
155 targets, err := SRV(context.Background(), lookup, "", "svc-filtered", "tcp", "x", time.Minute, 0, zaptest.NewLogger(t))
156 if err != nil {
157 t.Fatalf("usable records must be returned despite the partial error: %v", err)
158 }
159 if len(targets) != 1 || targets[0].Host != "ok.example." {
160 t.Fatalf("unexpected targets: %+v", targets)
161 }
162 }
163
164 // TestResetAllSRV verifies that the whole SRV cache is dropped.
165 func TestResetAllSRV(t *testing.T) {
166 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
167 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
168 }
169 if _, err := SRV(context.Background(), lookup, "", "svc-reset-all", "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
170 t.Fatalf("SRV: %v", err)
171 }
172 srvCache.mu.RLock()
173 populated := len(srvCache.entries) > 0
174 srvCache.mu.RUnlock()
175 if !populated {
176 t.Fatal("expected the SRV cache to be populated before reset")
177 }
178
179 ResetAllSRV()
180
181 srvCache.mu.RLock()
182 n := len(srvCache.entries)
183 srvCache.mu.RUnlock()
184 if n != 0 {
185 t.Fatalf("srvCache len = %d after ResetAllSRV, want 0", n)
186 }
187 }
188
189 // TestSRVCacheBound verifies that inserting a brand-new entry once the cache is
190 // full evicts an existing one so the cache stays bounded.
191 func TestSRVCacheBound(t *testing.T) {
192 ResetAllSRV()
193
194 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
195 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
196 }
197 for i := 0; i < maxCacheEntries+5; i++ {
198 name := fmt.Sprintf("svc-bound-%d", i)
199 if _, err := SRV(context.Background(), lookup, "", name, "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
200 t.Fatalf("SRV[%d]: %v", i, err)
201 }
202 }
203 srvCache.mu.RLock()
204 n := len(srvCache.entries)
205 srvCache.mu.RUnlock()
206 if n > maxCacheEntries {
207 t.Fatalf("srvCache len = %d, want <= %d (cache must stay bounded)", n, maxCacheEntries)
208 }
209 }
210
211 // TestSRVResolverKeyIsolatesCache verifies that two lookups for the same SRV
212 // record but with different resolver keys do not share a cache entry.
213 func TestSRVResolverKeyIsolatesCache(t *testing.T) {
214 ResetAllSRV()
215 t.Cleanup(ResetAllSRV)
216
217 lookupA := func(context.Context, string, string, string) (string, []*net.SRV, error) {
218 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
219 }
220 lookupB := func(context.Context, string, string, string) (string, []*net.SRV, error) {
221 return "", []*net.SRV{{Target: "b.example.", Port: 2}}, nil
222 }
223
224 ta, err := SRV(context.Background(), lookupA, "resolver-a", "svc", "tcp", "x", time.Minute, 0, zap.NewNop())
225 if err != nil {
226 t.Fatalf("resolver-a: %v", err)
227 }
228 tb, err := SRV(context.Background(), lookupB, "resolver-b", "svc", "tcp", "x", time.Minute, 0, zap.NewNop())
229 if err != nil {
230 t.Fatalf("resolver-b: %v", err)
231 }
232 if len(ta) != 1 || ta[0].Host != "a.example." {
233 t.Fatalf("resolver-a targets = %+v, want a.example.", ta)
234 }
235 if len(tb) != 1 || tb[0].Host != "b.example." {
236 t.Fatalf("resolver-b targets = %+v, want b.example. (resolver-a's entry leaked across resolvers)", tb)
237 }
238 }
239
240 // TestAResolverKeyIsolatesCache is the A-cache equivalent of the above.
241 func TestAResolverKeyIsolatesCache(t *testing.T) {
242 ResetAllA()
243
244 lookupA := func(context.Context, string, string) ([]net.IP, error) {
245 return []net.IP{net.ParseIP("10.0.0.1")}, nil
246 }
247 lookupB := func(context.Context, string, string) ([]net.IP, error) {
248 return []net.IP{net.ParseIP("10.0.0.2")}, nil
249 }
250
251 ta, err := A(context.Background(), lookupA, "resolver-a", "ip", "db.iso", "5432", time.Minute, zap.NewNop())
252 if err != nil {
253 t.Fatalf("resolver-a: %v", err)
254 }
255 tb, err := A(context.Background(), lookupB, "resolver-b", "ip", "db.iso", "5432", time.Minute, zap.NewNop())
256 if err != nil {
257 t.Fatalf("resolver-b: %v", err)
258 }
259 if len(ta) != 1 || ta[0].Host != "10.0.0.1" {
260 t.Fatalf("resolver-a targets = %+v, want 10.0.0.1", ta)
261 }
262 if len(tb) != 1 || tb[0].Host != "10.0.0.2" {
263 t.Fatalf("resolver-b targets = %+v, want 10.0.0.2 (resolver-a's entry leaked across resolvers)", tb)
264 }
265 }
266
267 // TestACacheBound verifies the same bounding behavior for the A cache.
268 func TestACacheBound(t *testing.T) {
269 ResetAllA()
270
271 lookup := func(context.Context, string, string) ([]net.IP, error) {
272 return []net.IP{net.ParseIP("10.0.0.1")}, nil
273 }
274 for i := 0; i < maxCacheEntries+5; i++ {
275 name := fmt.Sprintf("db.bound-%d", i)
276 if _, err := A(context.Background(), lookup, "", "ip", name, "5432", time.Minute, zap.NewNop()); err != nil {
277 t.Fatalf("A[%d]: %v", i, err)
278 }
279 }
280 aCache.mu.RLock()
281 n := len(aCache.entries)
282 aCache.mu.RUnlock()
283 if n > maxCacheEntries {
284 t.Fatalf("aCache len = %d, want <= %d (cache must stay bounded)", n, maxCacheEntries)
285 }
286 }
287
288 // TestSRVConcurrentRefreshDeduplicates verifies that two concurrent callers
289 // missing the cache for the same key result in a single lookup. Depending on
290 // scheduling the second caller either joins the first one's in-flight lookup
291 // or is served the result it cached; this test does not force one over the
292 // other, it asserts deduplication (exactly one lookup). Run under -race it also guards against
293 // data races on the shared cache.
294 func TestSRVConcurrentRefreshDeduplicates(t *testing.T) {
295 ResetAllSRV()
296
297 var calls int
298 var mu sync.Mutex
299 inLookup := make(chan struct{})
300 release := make(chan struct{})
301 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
302 mu.Lock()
303 calls++
304 first := calls == 1
305 mu.Unlock()
306 if first {
307 close(inLookup)
308 <-release // hold the lookup open while the second goroutine starts
309 }
310 return "", []*net.SRV{{Target: "a.example.", Port: 1}}, nil
311 }
312
313 done := make(chan struct{}, 2)
314 // G1: starts the lookup and blocks inside it.
315 go func() {
316 _, _ = SRV(context.Background(), lookup, "", "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
317 done <- struct{}{}
318 }()
319 <-inLookup // G1 is mid-lookup; cache is still empty
320
321 // G2: a second caller for the same key; it must not start a lookup of its
322 // own.
323 go func() {
324 _, _ = SRV(context.Background(), lookup, "", "svc-conc", "tcp", "x", time.Minute, 0, zap.NewNop())
325 done <- struct{}{}
326 }()
327
328 close(release) // G1 populates the cache and releases the lock
329 <-done
330 <-done
331
332 mu.Lock()
333 got := calls
334 mu.Unlock()
335 if got != 1 {
336 t.Fatalf("lookup calls = %d, want 1 (concurrent callers for the same key must share one lookup)", got)
337 }
338 }
339
340 // TestAConcurrentRefreshDeduplicates is the A-cache equivalent of the above:
341 // it asserts deduplication (one lookup for two concurrent callers) and, under
342 // -race, race-safety; it does not force a particular locking branch.
343 func TestAConcurrentRefreshDeduplicates(t *testing.T) {
344 ResetAllA()
345
346 var calls int
347 var mu sync.Mutex
348 inLookup := make(chan struct{})
349 release := make(chan struct{})
350 lookup := func(context.Context, string, string) ([]net.IP, error) {
351 mu.Lock()
352 calls++
353 first := calls == 1
354 mu.Unlock()
355 if first {
356 close(inLookup)
357 <-release
358 }
359 return []net.IP{net.ParseIP("10.0.0.1")}, nil
360 }
361
362 done := make(chan struct{}, 2)
363 go func() {
364 _, _ = A(context.Background(), lookup, "", "ip", "db.conc", "5432", time.Minute, zap.NewNop())
365 done <- struct{}{}
366 }()
367 <-inLookup
368
369 // Second caller for the same key; ordering is enforced by the channels,
370 // not by timing (see the SRV test above).
371 go func() {
372 _, _ = A(context.Background(), lookup, "", "ip", "db.conc", "5432", time.Minute, zap.NewNop())
373 done <- struct{}{}
374 }()
375
376 close(release)
377 <-done
378 <-done
379
380 mu.Lock()
381 got := calls
382 mu.Unlock()
383 if got != 1 {
384 t.Fatalf("lookup calls = %d, want 1 (concurrent callers for the same key must share one lookup)", got)
385 }
386 }
387
388 func TestSRVGracePeriodWithoutCachedResult(t *testing.T) {
389 ResetSRV("", "svc-cold-grace", "tcp", "x")
390 t.Cleanup(func() { ResetSRV("", "svc-cold-grace", "tcp", "x") })
391 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
392 return "", nil, errors.New("dns boom")
393 }
394 targets, err := SRV(context.Background(), lookup, "", "svc-cold-grace", "tcp", "x", time.Minute, time.Minute, zap.NewNop())
395 if err != nil || len(targets) != 0 {
396 t.Fatalf("cold-cache failure with grace: targets = %v, err = %v; want no targets and no error", targets, err)
397 }
398 }
399
400 func TestDiscoveryLoggingOnlyOnRefresh(t *testing.T) {
401 t.Run("SRV", func(t *testing.T) {
402 ResetSRV("", "svc-log", "tcp", "x")
403 t.Cleanup(func() { ResetSRV("", "svc-log", "tcp", "x") })
404 core, logs := observer.New(zap.DebugLevel)
405 logger := zap.New(core)
406 lookup := func(context.Context, string, string, string) (string, []*net.SRV, error) {
407 return "", []*net.SRV{{Target: "a.example.", Port: 5432, Priority: 1, Weight: 10}}, nil
408 }
409 for range 2 {
410 if _, err := SRV(context.Background(), lookup, "", "svc-log", "tcp", "x", time.Minute, 0, logger); err != nil {
411 t.Fatal(err)
412 }
413 }
414 if got := logs.FilterMessage("refreshing SRV upstreams").Len(); got != 1 {
415 t.Fatalf("refresh logs = %d, want 1", got)
416 }
417 records := logs.FilterMessage("discovered SRV record").All()
418 if len(records) != 1 {
419 t.Fatalf("discovery logs = %d, want 1", len(records))
420 }
421 fields := records[0].ContextMap()
422 if fields["target"] != "a.example." || fields["port"] != uint16(5432) || fields["priority"] != uint16(1) || fields["weight"] != uint16(10) {
423 t.Fatalf("unexpected discovery log fields: %v", fields)
424 }
425 })
426 t.Run("A", func(t *testing.T) {
427 ResetAllA()
428 core, logs := observer.New(zap.DebugLevel)
429 logger := zap.New(core)
430 lookup := func(context.Context, string, string) ([]net.IP, error) {
431 return []net.IP{net.ParseIP("10.0.0.1")}, nil
432 }
433 for range 2 {
434 if _, err := A(context.Background(), lookup, "", "ip", "db.log", "5432", time.Minute, logger); err != nil {
435 t.Fatal(err)
436 }
437 }
438 if got := logs.FilterMessage("refreshing A upstreams").Len(); got != 1 {
439 t.Fatalf("refresh logs = %d, want 1", got)
440 }
441 records := logs.FilterMessage("discovered A record").All()
442 if len(records) != 1 {
443 t.Fatalf("discovery logs = %d, want 1", len(records))
444 }
445 if got := records[0].ContextMap()["ip"]; got != "10.0.0.1" {
446 t.Fatalf("logged IP = %v, want 10.0.0.1", got)
447 }
448 })
449 }
450
451 // blockingSRV returns an SRV lookup that sends its context on started and then
452 // blocks until release is closed, answering with target.
453 func blockingSRV(target string, started chan<- context.Context, release <-chan struct{}) SRVLookupFunc {
454 return func(ctx context.Context, _, _, _ string) (string, []*net.SRV, error) {
455 started <- ctx
456 <-release
457 return "", []*net.SRV{{Target: target, Port: 1}}, nil
458 }
459 }
460
461 // instantSRV returns an SRV lookup that answers with target right away,
462 // counting its calls.
463 func instantSRV(target string, calls *atomic.Int32) SRVLookupFunc {
464 return func(context.Context, string, string, string) (string, []*net.SRV, error) {
465 calls.Add(1)
466 return "", []*net.SRV{{Target: target, Port: 1}}, nil
467 }
468 }
469
470 type srvResult struct {
471 targets []Target
472 err error
473 }
474
475 func goSRV(ctx context.Context, lookup SRVLookupFunc, service string) <-chan srvResult {
476 ch := make(chan srvResult, 1)
477 go func() {
478 targets, err := SRV(ctx, lookup, "", service, "tcp", "x", time.Minute, 0, zap.NewNop())
479 ch <- srvResult{targets, err}
480 }()
481 return ch
482 }
483
484 func waitResult(t *testing.T, ch <-chan srvResult) srvResult {
485 t.Helper()
486 select {
487 case res := <-ch:
488 return res
489 case <-time.After(5 * time.Second):
490 t.Fatal("timed out waiting for SRV to return")
491 return srvResult{}
492 }
493 }
494
495 // TestSlowLookupDoesNotBlockOtherKeys is a regression test for #7946: a slow
496 // lookup for one key used to hold the cache lock across the DNS call, blocking
497 // every other key.
498 func TestSlowLookupDoesNotBlockOtherKeys(t *testing.T) {
499 started := make(chan context.Context, 1)
500 release := make(chan struct{})
501 slow := goSRV(context.Background(), blockingSRV("slow.example.", started, release), "svc-slow")
502 <-started
503
504 var calls atomic.Int32
505 res := waitResult(t, goSRV(context.Background(), instantSRV("fast.example.", &calls), "svc-fast"))
506 if res.err != nil || len(res.targets) != 1 || res.targets[0].Host != "fast.example." {
507 t.Fatalf("unrelated key while another is slow: targets = %v, err = %v", res.targets, res.err)
508 }
509
510 close(release)
511 if res := waitResult(t, slow); res.err != nil {
512 t.Fatalf("slow lookup: %v", res.err)
513 }
514 }
515
516 // TestCallerCancellationDoesNotFailSharedLookup checks that a shared lookup
517 // outlives the caller that started it: that caller returns promptly with its
518 // own context error, while another caller on the same flight still gets the
519 // result. The lookup must still be bounded by a deadline of its own.
520 func TestCallerCancellationDoesNotFailSharedLookup(t *testing.T) {
521 started := make(chan context.Context, 2)
522 release := make(chan struct{})
523 lookup := blockingSRV("a.example.", started, release)
524
525 leaderCtx, cancelLeader := context.WithCancel(context.Background())
526 leader := goSRV(leaderCtx, lookup, "svc-leader")
527 lookupCtx := <-started
528 follower := goSRV(context.Background(), lookup, "svc-leader")
529
530 cancelLeader()
531 if res := waitResult(t, leader); !errors.Is(res.err, context.Canceled) {
532 t.Fatalf("cancelled caller: err = %v, want context.Canceled", res.err)
533 }
534 if err := lookupCtx.Err(); err != nil {
535 t.Fatalf("shared lookup was cancelled with its first caller: %v", err)
536 }
537 if _, ok := lookupCtx.Deadline(); !ok {
538 t.Fatal("shared lookup has no deadline of its own")
539 }
540
541 close(release)
542 res := waitResult(t, follower)
543 if res.err != nil || len(res.targets) != 1 {
544 t.Fatalf("remaining caller: targets = %v, err = %v", res.targets, res.err)
545 }
546 select {
547 case <-started:
548 t.Fatal("remaining caller started a second lookup instead of sharing the first")
549 default:
550 }
551 }
552
553 // TestResetDuringLookup checks that a reset invalidates a lookup already in
554 // flight: a caller arriving after the reset starts a new lookup rather than
555 // joining the old one, and the old one, finishing last, does not overwrite the
556 // newer result.
557 func TestResetDuringLookup(t *testing.T) {
558 started := make(chan context.Context, 1)
559 release := make(chan struct{})
560 old := goSRV(context.Background(), blockingSRV("old.example.", started, release), "svc-reset-inflight")
561 <-started
562
563 ResetSRV("", "svc-reset-inflight", "tcp", "x")
564
565 var calls atomic.Int32
566 res := waitResult(t, goSRV(context.Background(), instantSRV("new.example.", &calls), "svc-reset-inflight"))
567 if res.err != nil || calls.Load() != 1 || res.targets[0].Host != "new.example." {
568 t.Fatalf("caller after reset: targets = %v, err = %v, lookups = %d; want its own lookup", res.targets, res.err, calls.Load())
569 }
570
571 close(release)
572 if res := waitResult(t, old); res.err != nil {
573 t.Fatalf("pre-reset lookup: %v", res.err)
574 }
575
576 res = waitResult(t, goSRV(context.Background(), instantSRV("unused.example.", &calls), "svc-reset-inflight"))
577 if res.err != nil || res.targets[0].Host != "new.example." {
578 t.Fatalf("cached after both lookups: targets = %v, err = %v; want new.example.", res.targets, res.err)
579 }
580 }
581
582 // TestResetAllDuringLookup checks that a full reset also keeps an in-flight
583 // lookup from repopulating the cache.
584 func TestResetAllDuringLookup(t *testing.T) {
585 started := make(chan context.Context, 1)
586 release := make(chan struct{})
587 old := goSRV(context.Background(), blockingSRV("old.example.", started, release), "svc-reset-all-inflight")
588 <-started
589
590 ResetAllSRV()
591 close(release)
592 if res := waitResult(t, old); res.err != nil {
593 t.Fatalf("pre-reset lookup: %v", res.err)
594 }
595
596 srvCache.mu.RLock()
597 _, ok := srvCache.entries[srvKey("", "svc-reset-all-inflight", "tcp", "x")]
598 srvCache.mu.RUnlock()
599 if ok {
600 t.Fatal("lookup that started before ResetAllSRV wrote its result back")
601 }
602 }
603
604 // saturateLookups fills every slot of the shared lookup gate, and optionally
605 // its wait queue too, until the test ends.
606 func saturateLookups(t *testing.T, fillQueue bool) {
607 t.Helper()
608 for range cap(lookups.running) {
609 lookups.running <- struct{}{}
610 }
611 if fillQueue {
612 lookups.pending.Add(lookups.maxPending)
613 }
614 t.Cleanup(func() {
615 if fillQueue {
616 lookups.pending.Add(-lookups.maxPending)
617 }
618 for range cap(lookups.running) {
619 lookups.release()
620 }
621 })
622 }
623
624 // TestLookupsAreBounded checks the gate is wired in: while it is saturated a
625 // new key does not reach the resolver and its caller can still give up, and
626 // the lookup proceeds once slots free up.
627 func TestLookupsAreBounded(t *testing.T) {
628 var calls atomic.Int32
629 lookup := instantSRV("a.example.", &calls)
630
631 t.Run("saturated", func(t *testing.T) {
632 saturateLookups(t, false)
633 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
634 defer cancel()
635 if res := waitResult(t, goSRV(ctx, lookup, "svc-bounded")); !errors.Is(res.err, context.DeadlineExceeded) {
636 t.Fatalf("caller while saturated: err = %v, want its own deadline", res.err)
637 }
638 if n := calls.Load(); n != 0 {
639 t.Fatalf("resolver called %d times while the gate was saturated", n)
640 }
641 })
642
643 res := waitResult(t, goSRV(context.Background(), lookup, "svc-bounded"))
644 if res.err != nil || len(res.targets) != 1 {
645 t.Fatalf("after slots freed: targets = %v, err = %v", res.targets, res.err)
646 }
647 }
648
649 // TestShedLookups checks what happens when the gate's queue is full too: a
650 // stale SRV entry is served if there is a grace period; otherwise the lookup
651 // fails without reaching the resolver.
652 func TestShedLookups(t *testing.T) {
653 var calls atomic.Int32
654 seed := instantSRV("stale.example.", &calls)
655 for _, service := range []string{"svc-shed-stale", "svc-shed-nograce"} {
656 if _, err := SRV(context.Background(), seed, "", service, "tcp", "x", time.Minute, 0, zap.NewNop()); err != nil {
657 t.Fatal(err)
658 }
659 // backdate rather than relying on a tiny refresh, which a coarse
660 // clock (e.g. on Windows) can still report as fresh
661 key := srvKey("", service, "tcp", "x")
662 srvCache.mu.Lock()
663 e := srvCache.entries[key]
664 e.freshness = time.Now().Add(-time.Hour)
665 srvCache.entries[key] = e
666 srvCache.mu.Unlock()
667 }
668 calls.Store(0)
669
670 saturateLookups(t, true)
671
672 targets, err := SRV(context.Background(), seed, "", "svc-shed-stale", "tcp", "x", time.Minute, time.Hour, zap.NewNop())
673 if err != nil || len(targets) != 1 || targets[0].Host != "stale.example." {
674 t.Fatalf("shed with grace and a stale entry: targets = %v, err = %v; want the stale entry", targets, err)
675 }
676 for _, tc := range []struct {
677 name string
678 grace time.Duration
679 }{
680 {name: "svc-shed-nograce"},
681 {name: "svc-shed-cold", grace: time.Hour},
682 } {
683 if _, err := SRV(context.Background(), seed, "", tc.name, "tcp", "x", time.Minute, tc.grace, zap.NewNop()); !errors.Is(err, errTooManyLookups) {
684 t.Errorf("%s: err = %v, want errTooManyLookups", tc.name, err)
685 }
686 }
687 ipLookup := func(context.Context, string, string) ([]net.IP, error) {
688 calls.Add(1)
689 return []net.IP{net.ParseIP("10.0.0.1")}, nil
690 }
691 if _, err := A(context.Background(), ipLookup, "", "ip", "db.shed", "80", time.Minute, zap.NewNop()); !errors.Is(err, errTooManyLookups) {
692 t.Errorf("A: err = %v, want errTooManyLookups", err)
693 }
694 if n := calls.Load(); n != 0 {
695 t.Fatalf("resolver called %d times while lookups were being shed", n)
696 }
697 }
698
699 func TestResetA(t *testing.T) {
700 var calls atomic.Int32
701 lookup := func(context.Context, string, string) ([]net.IP, error) {
702 calls.Add(1)
703 return []net.IP{net.ParseIP("10.0.0.1")}, nil
704 }
705 get := func() {
706 t.Helper()
707 if _, err := A(context.Background(), lookup, "", "ip", "db.reset", "80", time.Minute, zap.NewNop()); err != nil {
708 t.Fatal(err)
709 }
710 }
711
712 get()
713 get()
714 ResetA("", "ip", "db.reset", "80")
715 get()
716 ResetAllA()
717 get()
718 if n := calls.Load(); n != 3 {
719 t.Fatalf("lookup calls = %d, want 3 (one, cached, then one after each reset)", n)
720 }
721 }