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

grpc / grpc-java / #20192

12 Mar 2026 12:40PM UTC coverage: 88.694% (+0.02%) from 88.675%
#20192

push

github

ejona86
Remove InternalResolutionResult from DnsNameResolver

The internal result was needed before 90d0fabb1 allowed addresses to
fail yet still provide attributes and service config. Now the code can
just use the regular API.

This does cause a behavior change where TXT records are looked up even
if address lookup failed, however that's actually what we wanted to
allow in 90d0fabb1 by adding the new API. Also, the TXT code was added
in 2017 and it's now 2026 yet it is still disabled, so it's unlikely to
matter soon.

35460 of 39980 relevant lines covered (88.69%)

0.89 hits per line

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

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

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

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

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

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

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

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

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

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

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

120
  @VisibleForTesting
121
  final ProxyDetector proxyDetector;
122

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

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

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

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

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

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

146
  private boolean resolving;
147

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

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

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

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

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

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

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

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

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

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

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

261
  /**
262
   * Main logic of name resolution.
263
   */
264
  protected ResolutionResult doResolve() {
265
    ResolutionResult.Builder resultBuilder = ResolutionResult.newBuilder();
1✔
266
    try {
267
      resultBuilder.setAddressesOrError(StatusOr.fromValue(resolveAddresses()));
1✔
268
    } catch (Exception e) {
1✔
269
      logger.log(Level.FINE, "Address resolution failure", e);
1✔
270
      resultBuilder.setAddressesOrError(StatusOr.fromStatus(
1✔
271
          Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e)));
1✔
272
    }
1✔
273
    if (enableTxt) {
1✔
274
      resultBuilder.setServiceConfig(resolveServiceConfig());
1✔
275
    }
276
    return resultBuilder.build();
1✔
277
  }
278

279
  private final class Resolve implements Runnable {
280
    private final Listener2 savedListener;
281

282
    Resolve(Listener2 savedListener) {
1✔
283
      this.savedListener = checkNotNull(savedListener, "savedListener");
1✔
284
    }
1✔
285

286
    @Override
287
    public void run() {
288
      if (logger.isLoggable(Level.FINER)) {
1✔
289
        logger.finer("Attempting DNS resolution of " + host);
×
290
      }
291
      ResolutionResult result = null;
1✔
292
      try {
293
        EquivalentAddressGroup proxiedAddr = detectProxy();
1✔
294
        if (proxiedAddr != null) {
1✔
295
          if (logger.isLoggable(Level.FINER)) {
1✔
296
            logger.finer("Using proxy address " + proxiedAddr);
×
297
          }
298
          result = ResolutionResult.newBuilder()
1✔
299
              .setAddressesOrError(StatusOr.fromValue(Collections.singletonList(proxiedAddr)))
1✔
300
              .build();
1✔
301
        } else {
302
          result = doResolve();
1✔
303
        }
304
        ResolutionResult savedResult = result;
1✔
305
        syncContext.execute(() -> {
1✔
306
          savedListener.onResult2(savedResult);
1✔
307
        });
1✔
308
      } catch (IOException e) {
1✔
309
        syncContext.execute(() ->
1✔
310
            savedListener.onResult2(ResolutionResult.newBuilder()
1✔
311
                .setAddressesOrError(
1✔
312
                    StatusOr.fromStatus(
1✔
313
                        Status.UNAVAILABLE.withDescription(
1✔
314
                            "Unable to resolve host " + host).withCause(e))).build()));
1✔
315
      } finally {
316
        final boolean succeed = result != null && result.getAddressesOrError().hasValue();
1✔
317
        syncContext.execute(new Runnable() {
1✔
318
          @Override
319
          public void run() {
320
            if (succeed) {
1✔
321
              resolved = true;
1✔
322
              if (cacheTtlNanos > 0) {
1✔
323
                stopwatch.reset().start();
1✔
324
              }
325
            }
326
            resolving = false;
1✔
327
          }
1✔
328
        });
329
      }
330
    }
1✔
331
  }
332

333
  @Nullable
334
  static ConfigOrError parseServiceConfig(
335
      List<String> rawTxtRecords, Random random, String localHostname) {
336
    List<Map<String, ?>> possibleServiceConfigChoices;
337
    try {
338
      possibleServiceConfigChoices = parseTxtResults(rawTxtRecords);
1✔
339
    } catch (IOException | RuntimeException e) {
1✔
340
      return ConfigOrError.fromError(
1✔
341
          Status.UNKNOWN.withDescription("failed to parse TXT records").withCause(e));
1✔
342
    }
1✔
343
    Map<String, ?> possibleServiceConfig = null;
1✔
344
    for (Map<String, ?> possibleServiceConfigChoice : possibleServiceConfigChoices) {
1✔
345
      try {
346
        possibleServiceConfig =
1✔
347
            maybeChooseServiceConfig(possibleServiceConfigChoice, random, localHostname);
1✔
348
      } catch (RuntimeException e) {
1✔
349
        return ConfigOrError.fromError(
1✔
350
            Status.UNKNOWN.withDescription("failed to pick service config choice").withCause(e));
1✔
351
      }
1✔
352
      if (possibleServiceConfig != null) {
1✔
353
        break;
1✔
354
      }
355
    }
×
356
    if (possibleServiceConfig == null) {
1✔
357
      return null;
1✔
358
    }
359
    return ConfigOrError.fromConfig(possibleServiceConfig);
1✔
360
  }
361

362
  private void resolve() {
363
    if (resolving || shutdown || !cacheRefreshRequired()) {
1✔
364
      return;
1✔
365
    }
366
    resolving = true;
1✔
367
    executor.execute(new Resolve(listener));
1✔
368
  }
1✔
369

370
  private boolean cacheRefreshRequired() {
371
    return !resolved
1✔
372
        || cacheTtlNanos == 0
373
        || (cacheTtlNanos > 0 && stopwatch.elapsed(TimeUnit.NANOSECONDS) > cacheTtlNanos);
1✔
374
  }
375

376
  @Override
377
  public void shutdown() {
378
    if (shutdown) {
1✔
379
      return;
×
380
    }
381
    shutdown = true;
1✔
382
    if (executor != null) {
1✔
383
      executor = executorPool.returnObject(executor);
1✔
384
    }
385
  }
1✔
386

387
  final int getPort() {
388
    return port;
1✔
389
  }
390

391
  /**
392
   * Parse TXT service config records as JSON.
393
   *
394
   * @throws IOException if one of the txt records contains improperly formatted JSON.
395
   */
396
  @VisibleForTesting
397
  static List<Map<String, ?>> parseTxtResults(List<String> txtRecords) throws IOException {
398
    List<Map<String, ?>> possibleServiceConfigChoices = new ArrayList<>();
1✔
399
    for (String txtRecord : txtRecords) {
1✔
400
      if (!txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
1✔
401
        logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
1✔
402
        continue;
1✔
403
      }
404
      Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
1✔
405
      if (!(rawChoices instanceof List)) {
1✔
406
        throw new ClassCastException("wrong type " + rawChoices);
1✔
407
      }
408
      List<?> listChoices = (List<?>) rawChoices;
1✔
409
      possibleServiceConfigChoices.addAll(JsonUtil.checkObjectList(listChoices));
1✔
410
    }
1✔
411
    return possibleServiceConfigChoices;
1✔
412
  }
413

414
  @Nullable
415
  private static final Double getPercentageFromChoice(Map<String, ?> serviceConfigChoice) {
416
    return JsonUtil.getNumberAsDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
1✔
417
  }
418

419
  @Nullable
420
  private static final List<String> getClientLanguagesFromChoice(
421
      Map<String, ?> serviceConfigChoice) {
422
    return JsonUtil.getListOfStrings(
1✔
423
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY);
424
  }
425

426
  @Nullable
427
  private static final List<String> getHostnamesFromChoice(Map<String, ?> serviceConfigChoice) {
428
    return JsonUtil.getListOfStrings(
1✔
429
        serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY);
430
  }
431

432
  /**
433
   * Returns value of network address cache ttl property if not Android environment. For android,
434
   * DnsNameResolver uses a fixed value.
435
   */
436
  private static long getNetworkAddressCacheTtlNanos(boolean isAndroid) {
437
    if (isAndroid) {
1✔
438
      // On Android, use fixed value. If the network used changes this value shouldn't matter, as
439
      // channel.enterIdle() should be called and this name resolver instance will be discarded. The
440
      // new name resolver instance will then re-request.
441
      return TimeUnit.SECONDS.toNanos(DEFAULT_NETWORK_CACHE_TTL_SECONDS);
1✔
442
    }
443

444
    String cacheTtlPropertyValue = System.getProperty(NETWORKADDRESS_CACHE_TTL_PROPERTY);
1✔
445
    long cacheTtl = DEFAULT_NETWORK_CACHE_TTL_SECONDS;
1✔
446
    if (cacheTtlPropertyValue != null) {
1✔
447
      try {
448
        cacheTtl = Long.parseLong(cacheTtlPropertyValue);
1✔
449
      } catch (NumberFormatException e) {
1✔
450
        logger.log(
1✔
451
            Level.WARNING,
452
            "Property({0}) valid is not valid number format({1}), fall back to default({2})",
453
            new Object[] {NETWORKADDRESS_CACHE_TTL_PROPERTY, cacheTtlPropertyValue, cacheTtl});
1✔
454
      }
1✔
455
    }
456
    return cacheTtl > 0 ? TimeUnit.SECONDS.toNanos(cacheTtl) : cacheTtl;
1✔
457
  }
458

459
  /**
460
   * Determines if a given Service Config choice applies, and if so, returns it.
461
   *
462
   * @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
463
   *     Service Config in DNS</a>
464
   * @param choice The service config choice.
465
   * @return The service config object or {@code null} if this choice does not apply.
466
   */
467
  @Nullable
468
  @VisibleForTesting
469
  static Map<String, ?> maybeChooseServiceConfig(
470
      Map<String, ?> choice, Random random, String hostname) {
471
    for (Map.Entry<String, ?> entry : choice.entrySet()) {
1✔
472
      Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
1✔
473
    }
1✔
474

475
    List<String> clientLanguages = getClientLanguagesFromChoice(choice);
1✔
476
    if (clientLanguages != null && !clientLanguages.isEmpty()) {
1✔
477
      boolean javaPresent = false;
1✔
478
      for (String lang : clientLanguages) {
1✔
479
        if ("java".equalsIgnoreCase(lang)) {
1✔
480
          javaPresent = true;
1✔
481
          break;
1✔
482
        }
483
      }
1✔
484
      if (!javaPresent) {
1✔
485
        return null;
1✔
486
      }
487
    }
488
    Double percentage = getPercentageFromChoice(choice);
1✔
489
    if (percentage != null) {
1✔
490
      int pct = percentage.intValue();
1✔
491
      Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
1✔
492
      if (random.nextInt(100) >= pct) {
1✔
493
        return null;
1✔
494
      }
495
    }
496
    List<String> clientHostnames = getHostnamesFromChoice(choice);
1✔
497
    if (clientHostnames != null && !clientHostnames.isEmpty()) {
1✔
498
      boolean hostnamePresent = false;
1✔
499
      for (String clientHostname : clientHostnames) {
1✔
500
        if (clientHostname.equals(hostname)) {
1✔
501
          hostnamePresent = true;
1✔
502
          break;
1✔
503
        }
504
      }
1✔
505
      if (!hostnamePresent) {
1✔
506
        return null;
1✔
507
      }
508
    }
509
    Map<String, ?> sc =
1✔
510
        JsonUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
1✔
511
    if (sc == null) {
1✔
512
      throw new VerifyException(String.format(
×
513
          "key '%s' missing in '%s'", choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY));
514
    }
515
    return sc;
1✔
516
  }
517

518
  /**
519
   * Describes a parsed SRV record.
520
   */
521
  @VisibleForTesting
522
  public static final class SrvRecord {
523
    public final String host;
524
    public final int port;
525

526
    public SrvRecord(String host, int port) {
1✔
527
      this.host = host;
1✔
528
      this.port = port;
1✔
529
    }
1✔
530

531
    @Override
532
    public int hashCode() {
533
      return Objects.hashCode(host, port);
×
534
    }
535

536
    @Override
537
    public boolean equals(Object obj) {
538
      if (this == obj) {
1✔
539
        return true;
×
540
      }
541
      if (obj == null || getClass() != obj.getClass()) {
1✔
542
        return false;
×
543
      }
544
      SrvRecord that = (SrvRecord) obj;
1✔
545
      return port == that.port && host.equals(that.host);
1✔
546
    }
547

548
    @Override
549
    public String toString() {
550
      return
×
551
          MoreObjects.toStringHelper(this)
×
552
              .add("host", host)
×
553
              .add("port", port)
×
554
              .toString();
×
555
    }
556
  }
557

558
  @VisibleForTesting
559
  protected void setAddressResolver(AddressResolver addressResolver) {
560
    this.addressResolver = addressResolver;
1✔
561
  }
1✔
562

563
  @VisibleForTesting
564
  protected void setResourceResolver(ResourceResolver resourceResolver) {
565
    this.resourceResolver.set(resourceResolver);
1✔
566
  }
1✔
567

568
  /**
569
   * {@link ResourceResolverFactory} is a factory for making resource resolvers.  It supports
570
   * optionally checking if the factory is available.
571
   */
572
  interface ResourceResolverFactory {
573

574
    /**
575
     * Creates a new resource resolver.  The return value is {@code null} iff
576
     * {@link #unavailabilityCause()} is not null;
577
     */
578
    @Nullable ResourceResolver newResourceResolver();
579

580
    /**
581
     * Returns the reason why the resource resolver cannot be created.  The return value is
582
     * {@code null} if {@link #newResourceResolver()} is suitable for use.
583
     */
584
    @Nullable Throwable unavailabilityCause();
585
  }
586

587
  /**
588
   * AddressResolver resolves a hostname into a list of addresses.
589
   */
590
  @VisibleForTesting
591
  public interface AddressResolver {
592
    List<InetAddress> resolveAddress(String host) throws Exception;
593
  }
594

595
  private enum JdkAddressResolver implements AddressResolver {
1✔
596
    INSTANCE;
1✔
597

598
    @Override
599
    public List<InetAddress> resolveAddress(String host) throws UnknownHostException {
600
      return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host)));
1✔
601
    }
602
  }
603

604
  /**
605
   * {@link ResourceResolver} is a Dns ResourceRecord resolver.
606
   */
607
  @VisibleForTesting
608
  public interface ResourceResolver {
609
    List<String> resolveTxt(String host) throws Exception;
610

611
    List<SrvRecord> resolveSrv(String host) throws Exception;
612
  }
613

614
  @Nullable
615
  protected ResourceResolver getResourceResolver() {
616
    if (!shouldUseJndi(enableJndi, enableJndiLocalhost, host)) {
1✔
617
      return null;
1✔
618
    }
619
    ResourceResolver rr;
620
    if ((rr = resourceResolver.get()) == null) {
1✔
621
      if (resourceResolverFactory != null) {
1✔
622
        assert resourceResolverFactory.unavailabilityCause() == null;
1✔
623
        rr = resourceResolverFactory.newResourceResolver();
1✔
624
      }
625
    }
626
    return rr;
1✔
627
  }
628

629
  @Nullable
630
  @VisibleForTesting
631
  static ResourceResolverFactory getResourceResolverFactory(ClassLoader loader) {
632
    Class<? extends ResourceResolverFactory> jndiClazz;
633
    try {
634
      jndiClazz =
1✔
635
          Class.forName("io.grpc.internal.JndiResourceResolverFactory", true, loader)
1✔
636
              .asSubclass(ResourceResolverFactory.class);
1✔
637
    } catch (ClassNotFoundException e) {
1✔
638
      logger.log(Level.FINE, "Unable to find JndiResourceResolverFactory, skipping.", e);
1✔
639
      return null;
1✔
640
    } catch (ClassCastException e) {
1✔
641
      // This can happen if JndiResourceResolverFactory was removed by something like Proguard
642
      // combined with a broken ClassLoader that prefers classes from the child over the parent
643
      // while also not properly filtering dependencies in the parent that should be hidden. If the
644
      // class loader prefers loading from the parent then ResourceresolverFactory would have also
645
      // been loaded from the parent. If the class loader filtered deps, then
646
      // JndiResourceResolverFactory wouldn't have been found.
647
      logger.log(Level.FINE, "Unable to cast JndiResourceResolverFactory, skipping.", e);
1✔
648
      return null;
1✔
649
    }
1✔
650
    Constructor<? extends ResourceResolverFactory> jndiCtor;
651
    try {
652
      jndiCtor = jndiClazz.getConstructor();
1✔
653
    } catch (Exception e) {
×
654
      logger.log(Level.FINE, "Can't find JndiResourceResolverFactory ctor, skipping.", e);
×
655
      return null;
×
656
    }
1✔
657
    ResourceResolverFactory rrf;
658
    try {
659
      rrf = jndiCtor.newInstance();
1✔
660
    } catch (Exception e) {
×
661
      logger.log(Level.FINE, "Can't construct JndiResourceResolverFactory, skipping.", e);
×
662
      return null;
×
663
    }
1✔
664
    if (rrf.unavailabilityCause() != null) {
1✔
665
      logger.log(
×
666
          Level.FINE,
667
          "JndiResourceResolverFactory not available, skipping.",
668
          rrf.unavailabilityCause());
×
669
      return null;
×
670
    }
671
    return rrf;
1✔
672
  }
673

674
  private static String getLocalHostname() {
675
    if (localHostname == null) {
1✔
676
      try {
677
        localHostname = InetAddress.getLocalHost().getHostName();
1✔
678
      } catch (UnknownHostException e) {
×
679
        throw new RuntimeException(e);
×
680
      }
1✔
681
    }
682
    return localHostname;
1✔
683
  }
684

685
  @VisibleForTesting
686
  protected static boolean shouldUseJndi(
687
      boolean jndiEnabled, boolean jndiLocalhostEnabled, String target) {
688
    if (!jndiEnabled) {
1✔
689
      return false;
1✔
690
    }
691
    if ("localhost".equalsIgnoreCase(target)) {
1✔
692
      return jndiLocalhostEnabled;
1✔
693
    }
694
    // Check if this name looks like IPv6
695
    if (target.contains(":")) {
1✔
696
      return false;
1✔
697
    }
698
    // Check if this might be IPv4.  Such addresses have no alphabetic characters.  This also
699
    // checks the target is empty.
700
    boolean alldigits = true;
1✔
701
    for (int i = 0; i < target.length(); i++) {
1✔
702
      char c = target.charAt(i);
1✔
703
      if (c != '.') {
1✔
704
        alldigits &= (c >= '0' && c <= '9');
1✔
705
      }
706
    }
707
    return !alldigits;
1✔
708
  }
709
}
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