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

grpc / grpc-java / #20191

11 Mar 2026 05:19PM UTC coverage: 88.675% (-0.01%) from 88.687%
#20191

push

github

ejona86
core: Enable dns "caching" on Android

DnsNameResolver discards refresh requests if it has been too soon after
the last refresh, because the result is assumed to be identical to the
previous fetch. Android itself will adhere to the RR's TTL, so
requesting too frequently shouldn't have been causing too much I/O, but
it could be causing extra CPU usage. Having some lower limit will reduce
the number of useless address updates into the LB tree.

30 seconds is the same as regular Java and Go/C++ (which copied Java as
a "reasonable" value). Note that other languages _delay_ the refresh
instead of _discarding_ the refresh, but there's no reason why the
existing discard behavior would cause much problem on Android vs normal
Java. Chrome apparently uses 1 minute, so this really looks like it
shouldn't cause problems as long as AndroidChannelBuilder is being used.

35462 of 39991 relevant lines covered (88.67%)

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 uses a fixed value; 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 uses a fixed value.
455
   */
456
  private static long getNetworkAddressCacheTtlNanos(boolean isAndroid) {
457
    if (isAndroid) {
1✔
458
      // On Android, use fixed value. If the network used changes this value shouldn't matter, as
459
      // channel.enterIdle() should be called and this name resolver instance will be discarded. The
460
      // new name resolver instance will then re-request.
461
      return TimeUnit.SECONDS.toNanos(DEFAULT_NETWORK_CACHE_TTL_SECONDS);
1✔
462
    }
463

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

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

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

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

547
    private InternalResolutionResult() {}
548
  }
549

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

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

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

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

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

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

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

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

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

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

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

627
  private enum JdkAddressResolver implements AddressResolver {
1✔
628
    INSTANCE;
1✔
629

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

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

643
    List<SrvRecord> resolveSrv(String host) throws Exception;
644
  }
645

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

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

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

717
  @VisibleForTesting
718
  protected static boolean shouldUseJndi(
719
      boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) {
720
    if (!jndiEnabled) {
1✔
721
      return false;
1✔
722
    }
723
    if ("localhost".equalsIgnoreCase(target)) {
1✔
724
      return jndiLocalhostEnabled;
1✔
725
    }
726
    // Check if this name looks like IPv6
727
    if (target.contains(":")) {
1✔
728
      return false;
1✔
729
    }
730
    // Check if this might be IPv4.  Such addresses have no alphabetic characters.  This also
731
    // checks the target is empty.
732
    boolean alldigits = true;
1✔
733
    for (int i = 0; i < target.length(); i++) {
1✔
734
      char c = target.charAt(i);
1✔
735
      if (c != '.') {
1✔
736
        alldigits &= (c >= '0' && c <= '9');
1✔
737
      }
738
    }
739
    return !alldigits;
1✔
740
  }
741
}
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