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

grpc / grpc-java / #19497

07 Oct 2024 12:25PM UTC coverage: 84.656% (+0.06%) from 84.599%
#19497

push

github

web-flow
On result2 resolution result have addresses or error (#11330)

Combined success / error status passed via ResolutionResult to the NameResolver.Listener2 interface's onResult2 method - Addresses in the success case or address resolution error in the failure case now get set in ResolutionResult::addressesOrError by the internal name resolvers.

33771 of 39892 relevant lines covered (84.66%)

0.85 hits per line

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

91.83
/../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.Throwables;
27
import com.google.common.base.Verify;
28
import com.google.common.base.VerifyException;
29
import io.grpc.Attributes;
30
import io.grpc.EquivalentAddressGroup;
31
import io.grpc.NameResolver;
32
import io.grpc.ProxiedSocketAddress;
33
import io.grpc.ProxyDetector;
34
import io.grpc.Status;
35
import io.grpc.StatusOr;
36
import io.grpc.SynchronizationContext;
37
import io.grpc.internal.SharedResourceHolder.Resource;
38
import java.io.IOException;
39
import java.lang.reflect.Constructor;
40
import java.net.InetAddress;
41
import java.net.InetSocketAddress;
42
import java.net.URI;
43
import java.net.UnknownHostException;
44
import java.util.ArrayList;
45
import java.util.Arrays;
46
import java.util.Collections;
47
import java.util.HashSet;
48
import java.util.List;
49
import java.util.Map;
50
import java.util.Random;
51
import java.util.Set;
52
import java.util.concurrent.Executor;
53
import java.util.concurrent.TimeUnit;
54
import java.util.concurrent.atomic.AtomicReference;
55
import java.util.logging.Level;
56
import java.util.logging.Logger;
57
import javax.annotation.Nullable;
58

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

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

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

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

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

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

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

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

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

122
  @VisibleForTesting
123
  final ProxyDetector proxyDetector;
124

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

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

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

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

137
  /** Executor that will be used if an Executor is not provide via {@link NameResolver.Args}. */
138
  private final Resource<Executor> executorResource;
139
  private final long cacheTtlNanos;
140
  private final SynchronizationContext syncContext;
141

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

148
  /** True if using an executor resource that should be released after use. */
149
  private final boolean usingExecutorResource;
150
  private final ServiceConfigParser serviceConfigParser;
151

152
  private boolean resolving;
153

154
  // The field must be accessed from syncContext, although the methods on an Listener2 can be called
155
  // from any thread.
156
  private NameResolver.Listener2 listener;
157

158
  protected DnsNameResolver(
159
      @Nullable String nsAuthority,
160
      String name,
161
      Args args,
162
      Resource<Executor> executorResource,
163
      Stopwatch stopwatch,
164
      boolean isAndroid) {
1✔
165
    checkNotNull(args, "args");
1✔
166
    // TODO: if a DNS server is provided as nsAuthority, use it.
167
    // https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java
168
    this.executorResource = executorResource;
1✔
169
    // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
170
    // opaque URI, thus the authority and host of the resulted URI would be null.
171
    URI nameUri = URI.create("//" + checkNotNull(name, "name"));
1✔
172
    Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name);
1✔
173
    authority = Preconditions.checkNotNull(nameUri.getAuthority(),
1✔
174
        "nameUri (%s) doesn't have an authority", nameUri);
175
    host = nameUri.getHost();
1✔
176
    if (nameUri.getPort() == -1) {
1✔
177
      port = args.getDefaultPort();
1✔
178
    } else {
179
      port = nameUri.getPort();
1✔
180
    }
181
    this.proxyDetector = checkNotNull(args.getProxyDetector(), "proxyDetector");
1✔
182
    this.cacheTtlNanos = getNetworkAddressCacheTtlNanos(isAndroid);
1✔
183
    this.stopwatch = checkNotNull(stopwatch, "stopwatch");
1✔
184
    this.syncContext = checkNotNull(args.getSynchronizationContext(), "syncContext");
1✔
185
    this.executor = args.getOffloadExecutor();
1✔
186
    this.usingExecutorResource = executor == null;
1✔
187
    this.serviceConfigParser = checkNotNull(args.getServiceConfigParser(), "serviceConfigParser");
1✔
188
  }
1✔
189

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

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

200
  @Override
201
  public void start(Listener2 listener) {
202
    Preconditions.checkState(this.listener == null, "already started");
1✔
203
    if (usingExecutorResource) {
1✔
204
      executor = SharedResourceHolder.get(executorResource);
1✔
205
    }
206
    this.listener = checkNotNull(listener, "listener");
1✔
207
    resolve();
1✔
208
  }
1✔
209

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

216
  private List<EquivalentAddressGroup> resolveAddresses() {
217
    List<? extends InetAddress> addresses;
218
    Exception addressesException = null;
1✔
219
    try {
220
      addresses = addressResolver.resolveAddress(host);
1✔
221
    } catch (Exception e) {
1✔
222
      addressesException = e;
1✔
223
      Throwables.throwIfUnchecked(e);
1✔
224
      throw new RuntimeException(e);
1✔
225
    } finally {
226
      if (addressesException != null) {
1✔
227
        logger.log(Level.FINE, "Address resolution failure", addressesException);
1✔
228
      }
229
    }
230
    // Each address forms an EAG
231
    List<EquivalentAddressGroup> servers = new ArrayList<>(addresses.size());
1✔
232
    for (InetAddress inetAddr : addresses) {
1✔
233
      servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
1✔
234
    }
1✔
235
    return Collections.unmodifiableList(servers);
1✔
236
  }
237

238
  @Nullable
239
  private ConfigOrError resolveServiceConfig() {
240
    List<String> txtRecords = Collections.emptyList();
1✔
241
    ResourceResolver resourceResolver = getResourceResolver();
1✔
242
    if (resourceResolver != null) {
1✔
243
      try {
244
        txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + host);
1✔
245
      } catch (Exception e) {
1✔
246
        logger.log(Level.FINE, "ServiceConfig resolution failure", e);
1✔
247
      }
1✔
248
    }
249
    if (!txtRecords.isEmpty()) {
1✔
250
      ConfigOrError rawServiceConfig = parseServiceConfig(txtRecords, random, getLocalHostname());
1✔
251
      if (rawServiceConfig != null) {
1✔
252
        if (rawServiceConfig.getError() != null) {
1✔
253
          return ConfigOrError.fromError(rawServiceConfig.getError());
1✔
254
        }
255

256
        @SuppressWarnings("unchecked")
257
        Map<String, ?> verifiedRawServiceConfig = (Map<String, ?>) rawServiceConfig.getConfig();
1✔
258
        return serviceConfigParser.parseServiceConfig(verifiedRawServiceConfig);
1✔
259
      }
260
    } else {
×
261
      logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host});
1✔
262
    }
263
    return null;
1✔
264
  }
265

266
  @Nullable
267
  private EquivalentAddressGroup detectProxy() throws IOException {
268
    InetSocketAddress destination =
1✔
269
        InetSocketAddress.createUnresolved(host, port);
1✔
270
    ProxiedSocketAddress proxiedAddr = proxyDetector.proxyFor(destination);
1✔
271
    if (proxiedAddr != null) {
1✔
272
      return new EquivalentAddressGroup(proxiedAddr);
1✔
273
    }
274
    return null;
1✔
275
  }
276

277
  /**
278
   * Main logic of name resolution.
279
   */
280
  protected InternalResolutionResult doResolve(boolean forceTxt) {
281
    InternalResolutionResult result = new InternalResolutionResult();
1✔
282
    try {
283
      result.addresses = resolveAddresses();
1✔
284
    } catch (Exception e) {
1✔
285
      if (!forceTxt) {
1✔
286
        result.error =
1✔
287
            Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e);
1✔
288
        return result;
1✔
289
      }
290
    }
1✔
291
    if (enableTxt) {
1✔
292
      result.config = resolveServiceConfig();
1✔
293
    }
294
    return result;
1✔
295
  }
296

297
  private final class Resolve implements Runnable {
298
    private final Listener2 savedListener;
299

300
    Resolve(Listener2 savedListener) {
1✔
301
      this.savedListener = checkNotNull(savedListener, "savedListener");
1✔
302
    }
1✔
303

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

367
  @Nullable
368
  static ConfigOrError parseServiceConfig(
369
      List<String> rawTxtRecords, Random random, String localHostname) {
370
    List<Map<String, ?>> possibleServiceConfigChoices;
371
    try {
372
      possibleServiceConfigChoices = parseTxtResults(rawTxtRecords);
1✔
373
    } catch (IOException | RuntimeException e) {
1✔
374
      return ConfigOrError.fromError(
1✔
375
          Status.UNKNOWN.withDescription("failed to parse TXT records").withCause(e));
1✔
376
    }
1✔
377
    Map<String, ?> possibleServiceConfig = null;
1✔
378
    for (Map<String, ?> possibleServiceConfigChoice : possibleServiceConfigChoices) {
1✔
379
      try {
380
        possibleServiceConfig =
1✔
381
            maybeChooseServiceConfig(possibleServiceConfigChoice, random, localHostname);
1✔
382
      } catch (RuntimeException e) {
1✔
383
        return ConfigOrError.fromError(
1✔
384
            Status.UNKNOWN.withDescription("failed to pick service config choice").withCause(e));
1✔
385
      }
1✔
386
      if (possibleServiceConfig != null) {
1✔
387
        break;
1✔
388
      }
389
    }
×
390
    if (possibleServiceConfig == null) {
1✔
391
      return null;
1✔
392
    }
393
    return ConfigOrError.fromConfig(possibleServiceConfig);
1✔
394
  }
395

396
  private void resolve() {
397
    if (resolving || shutdown || !cacheRefreshRequired()) {
1✔
398
      return;
1✔
399
    }
400
    resolving = true;
1✔
401
    executor.execute(new Resolve(listener));
1✔
402
  }
1✔
403

404
  private boolean cacheRefreshRequired() {
405
    return !resolved
1✔
406
        || cacheTtlNanos == 0
407
        || (cacheTtlNanos > 0 && stopwatch.elapsed(TimeUnit.NANOSECONDS) > cacheTtlNanos);
1✔
408
  }
409

410
  @Override
411
  public void shutdown() {
412
    if (shutdown) {
1✔
413
      return;
×
414
    }
415
    shutdown = true;
1✔
416
    if (executor != null && usingExecutorResource) {
1✔
417
      executor = SharedResourceHolder.release(executorResource, executor);
1✔
418
    }
419
  }
1✔
420

421
  final int getPort() {
422
    return port;
1✔
423
  }
424

425
  /**
426
   * Parse TXT service config records as JSON.
427
   *
428
   * @throws IOException if one of the txt records contains improperly formatted JSON.
429
   */
430
  @VisibleForTesting
431
  static List<Map<String, ?>> parseTxtResults(List<String> txtRecords) throws IOException {
432
    List<Map<String, ?>> possibleServiceConfigChoices = new ArrayList<>();
1✔
433
    for (String txtRecord : txtRecords) {
1✔
434
      if (!txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
1✔
435
        logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
1✔
436
        continue;
1✔
437
      }
438
      Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
1✔
439
      if (!(rawChoices instanceof List)) {
1✔
440
        throw new ClassCastException("wrong type " + rawChoices);
1✔
441
      }
442
      List<?> listChoices = (List<?>) rawChoices;
1✔
443
      possibleServiceConfigChoices.addAll(JsonUtil.checkObjectList(listChoices));
1✔
444
    }
1✔
445
    return possibleServiceConfigChoices;
1✔
446
  }
447

448
  @Nullable
449
  private static final Double getPercentageFromChoice(Map<String, ?> serviceConfigChoice) {
450
    return JsonUtil.getNumberAsDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
1✔
451
  }
452

453
  @Nullable
454
  private static final List<String> getClientLanguagesFromChoice(
455
      Map<String, ?> serviceConfigChoice) {
456
    return JsonUtil.getListOfStrings(
1✔
457
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY);
458
  }
459

460
  @Nullable
461
  private static final List<String> getHostnamesFromChoice(Map<String, ?> serviceConfigChoice) {
462
    return JsonUtil.getListOfStrings(
1✔
463
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY);
464
  }
465

466
  /**
467
   * Returns value of network address cache ttl property if not Android environment. For android,
468
   * DnsNameResolver does not cache the dns lookup result.
469
   */
470
  private static long getNetworkAddressCacheTtlNanos(boolean isAndroid) {
471
    if (isAndroid) {
1✔
472
      // on Android, ignore dns cache.
473
      return 0;
1✔
474
    }
475

476
    String cacheTtlPropertyValue = System.getProperty(NETWORKADDRESS_CACHE_TTL_PROPERTY);
1✔
477
    long cacheTtl = DEFAULT_NETWORK_CACHE_TTL_SECONDS;
1✔
478
    if (cacheTtlPropertyValue != null) {
1✔
479
      try {
480
        cacheTtl = Long.parseLong(cacheTtlPropertyValue);
1✔
481
      } catch (NumberFormatException e) {
1✔
482
        logger.log(
1✔
483
            Level.WARNING,
484
            "Property({0}) valid is not valid number format({1}), fall back to default({2})",
485
            new Object[] {NETWORKADDRESS_CACHE_TTL_PROPERTY, cacheTtlPropertyValue, cacheTtl});
1✔
486
      }
1✔
487
    }
488
    return cacheTtl > 0 ? TimeUnit.SECONDS.toNanos(cacheTtl) : cacheTtl;
1✔
489
  }
490

491
  /**
492
   * Determines if a given Service Config choice applies, and if so, returns it.
493
   *
494
   * @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
495
   *   Service Config in DNS</a>
496
   * @param choice The service config choice.
497
   * @return The service config object or {@code null} if this choice does not apply.
498
   */
499
  @Nullable
500
  @VisibleForTesting
501
  static Map<String, ?> maybeChooseServiceConfig(
502
      Map<String, ?> choice, Random random, String hostname) {
503
    for (Map.Entry<String, ?> entry : choice.entrySet()) {
1✔
504
      Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
1✔
505
    }
1✔
506

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

550
  /**
551
   * Used as a DNS-based name resolver's internal representation of resolution result.
552
   */
553
  protected static final class InternalResolutionResult {
554
    private Status error;
555
    private List<EquivalentAddressGroup> addresses;
556
    private ConfigOrError config;
557
    public Attributes attributes;
558

559
    private InternalResolutionResult() {}
560
  }
561

562
  /**
563
   * Describes a parsed SRV record.
564
   */
565
  @VisibleForTesting
566
  public static final class SrvRecord {
567
    public final String host;
568
    public final int port;
569

570
    public SrvRecord(String host, int port) {
1✔
571
      this.host = host;
1✔
572
      this.port = port;
1✔
573
    }
1✔
574

575
    @Override
576
    public int hashCode() {
577
      return Objects.hashCode(host, port);
×
578
    }
579

580
    @Override
581
    public boolean equals(Object obj) {
582
      if (this == obj) {
1✔
583
        return true;
×
584
      }
585
      if (obj == null || getClass() != obj.getClass()) {
1✔
586
        return false;
×
587
      }
588
      SrvRecord that = (SrvRecord) obj;
1✔
589
      return port == that.port && host.equals(that.host);
1✔
590
    }
591

592
    @Override
593
    public String toString() {
594
      return
×
595
          MoreObjects.toStringHelper(this)
×
596
              .add("host", host)
×
597
              .add("port", port)
×
598
              .toString();
×
599
    }
600
  }
601

602
  @VisibleForTesting
603
  protected void setAddressResolver(AddressResolver addressResolver) {
604
    this.addressResolver = addressResolver;
1✔
605
  }
1✔
606

607
  @VisibleForTesting
608
  protected void setResourceResolver(ResourceResolver resourceResolver) {
609
    this.resourceResolver.set(resourceResolver);
1✔
610
  }
1✔
611

612
  /**
613
   * {@link ResourceResolverFactory} is a factory for making resource resolvers.  It supports
614
   * optionally checking if the factory is available.
615
   */
616
  interface ResourceResolverFactory {
617

618
    /**
619
     * Creates a new resource resolver.  The return value is {@code null} iff
620
     * {@link #unavailabilityCause()} is not null;
621
     */
622
    @Nullable ResourceResolver newResourceResolver();
623

624
    /**
625
     * Returns the reason why the resource resolver cannot be created.  The return value is
626
     * {@code null} if {@link #newResourceResolver()} is suitable for use.
627
     */
628
    @Nullable Throwable unavailabilityCause();
629
  }
630

631
  /**
632
   * AddressResolver resolves a hostname into a list of addresses.
633
   */
634
  @VisibleForTesting
635
  public interface AddressResolver {
636
    List<InetAddress> resolveAddress(String host) throws Exception;
637
  }
638

639
  private enum JdkAddressResolver implements AddressResolver {
1✔
640
    INSTANCE;
1✔
641

642
    @Override
643
    public List<InetAddress> resolveAddress(String host) throws UnknownHostException {
644
      return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host)));
1✔
645
    }
646
  }
647

648
  /**
649
   * {@link ResourceResolver} is a Dns ResourceRecord resolver.
650
   */
651
  @VisibleForTesting
652
  public interface ResourceResolver {
653
    List<String> resolveTxt(String host) throws Exception;
654

655
    List<SrvRecord> resolveSrv(String host) throws Exception;
656
  }
657

658
  @Nullable
659
  protected ResourceResolver getResourceResolver() {
660
    if (!shouldUseJndi(enableJndi, enableJndiLocalhost, host)) {
1✔
661
      return null;
1✔
662
    }
663
    ResourceResolver rr;
664
    if ((rr = resourceResolver.get()) == null) {
1✔
665
      if (resourceResolverFactory != null) {
1✔
666
        assert resourceResolverFactory.unavailabilityCause() == null;
1✔
667
        rr = resourceResolverFactory.newResourceResolver();
1✔
668
      }
669
    }
670
    return rr;
1✔
671
  }
672

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

718
  private static String getLocalHostname() {
719
    if (localHostname == null) {
1✔
720
      try {
721
        localHostname = InetAddress.getLocalHost().getHostName();
1✔
722
      } catch (UnknownHostException e) {
×
723
        throw new RuntimeException(e);
×
724
      }
1✔
725
    }
726
    return localHostname;
1✔
727
  }
728

729
  @VisibleForTesting
730
  protected static boolean shouldUseJndi(
731
      boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) {
732
    if (!jndiEnabled) {
1✔
733
      return false;
1✔
734
    }
735
    if ("localhost".equalsIgnoreCase(target)) {
1✔
736
      return jndiLocalhostEnabled;
1✔
737
    }
738
    // Check if this name looks like IPv6
739
    if (target.contains(":")) {
1✔
740
      return false;
1✔
741
    }
742
    // Check if this might be IPv4.  Such addresses have no alphabetic characters.  This also
743
    // checks the target is empty.
744
    boolean alldigits = true;
1✔
745
    for (int i = 0; i < target.length(); i++) {
1✔
746
      char c = target.charAt(i);
1✔
747
      if (c != '.') {
1✔
748
        alldigits &= (c >= '0' && c <= '9');
1✔
749
      }
750
    }
751
    return !alldigits;
1✔
752
  }
753
}
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