Skip to content

Commit f7c5e6a

Browse files
authored
DNS resolving with timeout (#6917)
1 parent 815e2e2 commit f7c5e6a

File tree

4 files changed

+84
-11
lines changed

4 files changed

+84
-11
lines changed

internal/resolver/dns/dns_resolver.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ import (
4545
// addresses from SRV records. Must not be changed after init time.
4646
var EnableSRVLookups = false
4747

48+
// ResolvingTimeout specifies the maximum duration for a DNS resolution request.
49+
// If the timeout expires before a response is received, the request will be canceled.
50+
//
51+
// It is recommended to set this value at application startup. Avoid modifying this variable
52+
// after initialization as it's not thread-safe for concurrent modification.
53+
var ResolvingTimeout = 30 * time.Second
54+
4855
var logger = grpclog.Component("dns")
4956

5057
func init() {
@@ -221,18 +228,18 @@ func (d *dnsResolver) watcher() {
221228
}
222229
}
223230

224-
func (d *dnsResolver) lookupSRV() ([]resolver.Address, error) {
231+
func (d *dnsResolver) lookupSRV(ctx context.Context) ([]resolver.Address, error) {
225232
if !EnableSRVLookups {
226233
return nil, nil
227234
}
228235
var newAddrs []resolver.Address
229-
_, srvs, err := d.resolver.LookupSRV(d.ctx, "grpclb", "tcp", d.host)
236+
_, srvs, err := d.resolver.LookupSRV(ctx, "grpclb", "tcp", d.host)
230237
if err != nil {
231238
err = handleDNSError(err, "SRV") // may become nil
232239
return nil, err
233240
}
234241
for _, s := range srvs {
235-
lbAddrs, err := d.resolver.LookupHost(d.ctx, s.Target)
242+
lbAddrs, err := d.resolver.LookupHost(ctx, s.Target)
236243
if err != nil {
237244
err = handleDNSError(err, "A") // may become nil
238245
if err == nil {
@@ -269,8 +276,8 @@ func handleDNSError(err error, lookupType string) error {
269276
return err
270277
}
271278

272-
func (d *dnsResolver) lookupTXT() *serviceconfig.ParseResult {
273-
ss, err := d.resolver.LookupTXT(d.ctx, txtPrefix+d.host)
279+
func (d *dnsResolver) lookupTXT(ctx context.Context) *serviceconfig.ParseResult {
280+
ss, err := d.resolver.LookupTXT(ctx, txtPrefix+d.host)
274281
if err != nil {
275282
if envconfig.TXTErrIgnore {
276283
return nil
@@ -297,8 +304,8 @@ func (d *dnsResolver) lookupTXT() *serviceconfig.ParseResult {
297304
return d.cc.ParseServiceConfig(sc)
298305
}
299306

300-
func (d *dnsResolver) lookupHost() ([]resolver.Address, error) {
301-
addrs, err := d.resolver.LookupHost(d.ctx, d.host)
307+
func (d *dnsResolver) lookupHost(ctx context.Context) ([]resolver.Address, error) {
308+
addrs, err := d.resolver.LookupHost(ctx, d.host)
302309
if err != nil {
303310
err = handleDNSError(err, "A")
304311
return nil, err
@@ -316,8 +323,10 @@ func (d *dnsResolver) lookupHost() ([]resolver.Address, error) {
316323
}
317324

318325
func (d *dnsResolver) lookup() (*resolver.State, error) {
319-
srv, srvErr := d.lookupSRV()
320-
addrs, hostErr := d.lookupHost()
326+
ctx, cancel := context.WithTimeout(d.ctx, ResolvingTimeout)
327+
defer cancel()
328+
srv, srvErr := d.lookupSRV(ctx)
329+
addrs, hostErr := d.lookupHost(ctx)
321330
if hostErr != nil && (srvErr != nil || len(srv) == 0) {
322331
return nil, hostErr
323332
}
@@ -327,7 +336,7 @@ func (d *dnsResolver) lookup() (*resolver.State, error) {
327336
state = grpclbstate.Set(state, &grpclbstate.State{BalancerAddresses: srv})
328337
}
329338
if !d.disableServiceConfig {
330-
state.ServiceConfig = d.lookupTXT()
339+
state.ServiceConfig = d.lookupTXT(ctx)
331340
}
332341
return &state, nil
333342
}

internal/resolver/dns/dns_resolver_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
dnsinternal "google.golang.org/grpc/internal/resolver/dns/internal"
4040
"google.golang.org/grpc/internal/testutils"
4141
"google.golang.org/grpc/resolver"
42+
dnspublic "google.golang.org/grpc/resolver/dns"
4243
"google.golang.org/grpc/serviceconfig"
4344

4445
_ "google.golang.org/grpc" // To initialize internal.ParseServiceConfig
@@ -1215,3 +1216,45 @@ func (s) TestReportError(t *testing.T) {
12151216
}
12161217
}
12171218
}
1219+
1220+
// Override the default dns.ResolvingTimeout with a test duration.
1221+
func overrideResolveTimeoutDuration(t *testing.T, dur time.Duration) {
1222+
t.Helper()
1223+
1224+
origDur := dns.ResolvingTimeout
1225+
dnspublic.SetResolvingTimeout(dur)
1226+
1227+
t.Cleanup(func() { dnspublic.SetResolvingTimeout(origDur) })
1228+
}
1229+
1230+
// Test verifies that the DNS resolver gets timeout error when net.Resolver
1231+
// takes too long to resolve a target.
1232+
func (s) TestResolveTimeout(t *testing.T) {
1233+
// Set DNS resolving timeout duration to 7ms
1234+
timeoutDur := 7 * time.Millisecond
1235+
overrideResolveTimeoutDuration(t, timeoutDur)
1236+
1237+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
1238+
defer cancel()
1239+
1240+
// We are trying to resolve hostname which takes infinity time to resolve.
1241+
const target = "infinity"
1242+
1243+
// Define a testNetResolver with lookupHostCh, an unbuffered channel,
1244+
// so we can block the resolver until reaching timeout.
1245+
tr := &testNetResolver{
1246+
lookupHostCh: testutils.NewChannelWithSize(0),
1247+
hostLookupTable: map[string][]string{target: {"1.2.3.4"}},
1248+
}
1249+
overrideNetResolver(t, tr)
1250+
1251+
_, _, errCh := buildResolverWithTestClientConn(t, target)
1252+
select {
1253+
case <-ctx.Done():
1254+
t.Fatal("Timeout when waiting for the DNS resolver to timeout")
1255+
case err := <-errCh:
1256+
if err == nil || !strings.Contains(err.Error(), "context deadline exceeded") {
1257+
t.Fatalf(`Expected to see Timeout error; got: %v`, err)
1258+
}
1259+
}
1260+
}

internal/resolver/dns/fake_net_resolver_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ type testNetResolver struct {
4141

4242
func (tr *testNetResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
4343
if tr.lookupHostCh != nil {
44-
tr.lookupHostCh.Send(nil)
44+
if err := tr.lookupHostCh.SendContext(ctx, nil); err != nil {
45+
return nil, err
46+
}
4547
}
4648

4749
tr.mu.Lock()
@@ -50,6 +52,7 @@ func (tr *testNetResolver) LookupHost(ctx context.Context, host string) ([]strin
5052
if addrs, ok := tr.hostLookupTable[host]; ok {
5153
return addrs, nil
5254
}
55+
5356
return nil, &net.DNSError{
5457
Err: "hostLookup error",
5558
Name: host,

resolver/dns/dns_resolver.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,28 @@
2424
package dns
2525

2626
import (
27+
"time"
28+
2729
"google.golang.org/grpc/internal/resolver/dns"
2830
"google.golang.org/grpc/resolver"
2931
)
3032

33+
// SetResolvingTimeout sets the maximum duration for DNS resolution requests.
34+
//
35+
// This function affects the global timeout used by all channels using the DNS
36+
// name resolver scheme.
37+
//
38+
// It must be called only at application startup, before any gRPC calls are
39+
// made. Modifying this value after initialization is not thread-safe.
40+
//
41+
// The default value is 30 seconds. Setting the timeout too low may result in
42+
// premature timeouts during resolution, while setting it too high may lead to
43+
// unnecessary delays in service discovery. Choose a value appropriate for your
44+
// specific needs and network environment.
45+
func SetResolvingTimeout(timeout time.Duration) {
46+
dns.ResolvingTimeout = timeout
47+
}
48+
3149
// NewBuilder creates a dnsBuilder which is used to factory DNS resolvers.
3250
//
3351
// Deprecated: import grpc and use resolver.Get("dns") instead.

0 commit comments

Comments
 (0)