Skip to content

Commit e7d310f

Browse files
authored
Add DNS cache management methods for TCPDialer (#2072)
* Add DNS cache management methods for TCPDialer Resolves #2066 This commit introduces two new methods for managing DNS cache in TCPDialer: 1. FlushDNSCache() - Clears all cached DNS entries, forcing fresh lookups 2. CleanDNSCache() - Removes only expired entries based on DNSCacheDuration Key changes: - Add FlushDNSCache() and CleanDNSCache() methods to TCPDialer - Add global FlushDNSCache() and CleanDNSCache() functions for default dialer - Refactor tcpAddrsClean() to extract reusable cleanExpiredDNSEntries() method - Add comprehensive tests with mock resolver to verify caching behavior Use case: Users can now set longer cache durations (e.g., 30 minutes) and manually refresh DNS when needed, providing better control over DNS resolution timing while maintaining performance benefits of caching. * Remove CleanDNSCache method to reduce the API surface layer and related tests from TCPDialer * fix: resolve godot linter issue in client_test.go Add missing period to comment to comply with godot linter rule requiring comments to end with proper punctuation.
1 parent 563f4f6 commit e7d310f

File tree

2 files changed

+91
-8
lines changed

2 files changed

+91
-8
lines changed

client_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package fasthttp
33
import (
44
"bufio"
55
"bytes"
6+
"context"
67
"crypto/tls"
78
"errors"
89
"fmt"
@@ -3559,3 +3560,62 @@ func (f F) Read(p []byte) (n int, err error) {
35593560
time.Sleep(500 * time.Microsecond)
35603561
return f.Reader.Read(p)
35613562
}
3563+
3564+
func TestTCPDialerFlushDNSCache(t *testing.T) {
3565+
resolver := &testResolver{
3566+
lookupCountByHost: make(map[string]int),
3567+
resolver: net.DefaultResolver,
3568+
}
3569+
3570+
dialer := &TCPDialer{
3571+
DNSCacheDuration: 30 * time.Minute, // Long cache
3572+
Resolver: resolver,
3573+
}
3574+
3575+
// First dial - should trigger DNS lookup
3576+
conn1, err := dialer.DialTimeout("httpbin.org:80", 5*time.Second)
3577+
if err != nil {
3578+
t.Skip("Dial failed:", err)
3579+
}
3580+
conn1.Close()
3581+
3582+
if resolver.lookupCountByHost["httpbin.org"] != 1 {
3583+
t.Errorf("Expected 1 DNS lookup after first dial, got %d", resolver.lookupCountByHost["httpbin.org"])
3584+
}
3585+
3586+
// Second dial - should use cache (no new DNS lookup)
3587+
conn2, err := dialer.DialTimeout("httpbin.org:80", 5*time.Second)
3588+
if err != nil {
3589+
t.Skip("Second dial failed:", err)
3590+
}
3591+
conn2.Close()
3592+
3593+
if resolver.lookupCountByHost["httpbin.org"] != 1 {
3594+
t.Errorf("Expected 1 DNS lookup after cached dial, got %d", resolver.lookupCountByHost["httpbin.org"])
3595+
}
3596+
3597+
// Flush cache - should clear all entries
3598+
dialer.FlushDNSCache()
3599+
3600+
// Third dial - should trigger new DNS lookup since cache was flushed
3601+
conn3, err := dialer.DialTimeout("httpbin.org:80", 5*time.Second)
3602+
if err != nil {
3603+
t.Skip("Third dial failed:", err)
3604+
}
3605+
conn3.Close()
3606+
3607+
if resolver.lookupCountByHost["httpbin.org"] != 2 {
3608+
t.Errorf("Expected 2 DNS lookups after cache flush, got %d", resolver.lookupCountByHost["httpbin.org"])
3609+
}
3610+
}
3611+
3612+
// Simple test resolver that implements the Resolver interface.
3613+
type testResolver struct {
3614+
resolver *net.Resolver
3615+
lookupCountByHost map[string]int
3616+
}
3617+
3618+
func (r *testResolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {
3619+
r.lookupCountByHost[host]++
3620+
return r.resolver.LookupIPAddr(ctx, host)
3621+
}

tcpdialer.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,22 @@ func (d *TCPDialer) DialDualStackTimeout(addr string, timeout time.Duration) (ne
271271
return d.dial(addr, true, timeout)
272272
}
273273

274+
// FlushDNSCache clears all cached DNS entries, forcing fresh DNS lookups on subsequent dials.
275+
// This is useful when you want to ensure fresh DNS resolution, for example after network changes.
276+
func (d *TCPDialer) FlushDNSCache() {
277+
d.tcpAddrsMap.Range(func(k, v any) bool {
278+
d.tcpAddrsMap.Delete(k)
279+
return true
280+
})
281+
}
282+
283+
// FlushDNSCache clears all cached DNS entries for the default dialer,
284+
// forcing fresh DNS lookups on subsequent Dial* calls.
285+
// This is useful when you want to ensure fresh DNS resolution, for example after network changes.
286+
func FlushDNSCache() {
287+
defaultDialer.FlushDNSCache()
288+
}
289+
274290
func (d *TCPDialer) dial(addr string, dualStack bool, timeout time.Duration) (net.Conn, error) {
275291
d.once.Do(func() {
276292
if d.Concurrency > 0 {
@@ -406,17 +422,24 @@ type tcpAddrEntry struct {
406422
// by Dial* functions.
407423
const DefaultDNSCacheDuration = time.Minute
408424

409-
func (d *TCPDialer) tcpAddrsClean() {
425+
// cleanExpiredDNSEntries removes expired DNS cache entries based on DNSCacheDuration.
426+
// This is the core cleanup logic used by both the background cleaner and manual cleanup.
427+
func (d *TCPDialer) cleanExpiredDNSEntries() {
410428
expireDuration := 2 * d.DNSCacheDuration
429+
430+
t := time.Now()
431+
d.tcpAddrsMap.Range(func(k, v any) bool {
432+
if e, ok := v.(*tcpAddrEntry); ok && t.Sub(e.resolveTime) > expireDuration {
433+
d.tcpAddrsMap.Delete(k)
434+
}
435+
return true
436+
})
437+
}
438+
439+
func (d *TCPDialer) tcpAddrsClean() {
411440
for {
412441
time.Sleep(time.Second)
413-
t := time.Now()
414-
d.tcpAddrsMap.Range(func(k, v any) bool {
415-
if e, ok := v.(*tcpAddrEntry); ok && t.Sub(e.resolveTime) > expireDuration {
416-
d.tcpAddrsMap.Delete(k)
417-
}
418-
return true
419-
})
442+
d.cleanExpiredDNSEntries()
420443
}
421444
}
422445

0 commit comments

Comments
 (0)