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

grpc / grpc-java / #19378

26 Jul 2024 10:13AM UTC coverage: 84.451% (+0.001%) from 84.45%
#19378

push

github

web-flow
Introduce onResult2 in NameResolver Listener2 that returns Status (#11313)

Introducing NameResolver listener method "Status Listener2::onResult2(ResolutionResult)" that returns Status of the acceptance of the name resolution by the load balancer, and the Name Resolver will call this method for both success and error cases.

33239 of 39359 relevant lines covered (84.45%)

0.84 hits per line

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

90.57
/../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.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#onResult(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
  /** Executor that will be used if an Executor is not provide via {@link NameResolver.Args}. */
137
  private final Resource<Executor> executorResource;
138
  private final long cacheTtlNanos;
139
  private final SynchronizationContext syncContext;
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
  /** True if using an executor resource that should be released after use. */
148
  private final boolean usingExecutorResource;
149
  private final ServiceConfigParser serviceConfigParser;
150

151
  private boolean resolving;
152

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

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

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

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

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

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

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

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

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

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

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

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

386
  private void resolve() {
387
    if (resolving || shutdown || !cacheRefreshRequired()) {
1✔
388
      return;
1✔
389
    }
390
    resolving = true;
1✔
391
    executor.execute(new Resolve(listener));
1✔
392
  }
1✔
393

394
  private boolean cacheRefreshRequired() {
395
    return !resolved
1✔
396
        || cacheTtlNanos == 0
397
        || (cacheTtlNanos > 0 && stopwatch.elapsed(TimeUnit.NANOSECONDS) > cacheTtlNanos);
1✔
398
  }
399

400
  @Override
401
  public void shutdown() {
402
    if (shutdown) {
1✔
403
      return;
×
404
    }
405
    shutdown = true;
1✔
406
    if (executor != null && usingExecutorResource) {
1✔
407
      executor = SharedResourceHolder.release(executorResource, executor);
1✔
408
    }
409
  }
1✔
410

411
  final int getPort() {
412
    return port;
1✔
413
  }
414

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

438
  @Nullable
439
  private static final Double getPercentageFromChoice(Map<String, ?> serviceConfigChoice) {
440
    return JsonUtil.getNumberAsDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
1✔
441
  }
442

443
  @Nullable
444
  private static final List<String> getClientLanguagesFromChoice(
445
      Map<String, ?> serviceConfigChoice) {
446
    return JsonUtil.getListOfStrings(
1✔
447
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY);
448
  }
449

450
  @Nullable
451
  private static final List<String> getHostnamesFromChoice(Map<String, ?> serviceConfigChoice) {
452
    return JsonUtil.getListOfStrings(
1✔
453
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY);
454
  }
455

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

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

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

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

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

549
    private InternalResolutionResult() {}
550
  }
551

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

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

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

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

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

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

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

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

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

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

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

629
  private enum JdkAddressResolver implements AddressResolver {
1✔
630
    INSTANCE;
1✔
631

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

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

645
    List<SrvRecord> resolveSrv(String host) throws Exception;
646
  }
647

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

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

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

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

© 2025 Coveralls, Inc