20
"fmt"
21
"net"
22
"sync"
23
+ "sync/atomic"
24
"testing"
25
"time"
26
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
}
171
- srvMu.RLock()
172
- populated := len(srvCache) > 0
173
- srvMu.RUnlock()
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
180
- srvMu.RLock()
181
- n := len(srvCache)
182
- srvMu.RUnlock()
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
}
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) {
191
- srvMu.Lock()
192
- srvCache = make(map[string]cacheEntry)
193
- srvMu.Unlock()
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
200
t.Fatalf("SRV[%d]: %v", i, err)
201
}
202
}
204
- srvMu.RLock()
205
- n := len(srvCache)
206
- srvMu.RUnlock()
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
}
239
240
// TestAResolverKeyIsolatesCache is the A-cache equivalent of the above.
241
func TestAResolverKeyIsolatesCache(t *testing.T) {
243
- aMu.Lock()
244
- aCache = make(map[string]cacheEntry)
245
- aMu.Unlock()
242
+ ResetAllA()
243
244
lookupA := func(context.Context, string, string) ([]net.IP, error) {
245
return []net.IP{net.ParseIP("10.0.0.1")}, nil
266
267
// TestACacheBound verifies the same bounding behavior for the A cache.
268
func TestACacheBound(t *testing.T) {
272
- aMu.Lock()
273
- aCache = make(map[string]cacheEntry)
274
- aMu.Unlock()
269
+ ResetAllA()
270
271
lookup := func(context.Context, string, string) ([]net.IP, error) {
272
return []net.IP{net.ParseIP("10.0.0.1")}, nil
277
t.Fatalf("A[%d]: %v", i, err)
278
}
279
}
285
- aMu.RLock()
286
- n := len(aCache)
287
- aMu.RUnlock()
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
294
-// missing the cache for the same key result in a single lookup: the first
295
-// performs it while holding the write lock, and the second is served the
296
-// cached result once the first releases. Depending on scheduling the second
297
-// caller may be served by either the read-lock fast path or the write-lock
298
-// re-check — this test does not force one branch over the other; it asserts
299
-// deduplication (exactly one lookup). Run under -race it also guards against
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) {
302
- srvMu.Lock()
303
- srvCache = make(map[string]cacheEntry)
304
- srvMu.Unlock()
295
+ ResetAllSRV()
296
297
var calls int
298
var mu sync.Mutex
305
mu.Unlock()
306
if first {
307
close(inLookup)
317
- <-release // hold the write lock until the second goroutine is queued
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)
323
- // G1: takes the write lock and blocks inside lookup.
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
}()
328
- <-inLookup // G1 now holds the write lock; cache is still empty
319
+ <-inLookup // G1 is mid-lookup; cache is still empty
320
330
- // G2: a second caller for the same key. While G1 holds the write lock, G2
331
- // cannot acquire srvMu at all (a reader blocks behind the writer); once G1
332
- // populates the cache and releases, G2 observes the cached entry and
333
- // performs no lookup. Ordering here is enforced by the channels below, not
334
- // by timing.
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{}{}
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) {
356
- aMu.Lock()
357
- aCache = make(map[string]cacheEntry)
358
- aMu.Unlock()
344
+ ResetAllA()
345
346
var calls int
347
var mu sync.Mutex
424
}
425
})
426
t.Run("A", func(t *testing.T) {
441
- aMu.Lock()
442
- aCache = make(map[string]cacheEntry)
443
- aMu.Unlock()
427
+ ResetAllA()
428
core, logs := observer.New(zap.DebugLevel)
429
logger := zap.New(core)
430
lookup := func(context.Context, string, string) ([]net.IP, error) {
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
+}