• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20038

30 Oct 2025 02:58PM UTC coverage: 88.518% (-0.05%) from 88.57%
#20038

push

github

web-flow
core: simplify DnsNameResolver.resolveAddresses()

`resolveAddresses()` is a private method, called only once. There is no
need to handle exceptions in multiple places.

The reason for creating this PR:
I have noticed an exception like this in the logs
```
2025-10-16 13:09:33.141 WARN  [grpc-default-executor-222]    ManagedChannelImpl            [Channel<47>: (x.y.com:443)] Failed to resolve name. status=Status{code=UNAVAILABLE, 
description=Unable to resolve host x.y.com, 
cause=java.lang.RuntimeException: java.net.UnknownHostException: x.y.com: nodename nor servname provided, or not known
...
Caused by: java.net.UnknownHostException: x.y.com: nodename nor servname provided, or not known
...
}
```

34953 of 39487 relevant lines covered (88.52%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.67
/../core/src/main/java/io/grpc/internal/DnsNameResolver.java
1
/*
2
 * Copyright 2015 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.internal;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.base.MoreObjects;
23
import com.google.common.base.Objects;
24
import com.google.common.base.Preconditions;
25
import com.google.common.base.Stopwatch;
26
import com.google.common.base.Verify;
27
import com.google.common.base.VerifyException;
28
import io.grpc.Attributes;
29
import io.grpc.EquivalentAddressGroup;
30
import io.grpc.NameResolver;
31
import io.grpc.ProxiedSocketAddress;
32
import io.grpc.ProxyDetector;
33
import io.grpc.Status;
34
import io.grpc.StatusOr;
35
import io.grpc.SynchronizationContext;
36
import io.grpc.internal.SharedResourceHolder.Resource;
37
import java.io.IOException;
38
import java.lang.reflect.Constructor;
39
import java.net.InetAddress;
40
import java.net.InetSocketAddress;
41
import java.net.URI;
42
import java.net.UnknownHostException;
43
import java.util.ArrayList;
44
import java.util.Arrays;
45
import java.util.Collections;
46
import java.util.HashSet;
47
import java.util.List;
48
import java.util.Map;
49
import java.util.Random;
50
import java.util.Set;
51
import java.util.concurrent.Executor;
52
import java.util.concurrent.TimeUnit;
53
import java.util.concurrent.atomic.AtomicReference;
54
import java.util.logging.Level;
55
import java.util.logging.Logger;
56
import javax.annotation.Nullable;
57

58
/**
59
 * A DNS-based {@link NameResolver}.
60
 *
61
 * <p>Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list
62
 * passed to {@link NameResolver.Listener2#onResult2(ResolutionResult)}.
63
 *
64
 * @see DnsNameResolverProvider
65
 */
66
public class DnsNameResolver extends NameResolver {
67

68
  private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
1✔
69

70
  private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
71
  private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
72
  private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
73
  private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
74

75
  // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
76
  static final String SERVICE_CONFIG_PREFIX = "grpc_config=";
77
  private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
1✔
78
      Collections.unmodifiableSet(
1✔
79
          new HashSet<>(
80
              Arrays.asList(
1✔
81
                  SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
82
                  SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
83
                  SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
84
                  SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
85

86
  // From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
87
  private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
88

89
  private static final String JNDI_PROPERTY =
1✔
90
      System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "true");
1✔
91
  private static final String JNDI_LOCALHOST_PROPERTY =
1✔
92
      System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi_localhost", "false");
1✔
93
  private static final String JNDI_TXT_PROPERTY =
1✔
94
      System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_service_config", "false");
1✔
95

96
  /**
97
   * Java networking system properties name for caching DNS result.
98
   *
99
   * <p>Default value is -1 (cache forever) if security manager is installed. If security manager is
100
   * not installed, the ttl value is {@code null} which falls back to {@link
101
   * #DEFAULT_NETWORK_CACHE_TTL_SECONDS gRPC default value}.
102
   *
103
   * <p>For android, gRPC doesn't attempt to cache; this property value will be ignored.
104
   */
105
  @VisibleForTesting
106
  static final String NETWORKADDRESS_CACHE_TTL_PROPERTY = "networkaddress.cache.ttl";
107
  /** Default DNS cache duration if network cache ttl value is not specified ({@code null}). */
108
  @VisibleForTesting
109
  static final long DEFAULT_NETWORK_CACHE_TTL_SECONDS = 30;
110

111
  @VisibleForTesting
112
  static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY);
1✔
113
  @VisibleForTesting
114
  static boolean enableJndiLocalhost = Boolean.parseBoolean(JNDI_LOCALHOST_PROPERTY);
1✔
115
  @VisibleForTesting
116
  protected static boolean enableTxt = Boolean.parseBoolean(JNDI_TXT_PROPERTY);
1✔
117

118
  private static final ResourceResolverFactory resourceResolverFactory =
1✔
119
      getResourceResolverFactory(DnsNameResolver.class.getClassLoader());
1✔
120

121
  @VisibleForTesting
122
  final ProxyDetector proxyDetector;
123

124
  /** Access through {@link #getLocalHostname}. */
125
  private static String localHostname;
126

127
  private final Random random = new Random();
1✔
128

129
  protected volatile AddressResolver addressResolver = JdkAddressResolver.INSTANCE;
1✔
130
  private final AtomicReference<ResourceResolver> resourceResolver = new AtomicReference<>();
1✔
131

132
  private final String authority;
133
  private final String host;
134
  private final int port;
135

136
  private final ObjectPool<Executor> executorPool;
137
  private final long cacheTtlNanos;
138
  private final SynchronizationContext syncContext;
139
  private final ServiceConfigParser serviceConfigParser;
140

141
  // Following fields must be accessed from syncContext
142
  private final Stopwatch stopwatch;
143
  protected boolean resolved;
144
  private boolean shutdown;
145
  private Executor executor;
146

147
  private boolean resolving;
148

149
  // The field must be accessed from syncContext, although the methods on an Listener2 can be called
150
  // from any thread.
151
  private NameResolver.Listener2 listener;
152

153
  protected DnsNameResolver(
154
      @Nullable String nsAuthority,
155
      String name,
156
      Args args,
157
      Resource<Executor> executorResource,
158
      Stopwatch stopwatch,
159
      boolean isAndroid) {
1✔
160
    checkNotNull(args, "args");
1✔
161
    // TODO: if a DNS server is provided as nsAuthority, use it.
162
    // https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java
163

164
    // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
165
    // opaque URI, thus the authority and host of the resulted URI would be null.
166
    URI nameUri = URI.create("//" + checkNotNull(name, "name"));
1✔
167
    Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name);
1✔
168
    authority = Preconditions.checkNotNull(nameUri.getAuthority(),
1✔
169
        "nameUri (%s) doesn't have an authority", nameUri);
170
    host = nameUri.getHost();
1✔
171
    if (nameUri.getPort() == -1) {
1✔
172
      port = args.getDefaultPort();
1✔
173
    } else {
174
      port = nameUri.getPort();
1✔
175
    }
176
    this.proxyDetector = checkNotNull(args.getProxyDetector(), "proxyDetector");
1✔
177
    Executor offloadExecutor = args.getOffloadExecutor();
1✔
178
    if (offloadExecutor != null) {
1✔
179
      this.executorPool = new FixedObjectPool<>(offloadExecutor);
1✔
180
    } else {
181
      this.executorPool = SharedResourcePool.forResource(executorResource);
1✔
182
    }
183
    this.cacheTtlNanos = getNetworkAddressCacheTtlNanos(isAndroid);
1✔
184
    this.stopwatch = checkNotNull(stopwatch, "stopwatch");
1✔
185
    this.syncContext = checkNotNull(args.getSynchronizationContext(), "syncContext");
1✔
186
    this.serviceConfigParser = checkNotNull(args.getServiceConfigParser(), "serviceConfigParser");
1✔
187
  }
1✔
188

189
  @Override
190
  public String getServiceAuthority() {
191
    return authority;
1✔
192
  }
193

194
  @VisibleForTesting
195
  protected String getHost() {
196
    return host;
1✔
197
  }
198

199
  @Override
200
  public void start(Listener2 listener) {
201
    Preconditions.checkState(this.listener == null, "already started");
1✔
202
    executor = executorPool.getObject();
1✔
203
    this.listener = checkNotNull(listener, "listener");
1✔
204
    resolve();
1✔
205
  }
1✔
206

207
  @Override
208
  public void refresh() {
209
    Preconditions.checkState(listener != null, "not started");
1✔
210
    resolve();
1✔
211
  }
1✔
212

213
  private List<EquivalentAddressGroup> resolveAddresses() throws Exception {
214
    List<? extends InetAddress> addresses = addressResolver.resolveAddress(host);
1✔
215
    // Each address forms an EAG
216
    List<EquivalentAddressGroup> servers = new ArrayList<>(addresses.size());
1✔
217
    for (InetAddress inetAddr : addresses) {
1✔
218
      servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
1✔
219
    }
1✔
220
    return Collections.unmodifiableList(servers);
1✔
221
  }
222

223
  @Nullable
224
  private ConfigOrError resolveServiceConfig() {
225
    List<String> txtRecords = Collections.emptyList();
1✔
226
    ResourceResolver resourceResolver = getResourceResolver();
1✔
227
    if (resourceResolver != null) {
1✔
228
      try {
229
        txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + host);
1✔
230
      } catch (Exception e) {
1✔
231
        logger.log(Level.FINE, "ServiceConfig resolution failure", e);
1✔
232
      }
1✔
233
    }
234
    if (!txtRecords.isEmpty()) {
1✔
235
      ConfigOrError rawServiceConfig = parseServiceConfig(txtRecords, random, getLocalHostname());
1✔
236
      if (rawServiceConfig != null) {
1✔
237
        if (rawServiceConfig.getError() != null) {
1✔
238
          return ConfigOrError.fromError(rawServiceConfig.getError());
1✔
239
        }
240

241
        @SuppressWarnings("unchecked")
242
        Map<String, ?> verifiedRawServiceConfig = (Map<String, ?>) rawServiceConfig.getConfig();
1✔
243
        return serviceConfigParser.parseServiceConfig(verifiedRawServiceConfig);
1✔
244
      }
245
    } else {
×
246
      logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host});
1✔
247
    }
248
    return null;
1✔
249
  }
250

251
  @Nullable
252
  private EquivalentAddressGroup detectProxy() throws IOException {
253
    InetSocketAddress destination =
1✔
254
        InetSocketAddress.createUnresolved(host, port);
1✔
255
    ProxiedSocketAddress proxiedAddr = proxyDetector.proxyFor(destination);
1✔
256
    if (proxiedAddr != null) {
1✔
257
      return new EquivalentAddressGroup(proxiedAddr);
1✔
258
    }
259
    return null;
1✔
260
  }
261

262
  /**
263
   * Main logic of name resolution.
264
   */
265
  protected InternalResolutionResult doResolve(boolean forceTxt) {
266
    InternalResolutionResult result = new InternalResolutionResult();
1✔
267
    try {
268
      result.addresses = resolveAddresses();
1✔
269
    } catch (Exception e) {
1✔
270
      logger.log(Level.FINE, "Address resolution failure", e);
1✔
271
      if (!forceTxt) {
1✔
272
        result.error =
1✔
273
            Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e);
1✔
274
        return result;
1✔
275
      }
276
    }
1✔
277
    if (enableTxt) {
1✔
278
      result.config = resolveServiceConfig();
1✔
279
    }
280
    return result;
1✔
281
  }
282

283
  private final class Resolve implements Runnable {
284
    private final Listener2 savedListener;
285

286
    Resolve(Listener2 savedListener) {
1✔
287
      this.savedListener = checkNotNull(savedListener, "savedListener");
1✔
288
    }
1✔
289

290
    @Override
291
    public void run() {
292
      if (logger.isLoggable(Level.FINER)) {
1✔
293
        logger.finer("Attempting DNS resolution of " + host);
×
294
      }
295
      InternalResolutionResult result = null;
1✔
296
      try {
297
        EquivalentAddressGroup proxiedAddr = detectProxy();
1✔
298
        ResolutionResult.Builder resolutionResultBuilder = ResolutionResult.newBuilder();
1✔
299
        if (proxiedAddr != null) {
1✔
300
          if (logger.isLoggable(Level.FINER)) {
1✔
301
            logger.finer("Using proxy address " + proxiedAddr);
×
302
          }
303
          resolutionResultBuilder.setAddressesOrError(
1✔
304
              StatusOr.fromValue(Collections.singletonList(proxiedAddr)));
1✔
305
        } else {
306
          result = doResolve(false);
1✔
307
          if (result.error != null) {
1✔
308
            InternalResolutionResult finalResult = result;
1✔
309
            syncContext.execute(() ->
1✔
310
                savedListener.onResult2(ResolutionResult.newBuilder()
1✔
311
                    .setAddressesOrError(StatusOr.fromStatus(finalResult.error))
1✔
312
                    .build()));
1✔
313
            return;
1✔
314
          }
315
          if (result.addresses != null) {
1✔
316
            resolutionResultBuilder.setAddressesOrError(StatusOr.fromValue(result.addresses));
1✔
317
          }
318
          if (result.config != null) {
1✔
319
            resolutionResultBuilder.setServiceConfig(result.config);
1✔
320
          }
321
          if (result.attributes != null) {
1✔
322
            resolutionResultBuilder.setAttributes(result.attributes);
1✔
323
          }
324
        }
325
        syncContext.execute(() -> {
1✔
326
          savedListener.onResult2(resolutionResultBuilder.build());
1✔
327
        });
1✔
328
      } catch (IOException e) {
1✔
329
        syncContext.execute(() ->
1✔
330
            savedListener.onResult2(ResolutionResult.newBuilder()
1✔
331
                .setAddressesOrError(
1✔
332
                    StatusOr.fromStatus(
1✔
333
                        Status.UNAVAILABLE.withDescription(
1✔
334
                            "Unable to resolve host " + host).withCause(e))).build()));
1✔
335
      } finally {
336
        final boolean succeed = result != null && result.error == null;
1✔
337
        syncContext.execute(new Runnable() {
1✔
338
          @Override
339
          public void run() {
340
            if (succeed) {
1✔
341
              resolved = true;
1✔
342
              if (cacheTtlNanos > 0) {
1✔
343
                stopwatch.reset().start();
1✔
344
              }
345
            }
346
            resolving = false;
1✔
347
          }
1✔
348
        });
349
      }
350
    }
1✔
351
  }
352

353
  @Nullable
354
  static ConfigOrError parseServiceConfig(
355
      List<String> rawTxtRecords, Random random, String localHostname) {
356
    List<Map<String, ?>> possibleServiceConfigChoices;
357
    try {
358
      possibleServiceConfigChoices = parseTxtResults(rawTxtRecords);
1✔
359
    } catch (IOException | RuntimeException e) {
1✔
360
      return ConfigOrError.fromError(
1✔
361
          Status.UNKNOWN.withDescription("failed to parse TXT records").withCause(e));
1✔
362
    }
1✔
363
    Map<String, ?> possibleServiceConfig = null;
1✔
364
    for (Map<String, ?> possibleServiceConfigChoice : possibleServiceConfigChoices) {
1✔
365
      try {
366
        possibleServiceConfig =
1✔
367
            maybeChooseServiceConfig(possibleServiceConfigChoice, random, localHostname);
1✔
368
      } catch (RuntimeException e) {
1✔
369
        return ConfigOrError.fromError(
1✔
370
            Status.UNKNOWN.withDescription("failed to pick service config choice").withCause(e));
1✔
371
      }
1✔
372
      if (possibleServiceConfig != null) {
1✔
373
        break;
1✔
374
      }
375
    }
×
376
    if (possibleServiceConfig == null) {
1✔
377
      return null;
1✔
378
    }
379
    return ConfigOrError.fromConfig(possibleServiceConfig);
1✔
380
  }
381

382
  private void resolve() {
383
    if (resolving || shutdown || !cacheRefreshRequired()) {
1✔
384
      return;
1✔
385
    }
386
    resolving = true;
1✔
387
    executor.execute(new Resolve(listener));
1✔
388
  }
1✔
389

390
  private boolean cacheRefreshRequired() {
391
    return !resolved
1✔
392
        || cacheTtlNanos == 0
393
        || (cacheTtlNanos > 0 && stopwatch.elapsed(TimeUnit.NANOSECONDS) > cacheTtlNanos);
1✔
394
  }
395

396
  @Override
397
  public void shutdown() {
398
    if (shutdown) {
1✔
399
      return;
×
400
    }
401
    shutdown = true;
1✔
402
    if (executor != null) {
1✔
403
      executor = executorPool.returnObject(executor);
1✔
404
    }
405
  }
1✔
406

407
  final int getPort() {
408
    return port;
1✔
409
  }
410

411
  /**
412
   * Parse TXT service config records as JSON.
413
   *
414
   * @throws IOException if one of the txt records contains improperly formatted JSON.
415
   */
416
  @VisibleForTesting
417
  static List<Map<String, ?>> parseTxtResults(List<String> txtRecords) throws IOException {
418
    List<Map<String, ?>> possibleServiceConfigChoices = new ArrayList<>();
1✔
419
    for (String txtRecord : txtRecords) {
1✔
420
      if (!txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
1✔
421
        logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
1✔
422
        continue;
1✔
423
      }
424
      Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
1✔
425
      if (!(rawChoices instanceof List)) {
1✔
426
        throw new ClassCastException("wrong type " + rawChoices);
1✔
427
      }
428
      List<?> listChoices = (List<?>) rawChoices;
1✔
429
      possibleServiceConfigChoices.addAll(JsonUtil.checkObjectList(listChoices));
1✔
430
    }
1✔
431
    return possibleServiceConfigChoices;
1✔
432
  }
433

434
  @Nullable
435
  private static final Double getPercentageFromChoice(Map<String, ?> serviceConfigChoice) {
436
    return JsonUtil.getNumberAsDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
1✔
437
  }
438

439
  @Nullable
440
  private static final List<String> getClientLanguagesFromChoice(
441
      Map<String, ?> serviceConfigChoice) {
442
    return JsonUtil.getListOfStrings(
1✔
443
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY);
444
  }
445

446
  @Nullable
447
  private static final List<String> getHostnamesFromChoice(Map<String, ?> serviceConfigChoice) {
448
    return JsonUtil.getListOfStrings(
1✔
449
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY);
450
  }
451

452
  /**
453
   * Returns value of network address cache ttl property if not Android environment. For android,
454
   * DnsNameResolver does not cache the dns lookup result.
455
   */
456
  private static long getNetworkAddressCacheTtlNanos(boolean isAndroid) {
457
    if (isAndroid) {
1✔
458
      // on Android, ignore dns cache.
459
      return 0;
1✔
460
    }
461

462
    String cacheTtlPropertyValue = System.getProperty(NETWORKADDRESS_CACHE_TTL_PROPERTY);
1✔
463
    long cacheTtl = DEFAULT_NETWORK_CACHE_TTL_SECONDS;
1✔
464
    if (cacheTtlPropertyValue != null) {
1✔
465
      try {
466
        cacheTtl = Long.parseLong(cacheTtlPropertyValue);
1✔
467
      } catch (NumberFormatException e) {
1✔
468
        logger.log(
1✔
469
            Level.WARNING,
470
            "Property({0}) valid is not valid number format({1}), fall back to default({2})",
471
            new Object[] {NETWORKADDRESS_CACHE_TTL_PROPERTY, cacheTtlPropertyValue, cacheTtl});
1✔
472
      }
1✔
473
    }
474
    return cacheTtl > 0 ? TimeUnit.SECONDS.toNanos(cacheTtl) : cacheTtl;
1✔
475
  }
476

477
  /**
478
   * Determines if a given Service Config choice applies, and if so, returns it.
479
   *
480
   * @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
481
   *   Service Config in DNS</a>
482
   * @param choice The service config choice.
483
   * @return The service config object or {@code null} if this choice does not apply.
484
   */
485
  @Nullable
486
  @VisibleForTesting
487
  static Map<String, ?> maybeChooseServiceConfig(
488
      Map<String, ?> choice, Random random, String hostname) {
489
    for (Map.Entry<String, ?> entry : choice.entrySet()) {
1✔
490
      Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
1✔
491
    }
1✔
492

493
    List<String> clientLanguages = getClientLanguagesFromChoice(choice);
1✔
494
    if (clientLanguages != null && !clientLanguages.isEmpty()) {
1✔
495
      boolean javaPresent = false;
1✔
496
      for (String lang : clientLanguages) {
1✔
497
        if ("java".equalsIgnoreCase(lang)) {
1✔
498
          javaPresent = true;
1✔
499
          break;
1✔
500
        }
501
      }
1✔
502
      if (!javaPresent) {
1✔
503
        return null;
1✔
504
      }
505
    }
506
    Double percentage = getPercentageFromChoice(choice);
1✔
507
    if (percentage != null) {
1✔
508
      int pct = percentage.intValue();
1✔
509
      Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
1✔
510
      if (random.nextInt(100) >= pct) {
1✔
511
        return null;
1✔
512
      }
513
    }
514
    List<String> clientHostnames = getHostnamesFromChoice(choice);
1✔
515
    if (clientHostnames != null && !clientHostnames.isEmpty()) {
1✔
516
      boolean hostnamePresent = false;
1✔
517
      for (String clientHostname : clientHostnames) {
1✔
518
        if (clientHostname.equals(hostname)) {
1✔
519
          hostnamePresent = true;
1✔
520
          break;
1✔
521
        }
522
      }
1✔
523
      if (!hostnamePresent) {
1✔
524
        return null;
1✔
525
      }
526
    }
527
    Map<String, ?> sc =
1✔
528
        JsonUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
1✔
529
    if (sc == null) {
1✔
530
      throw new VerifyException(String.format(
×
531
          "key '%s' missing in '%s'", choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY));
532
    }
533
    return sc;
1✔
534
  }
535

536
  /**
537
   * Used as a DNS-based name resolver's internal representation of resolution result.
538
   */
539
  protected static final class InternalResolutionResult {
540
    private Status error;
541
    private List<EquivalentAddressGroup> addresses;
542
    private ConfigOrError config;
543
    public Attributes attributes;
544

545
    private InternalResolutionResult() {}
546
  }
547

548
  /**
549
   * Describes a parsed SRV record.
550
   */
551
  @VisibleForTesting
552
  public static final class SrvRecord {
553
    public final String host;
554
    public final int port;
555

556
    public SrvRecord(String host, int port) {
1✔
557
      this.host = host;
1✔
558
      this.port = port;
1✔
559
    }
1✔
560

561
    @Override
562
    public int hashCode() {
563
      return Objects.hashCode(host, port);
×
564
    }
565

566
    @Override
567
    public boolean equals(Object obj) {
568
      if (this == obj) {
1✔
569
        return true;
×
570
      }
571
      if (obj == null || getClass() != obj.getClass()) {
1✔
572
        return false;
×
573
      }
574
      SrvRecord that = (SrvRecord) obj;
1✔
575
      return port == that.port && host.equals(that.host);
1✔
576
    }
577

578
    @Override
579
    public String toString() {
580
      return
×
581
          MoreObjects.toStringHelper(this)
×
582
              .add("host", host)
×
583
              .add("port", port)
×
584
              .toString();
×
585
    }
586
  }
587

588
  @VisibleForTesting
589
  protected void setAddressResolver(AddressResolver addressResolver) {
590
    this.addressResolver = addressResolver;
1✔
591
  }
1✔
592

593
  @VisibleForTesting
594
  protected void setResourceResolver(ResourceResolver resourceResolver) {
595
    this.resourceResolver.set(resourceResolver);
1✔
596
  }
1✔
597

598
  /**
599
   * {@link ResourceResolverFactory} is a factory for making resource resolvers.  It supports
600
   * optionally checking if the factory is available.
601
   */
602
  interface ResourceResolverFactory {
603

604
    /**
605
     * Creates a new resource resolver.  The return value is {@code null} iff
606
     * {@link #unavailabilityCause()} is not null;
607
     */
608
    @Nullable ResourceResolver newResourceResolver();
609

610
    /**
611
     * Returns the reason why the resource resolver cannot be created.  The return value is
612
     * {@code null} if {@link #newResourceResolver()} is suitable for use.
613
     */
614
    @Nullable Throwable unavailabilityCause();
615
  }
616

617
  /**
618
   * AddressResolver resolves a hostname into a list of addresses.
619
   */
620
  @VisibleForTesting
621
  public interface AddressResolver {
622
    List<InetAddress> resolveAddress(String host) throws Exception;
623
  }
624

625
  private enum JdkAddressResolver implements AddressResolver {
1✔
626
    INSTANCE;
1✔
627

628
    @Override
629
    public List<InetAddress> resolveAddress(String host) throws UnknownHostException {
630
      return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host)));
1✔
631
    }
632
  }
633

634
  /**
635
   * {@link ResourceResolver} is a Dns ResourceRecord resolver.
636
   */
637
  @VisibleForTesting
638
  public interface ResourceResolver {
639
    List<String> resolveTxt(String host) throws Exception;
640

641
    List<SrvRecord> resolveSrv(String host) throws Exception;
642
  }
643

644
  @Nullable
645
  protected ResourceResolver getResourceResolver() {
646
    if (!shouldUseJndi(enableJndi, enableJndiLocalhost, host)) {
1✔
647
      return null;
1✔
648
    }
649
    ResourceResolver rr;
650
    if ((rr = resourceResolver.get()) == null) {
1✔
651
      if (resourceResolverFactory != null) {
1✔
652
        assert resourceResolverFactory.unavailabilityCause() == null;
1✔
653
        rr = resourceResolverFactory.newResourceResolver();
1✔
654
      }
655
    }
656
    return rr;
1✔
657
  }
658

659
  @Nullable
660
  @VisibleForTesting
661
  static ResourceResolverFactory getResourceResolverFactory(ClassLoader loader) {
662
    Class<? extends ResourceResolverFactory> jndiClazz;
663
    try {
664
      jndiClazz =
1✔
665
          Class.forName("io.grpc.internal.JndiResourceResolverFactory", true, loader)
1✔
666
              .asSubclass(ResourceResolverFactory.class);
1✔
667
    } catch (ClassNotFoundException e) {
1✔
668
      logger.log(Level.FINE, "Unable to find JndiResourceResolverFactory, skipping.", e);
1✔
669
      return null;
1✔
670
    } catch (ClassCastException e) {
1✔
671
      // This can happen if JndiResourceResolverFactory was removed by something like Proguard
672
      // combined with a broken ClassLoader that prefers classes from the child over the parent
673
      // while also not properly filtering dependencies in the parent that should be hidden. If the
674
      // class loader prefers loading from the parent then ResourceresolverFactory would have also
675
      // been loaded from the parent. If the class loader filtered deps, then
676
      // JndiResourceResolverFactory wouldn't have been found.
677
      logger.log(Level.FINE, "Unable to cast JndiResourceResolverFactory, skipping.", e);
1✔
678
      return null;
1✔
679
    }
1✔
680
    Constructor<? extends ResourceResolverFactory> jndiCtor;
681
    try {
682
      jndiCtor = jndiClazz.getConstructor();
1✔
683
    } catch (Exception e) {
×
684
      logger.log(Level.FINE, "Can't find JndiResourceResolverFactory ctor, skipping.", e);
×
685
      return null;
×
686
    }
1✔
687
    ResourceResolverFactory rrf;
688
    try {
689
      rrf = jndiCtor.newInstance();
1✔
690
    } catch (Exception e) {
×
691
      logger.log(Level.FINE, "Can't construct JndiResourceResolverFactory, skipping.", e);
×
692
      return null;
×
693
    }
1✔
694
    if (rrf.unavailabilityCause() != null) {
1✔
695
      logger.log(
×
696
          Level.FINE,
697
          "JndiResourceResolverFactory not available, skipping.",
698
          rrf.unavailabilityCause());
×
699
      return null;
×
700
    }
701
    return rrf;
1✔
702
  }
703

704
  private static String getLocalHostname() {
705
    if (localHostname == null) {
1✔
706
      try {
707
        localHostname = InetAddress.getLocalHost().getHostName();
1✔
708
      } catch (UnknownHostException e) {
×
709
        throw new RuntimeException(e);
×
710
      }
1✔
711
    }
712
    return localHostname;
1✔
713
  }
714

715
  @VisibleForTesting
716
  protected static boolean shouldUseJndi(
717
      boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) {
718
    if (!jndiEnabled) {
1✔
719
      return false;
1✔
720
    }
721
    if ("localhost".equalsIgnoreCase(target)) {
1✔
722
      return jndiLocalhostEnabled;
1✔
723
    }
724
    // Check if this name looks like IPv6
725
    if (target.contains(":")) {
1✔
726
      return false;
1✔
727
    }
728
    // Check if this might be IPv4.  Such addresses have no alphabetic characters.  This also
729
    // checks the target is empty.
730
    boolean alldigits = true;
1✔
731
    for (int i = 0; i < target.length(); i++) {
1✔
732
      char c = target.charAt(i);
1✔
733
      if (c != '.') {
1✔
734
        alldigits &= (c >= '0' && c <= '9');
1✔
735
      }
736
    }
737
    return !alldigits;
1✔
738
  }
739
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc