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

grpc / grpc-java / #20170

09 Feb 2026 10:46AM UTC coverage: 88.695% (+0.001%) from 88.694%
#20170

push

github

web-flow
fix(xds): Allow and normalize trailing dot (FQDN) in matchHostName (#12644)

## Summary

`matchHostName` in `RoutingUtils` and `XdsNameResolver` currently
rejects hostnames and patterns
with a trailing dot (`.`) via `checkArgument`. A trailing dot denotes a
**Fully Qualified Domain Name (FQDN)** as defined in
[RFC 1034 Section
3.1](https://www.rfc-editor.org/rfc/rfc1034#section-3.1), and is a
valid,
well-defined representation of an absolute domain name. Rejecting it is
inconsistent with the RFC.

This change removes the trailing-dot rejection and adds normalization to
strip the trailing dot
before matching, making `example.com.` and `example.com` match
equivalently.

## Background

Per [RFC 1034 Section
3.1](https://www.rfc-editor.org/rfc/rfc1034#section-3.1):

> "If the name ends with a dot, it is an absolute name ... For example,
`poneria.ISI.EDU.`"

A trailing dot simply indicates that the name is rooted at the DNS root
and is semantically
equivalent to the same name without the trailing dot. Treating it as
invalid prevents legitimate
FQDNs from being used as hostnames or virtual host domain patterns in
xDS routing configuration.

## Motivation

This was discovered when using gRPC Proxyless Service Mesh on a
Kubernetes cluster with Istio.
The issue surfaced after upgrading Istio from 1.26.8 to 1.28.3. The
Istio change
[istio/istio#56008](https://github.com/istio/istio/pull/56008) began
sending FQDN-style domain
names (with trailing dots) in xDS route configuration, which caused
grpc-java to throw an
`IllegalArgumentException` in `matchHostName`:

```text
java.lang.IllegalArgumentException: Invalid pattern/domain name
    at com.google.common.base.Preconditions.checkArgument(Preconditions.java:143)
```

The root cause is that grpc-java's `matchHostName` was not RFC-compliant
in rejecting trailing dots — the Istio upgrade merely made it visible.
The fix here is to bring grpc-java into compliance with RFC 1034,
independent of any specific Istio versi... (continued)

35391 of 39902 relevant lines covered (88.69%)

0.89 hits per line

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

92.11
/../xds/src/main/java/io/grpc/xds/XdsNameResolver.java
1
/*
2
 * Copyright 2019 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.xds;
18

19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static io.grpc.xds.client.Bootstrapper.XDSTP_SCHEME;
22

23
import com.google.common.annotations.VisibleForTesting;
24
import com.google.common.base.Joiner;
25
import com.google.common.base.Strings;
26
import com.google.common.collect.ImmutableList;
27
import com.google.common.collect.ImmutableMap;
28
import com.google.common.collect.Sets;
29
import com.google.gson.Gson;
30
import com.google.protobuf.util.Durations;
31
import io.grpc.Attributes;
32
import io.grpc.CallOptions;
33
import io.grpc.Channel;
34
import io.grpc.ClientCall;
35
import io.grpc.ClientInterceptor;
36
import io.grpc.ClientInterceptors;
37
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
38
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
39
import io.grpc.InternalConfigSelector;
40
import io.grpc.InternalLogId;
41
import io.grpc.LoadBalancer.PickSubchannelArgs;
42
import io.grpc.Metadata;
43
import io.grpc.MethodDescriptor;
44
import io.grpc.MetricRecorder;
45
import io.grpc.NameResolver;
46
import io.grpc.Status;
47
import io.grpc.Status.Code;
48
import io.grpc.StatusOr;
49
import io.grpc.SynchronizationContext;
50
import io.grpc.internal.GrpcUtil;
51
import io.grpc.internal.ObjectPool;
52
import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
53
import io.grpc.xds.Filter.FilterConfig;
54
import io.grpc.xds.Filter.NamedFilterConfig;
55
import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig;
56
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
57
import io.grpc.xds.VirtualHost.Route;
58
import io.grpc.xds.VirtualHost.Route.RouteAction;
59
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
60
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
61
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
62
import io.grpc.xds.VirtualHost.Route.RouteMatch;
63
import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
64
import io.grpc.xds.client.Bootstrapper.AuthorityInfo;
65
import io.grpc.xds.client.Bootstrapper.BootstrapInfo;
66
import io.grpc.xds.client.XdsClient;
67
import io.grpc.xds.client.XdsInitializationException;
68
import io.grpc.xds.client.XdsLogger;
69
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
70
import java.net.URI;
71
import java.util.ArrayList;
72
import java.util.Collections;
73
import java.util.HashMap;
74
import java.util.HashSet;
75
import java.util.List;
76
import java.util.Map;
77
import java.util.Objects;
78
import java.util.Set;
79
import java.util.concurrent.ConcurrentHashMap;
80
import java.util.concurrent.ConcurrentMap;
81
import java.util.concurrent.ScheduledExecutorService;
82
import java.util.concurrent.atomic.AtomicInteger;
83
import java.util.function.Supplier;
84
import javax.annotation.Nullable;
85

86
/**
87
 * A {@link NameResolver} for resolving gRPC target names with "xds:" scheme.
88
 *
89
 * <p>Resolving a gRPC target involves contacting the control plane management server via xDS
90
 * protocol to retrieve service information and produce a service config to the caller.
91
 *
92
 * @see XdsNameResolverProvider
93
 */
94
final class XdsNameResolver extends NameResolver {
95
  static final CallOptions.Key<String> CLUSTER_SELECTION_KEY =
1✔
96
      CallOptions.Key.create("io.grpc.xds.CLUSTER_SELECTION_KEY");
1✔
97
  static final CallOptions.Key<XdsConfig> XDS_CONFIG_CALL_OPTION_KEY =
1✔
98
      CallOptions.Key.create("io.grpc.xds.XDS_CONFIG_CALL_OPTION_KEY");
1✔
99
  static final CallOptions.Key<Long> RPC_HASH_KEY =
1✔
100
      CallOptions.Key.create("io.grpc.xds.RPC_HASH_KEY");
1✔
101
  static final CallOptions.Key<Boolean> AUTO_HOST_REWRITE_KEY =
1✔
102
      CallOptions.Key.create("io.grpc.xds.AUTO_HOST_REWRITE_KEY");
1✔
103
  @VisibleForTesting
104
  static boolean enableTimeout =
1✔
105
      Strings.isNullOrEmpty(System.getenv("GRPC_XDS_EXPERIMENTAL_ENABLE_TIMEOUT"))
1✔
106
          || Boolean.parseBoolean(System.getenv("GRPC_XDS_EXPERIMENTAL_ENABLE_TIMEOUT"));
1✔
107

108
  private final InternalLogId logId;
109
  private final XdsLogger logger;
110
  @Nullable
111
  private final String targetAuthority;
112
  private final String target;
113
  private final String serviceAuthority;
114
  // Encoded version of the service authority as per 
115
  // https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.
116
  private final String encodedServiceAuthority;
117
  private final String overrideAuthority;
118
  private final ServiceConfigParser serviceConfigParser;
119
  private final SynchronizationContext syncContext;
120
  private final ScheduledExecutorService scheduler;
121
  private final XdsClientPool xdsClientPool;
122
  private final ThreadSafeRandom random;
123
  private final FilterRegistry filterRegistry;
124
  private final XxHash64 hashFunc = XxHash64.INSTANCE;
1✔
125
  // Clusters (with reference counts) to which new/existing requests can be/are routed.
126
  // put()/remove() must be called in SyncContext, and get() can be called in any thread.
127
  private final ConcurrentMap<String, ClusterRefState> clusterRefs = new ConcurrentHashMap<>();
1✔
128
  private final ConfigSelector configSelector = new ConfigSelector();
1✔
129
  private final long randomChannelId;
130
  private final Args nameResolverArgs;
131
  // Must be accessed in syncContext.
132
  // Filter instances are unique per channel, and per filter (name+typeUrl).
133
  // NamedFilterConfig.filterStateKey -> filter_instance.
134
  private final HashMap<String, Filter> activeFilters = new HashMap<>();
1✔
135

136
  private volatile RoutingConfig routingConfig;
137
  private Listener2 listener;
138
  private XdsClient xdsClient;
139
  private CallCounterProvider callCounterProvider;
140
  private ResolveState resolveState;
141

142
  XdsNameResolver(
143
      URI targetUri, String name, @Nullable String overrideAuthority,
144
      ServiceConfigParser serviceConfigParser,
145
      SynchronizationContext syncContext, ScheduledExecutorService scheduler,
146
      @Nullable Map<String, ?> bootstrapOverride,
147
      MetricRecorder metricRecorder, Args nameResolverArgs) {
148
    this(targetUri, targetUri.getAuthority(), name, overrideAuthority, serviceConfigParser,
1✔
149
        syncContext, scheduler,
150
        bootstrapOverride == null
1✔
151
          ? SharedXdsClientPoolProvider.getDefaultProvider()
1✔
152
          : new SharedXdsClientPoolProvider(),
1✔
153
        ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride,
1✔
154
        metricRecorder, nameResolverArgs);
155
  }
1✔
156

157
  @VisibleForTesting
158
  XdsNameResolver(
159
      URI targetUri, @Nullable String targetAuthority, String name,
160
      @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser,
161
      SynchronizationContext syncContext, ScheduledExecutorService scheduler,
162
      XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random,
163
      FilterRegistry filterRegistry, @Nullable Map<String, ?> bootstrapOverride,
164
      MetricRecorder metricRecorder, Args nameResolverArgs) {
1✔
165
    this.targetAuthority = targetAuthority;
1✔
166
    target = targetUri.toString();
1✔
167

168
    // The name might have multiple slashes so encode it before verifying.
169
    serviceAuthority = checkNotNull(name, "name");
1✔
170
    this.encodedServiceAuthority = 
1✔
171
      GrpcUtil.checkAuthority(GrpcUtil.AuthorityEscaper.encodeAuthority(serviceAuthority));
1✔
172

173
    this.overrideAuthority = overrideAuthority;
1✔
174
    this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser");
1✔
175
    this.syncContext = checkNotNull(syncContext, "syncContext");
1✔
176
    this.scheduler = checkNotNull(scheduler, "scheduler");
1✔
177
    Supplier<XdsClient> xdsClientSupplierArg =
1✔
178
        nameResolverArgs.getArg(XdsNameResolverProvider.XDS_CLIENT_SUPPLIER);
1✔
179
    if (xdsClientSupplierArg != null) {
1✔
180
      this.xdsClientPool = new SupplierXdsClientPool(xdsClientSupplierArg);
×
181
    } else {
182
      checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory");
1✔
183
      this.xdsClientPool = new BootstrappingXdsClientPool(
1✔
184
          xdsClientPoolFactory, target, bootstrapOverride, metricRecorder);
185
    }
186
    this.random = checkNotNull(random, "random");
1✔
187
    this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry");
1✔
188
    this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs");
1✔
189

190
    randomChannelId = random.nextLong();
1✔
191
    logId = InternalLogId.allocate("xds-resolver", name);
1✔
192
    logger = XdsLogger.withLogId(logId);
1✔
193
    logger.log(XdsLogLevel.INFO, "Created resolver for {0}", name);
1✔
194
  }
1✔
195

196
  @Override
197
  public String getServiceAuthority() {
198
    return encodedServiceAuthority;
1✔
199
  }
200

201
  @Override
202
  public void start(Listener2 listener) {
203
    this.listener = checkNotNull(listener, "listener");
1✔
204
    try {
205
      xdsClient = xdsClientPool.getObject();
1✔
206
    } catch (Exception e) {
1✔
207
      listener.onError(
1✔
208
          Status.UNAVAILABLE.withDescription("Failed to initialize xDS").withCause(e));
1✔
209
      return;
1✔
210
    }
1✔
211
    BootstrapInfo bootstrapInfo = xdsClient.getBootstrapInfo();
1✔
212
    String listenerNameTemplate;
213
    if (targetAuthority == null) {
1✔
214
      listenerNameTemplate = bootstrapInfo.clientDefaultListenerResourceNameTemplate();
1✔
215
    } else {
216
      AuthorityInfo authorityInfo = bootstrapInfo.authorities().get(targetAuthority);
1✔
217
      if (authorityInfo == null) {
1✔
218
        listener.onError(Status.INVALID_ARGUMENT.withDescription(
1✔
219
            "invalid target URI: target authority not found in the bootstrap"));
220
        return;
1✔
221
      }
222
      listenerNameTemplate = authorityInfo.clientListenerResourceNameTemplate();
1✔
223
    }
224
    String replacement = serviceAuthority;
1✔
225
    if (listenerNameTemplate.startsWith(XDSTP_SCHEME)) {
1✔
226
      replacement = XdsClient.percentEncodePath(replacement);
1✔
227
    }
228
    String ldsResourceName = expandPercentS(listenerNameTemplate, replacement);
1✔
229
    if (!XdsClient.isResourceNameValid(ldsResourceName, XdsListenerResource.getInstance().typeUrl())
1✔
230
        ) {
231
      listener.onError(Status.INVALID_ARGUMENT.withDescription(
×
232
          "invalid listener resource URI for service authority: " + serviceAuthority));
233
      return;
×
234
    }
235
    ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName);
1✔
236
    callCounterProvider = SharedCallCounterMap.getInstance();
1✔
237

238
    resolveState = new ResolveState(ldsResourceName);
1✔
239
    resolveState.start();
1✔
240
  }
1✔
241

242
  @Override
243
  public void refresh() {
244
    if (resolveState != null) {
×
245
      resolveState.refresh();
×
246
    }
247
  }
×
248

249
  private static String expandPercentS(String template, String replacement) {
250
    return template.replace("%s", replacement);
1✔
251
  }
252

253
  @Override
254
  public void shutdown() {
255
    logger.log(XdsLogLevel.INFO, "Shutdown");
1✔
256
    if (resolveState != null) {
1✔
257
      resolveState.shutdown();
1✔
258
    }
259
    if (xdsClient != null) {
1✔
260
      xdsClient = xdsClientPool.returnObject(xdsClient);
1✔
261
    }
262
  }
1✔
263

264
  @VisibleForTesting
265
  static Map<String, ?> generateServiceConfigWithMethodConfig(
266
      @Nullable Long timeoutNano, @Nullable RetryPolicy retryPolicy) {
267
    if (timeoutNano == null
1✔
268
        && (retryPolicy == null || retryPolicy.retryableStatusCodes().isEmpty())) {
1✔
269
      return Collections.emptyMap();
1✔
270
    }
271
    ImmutableMap.Builder<String, Object> methodConfig = ImmutableMap.builder();
1✔
272
    methodConfig.put(
1✔
273
        "name", Collections.singletonList(Collections.emptyMap()));
1✔
274
    if (retryPolicy != null && !retryPolicy.retryableStatusCodes().isEmpty()) {
1✔
275
      ImmutableMap.Builder<String, Object> rawRetryPolicy = ImmutableMap.builder();
1✔
276
      rawRetryPolicy.put("maxAttempts", (double) retryPolicy.maxAttempts());
1✔
277
      rawRetryPolicy.put("initialBackoff", Durations.toString(retryPolicy.initialBackoff()));
1✔
278
      rawRetryPolicy.put("maxBackoff", Durations.toString(retryPolicy.maxBackoff()));
1✔
279
      rawRetryPolicy.put("backoffMultiplier", 2D);
1✔
280
      List<String> codes = new ArrayList<>(retryPolicy.retryableStatusCodes().size());
1✔
281
      for (Code code : retryPolicy.retryableStatusCodes()) {
1✔
282
        codes.add(code.name());
1✔
283
      }
1✔
284
      rawRetryPolicy.put(
1✔
285
          "retryableStatusCodes", Collections.unmodifiableList(codes));
1✔
286
      if (retryPolicy.perAttemptRecvTimeout() != null) {
1✔
287
        rawRetryPolicy.put(
×
288
            "perAttemptRecvTimeout", Durations.toString(retryPolicy.perAttemptRecvTimeout()));
×
289
      }
290
      methodConfig.put("retryPolicy", rawRetryPolicy.buildOrThrow());
1✔
291
    }
292
    if (timeoutNano != null) {
1✔
293
      String timeout = timeoutNano / 1_000_000_000.0 + "s";
1✔
294
      methodConfig.put("timeout", timeout);
1✔
295
    }
296
    return Collections.singletonMap(
1✔
297
        "methodConfig", Collections.singletonList(methodConfig.buildOrThrow()));
1✔
298
  }
299

300
  @VisibleForTesting
301
  XdsClient getXdsClient() {
302
    return xdsClient;
1✔
303
  }
304

305
  // called in syncContext
306
  private void updateResolutionResult(XdsConfig xdsConfig) {
307
    syncContext.throwIfNotInThisSynchronizationContext();
1✔
308

309
    ImmutableMap.Builder<String, Object> childPolicy = new ImmutableMap.Builder<>();
1✔
310
    for (String name : clusterRefs.keySet()) {
1✔
311
      Map<String, ?> lbPolicy = clusterRefs.get(name).toLbPolicy();
1✔
312
      childPolicy.put(name, ImmutableMap.of("lbPolicy", ImmutableList.of(lbPolicy)));
1✔
313
    }
1✔
314
    Map<String, ?> rawServiceConfig = ImmutableMap.of(
1✔
315
        "loadBalancingConfig",
316
        ImmutableList.of(ImmutableMap.of(
1✔
317
            XdsLbPolicies.CLUSTER_MANAGER_POLICY_NAME,
318
            ImmutableMap.of("childPolicy", childPolicy.buildOrThrow()))));
1✔
319

320
    if (logger.isLoggable(XdsLogLevel.INFO)) {
1✔
321
      logger.log(
×
322
          XdsLogLevel.INFO, "Generated service config: {0}", new Gson().toJson(rawServiceConfig));
×
323
    }
324
    ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
1✔
325
    Attributes attrs =
326
        Attributes.newBuilder()
1✔
327
            .set(XdsAttributes.XDS_CLIENT, xdsClient)
1✔
328
            .set(XdsAttributes.XDS_CONFIG, xdsConfig)
1✔
329
            .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, resolveState.xdsDependencyManager)
1✔
330
            .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
1✔
331
            .set(InternalConfigSelector.KEY, configSelector)
1✔
332
            .build();
1✔
333
    ResolutionResult result =
334
        ResolutionResult.newBuilder()
1✔
335
            .setAttributes(attrs)
1✔
336
            .setServiceConfig(parsedServiceConfig)
1✔
337
            .build();
1✔
338
    if (!listener.onResult2(result).isOk()) {
1✔
339
      resolveState.xdsDependencyManager.requestReresolution();
×
340
    }
341
  }
1✔
342

343
  private final class ConfigSelector extends InternalConfigSelector {
1✔
344
    @Override
345
    public Result selectConfig(PickSubchannelArgs args) {
346
      RoutingConfig routingCfg;
347
      RouteData selectedRoute;
348
      String cluster;
349
      ClientInterceptor filters;
350
      Metadata headers = args.getHeaders();
1✔
351
      String path = "/" + args.getMethodDescriptor().getFullMethodName();
1✔
352
      do {
353
        routingCfg = routingConfig;
1✔
354
        if (routingCfg.errorStatus != null) {
1✔
355
          return Result.forError(routingCfg.errorStatus);
1✔
356
        }
357
        selectedRoute = null;
1✔
358
        for (RouteData route : routingCfg.routes) {
1✔
359
          if (RoutingUtils.matchRoute(route.routeMatch, path, headers, random)) {
1✔
360
            selectedRoute = route;
1✔
361
            break;
1✔
362
          }
363
        }
1✔
364
        if (selectedRoute == null) {
1✔
365
          return Result.forError(
1✔
366
              Status.UNAVAILABLE.withDescription("Could not find xDS route matching RPC"));
1✔
367
        }
368
        if (selectedRoute.routeAction == null) {
1✔
369
          return Result.forError(Status.UNAVAILABLE.withDescription(
1✔
370
              "Could not route RPC to Route with non-forwarding action"));
371
        }
372
        RouteAction action = selectedRoute.routeAction;
1✔
373
        if (action.cluster() != null) {
1✔
374
          cluster = prefixedClusterName(action.cluster());
1✔
375
          filters = selectedRoute.filterChoices.get(0);
1✔
376
        } else if (action.weightedClusters() != null) {
1✔
377
          // XdsRouteConfigureResource verifies the total weight will not be 0 or exceed uint32
378
          long totalWeight = 0;
1✔
379
          for (ClusterWeight weightedCluster : action.weightedClusters()) {
1✔
380
            totalWeight += weightedCluster.weight();
1✔
381
          }
1✔
382
          long select = random.nextLong(totalWeight);
1✔
383
          long accumulator = 0;
1✔
384
          for (int i = 0; ; i++) {
1✔
385
            ClusterWeight weightedCluster = action.weightedClusters().get(i);
1✔
386
            accumulator += weightedCluster.weight();
1✔
387
            if (select < accumulator) {
1✔
388
              cluster = prefixedClusterName(weightedCluster.name());
1✔
389
              filters = selectedRoute.filterChoices.get(i);
1✔
390
              break;
1✔
391
            }
392
          }
393
        } else if (action.namedClusterSpecifierPluginConfig() != null) {
1✔
394
          cluster =
1✔
395
              prefixedClusterSpecifierPluginName(action.namedClusterSpecifierPluginConfig().name());
1✔
396
          filters = selectedRoute.filterChoices.get(0);
1✔
397
        } else {
398
          // updateRoutes() discards routes with unknown actions
399
          throw new AssertionError();
×
400
        }
401
      } while (!retainCluster(cluster));
1✔
402

403
      final RouteAction routeAction = selectedRoute.routeAction;
1✔
404
      Long timeoutNanos = null;
1✔
405
      if (enableTimeout) {
1✔
406
        timeoutNanos = routeAction.timeoutNano();
1✔
407
        if (timeoutNanos == null) {
1✔
408
          timeoutNanos = routingCfg.fallbackTimeoutNano;
1✔
409
        }
410
        if (timeoutNanos <= 0) {
1✔
411
          timeoutNanos = null;
1✔
412
        }
413
      }
414
      RetryPolicy retryPolicy = routeAction.retryPolicy();
1✔
415
      // TODO(chengyuanzhang): avoid service config generation and parsing for each call.
416
      Map<String, ?> rawServiceConfig =
1✔
417
          generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy);
1✔
418
      ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
1✔
419
      Object config = parsedServiceConfig.getConfig();
1✔
420
      if (config == null) {
1✔
421
        releaseCluster(cluster);
×
422
        return Result.forError(
×
423
            parsedServiceConfig.getError().augmentDescription(
×
424
                "Failed to parse service config (method config)"));
425
      }
426
      final String finalCluster = cluster;
1✔
427
      final XdsConfig xdsConfig = routingCfg.xdsConfig;
1✔
428
      final long hash = generateHash(routeAction.hashPolicies(), headers);
1✔
429
      class ClusterSelectionInterceptor implements ClientInterceptor {
1✔
430
        @Override
431
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
432
            final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
433
            final Channel next) {
434
          CallOptions callOptionsForCluster =
1✔
435
              callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster)
1✔
436
                  .withOption(XDS_CONFIG_CALL_OPTION_KEY, xdsConfig)
1✔
437
                  .withOption(RPC_HASH_KEY, hash);
1✔
438
          if (routeAction.autoHostRewrite()) {
1✔
439
            callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true);
1✔
440
          }
441
          return new SimpleForwardingClientCall<ReqT, RespT>(
1✔
442
              next.newCall(method, callOptionsForCluster)) {
1✔
443
            @Override
444
            public void start(Listener<RespT> listener, Metadata headers) {
445
              listener = new SimpleForwardingClientCallListener<RespT>(listener) {
1✔
446
                boolean committed;
447

448
                @Override
449
                public void onHeaders(Metadata headers) {
450
                  committed = true;
1✔
451
                  releaseCluster(finalCluster);
1✔
452
                  delegate().onHeaders(headers);
1✔
453
                }
1✔
454

455
                @Override
456
                public void onClose(Status status, Metadata trailers) {
457
                  if (!committed) {
1✔
458
                    releaseCluster(finalCluster);
1✔
459
                  }
460
                  delegate().onClose(status, trailers);
1✔
461
                }
1✔
462
              };
463
              delegate().start(listener, headers);
1✔
464
            }
1✔
465
          };
466
        }
467
      }
468

469
      return
1✔
470
          Result.newBuilder()
1✔
471
              .setConfig(config)
1✔
472
              .setInterceptor(combineInterceptors(
1✔
473
                  ImmutableList.of(new ClusterSelectionInterceptor(), filters)))
1✔
474
              .build();
1✔
475
    }
476

477
    private boolean retainCluster(String cluster) {
478
      ClusterRefState clusterRefState = clusterRefs.get(cluster);
1✔
479
      if (clusterRefState == null) {
1✔
480
        return false;
×
481
      }
482
      AtomicInteger refCount = clusterRefState.refCount;
1✔
483
      int count;
484
      do {
485
        count = refCount.get();
1✔
486
        if (count == 0) {
1✔
487
          return false;
×
488
        }
489
      } while (!refCount.compareAndSet(count, count + 1));
1✔
490
      return true;
1✔
491
    }
492

493
    private void releaseCluster(final String cluster) {
494
      int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
495
      if (count < 0) {
1✔
496
        throw new AssertionError();
×
497
      }
498
      if (count == 0) {
1✔
499
        syncContext.execute(new Runnable() {
1✔
500
          @Override
501
          public void run() {
502
            if (clusterRefs.get(cluster).refCount.get() != 0) {
1✔
503
              throw new AssertionError();
×
504
            }
505
            clusterRefs.remove(cluster).close();
1✔
506
            if (resolveState.lastConfigOrStatus.hasValue()) {
1✔
507
              updateResolutionResult(resolveState.lastConfigOrStatus.getValue());
1✔
508
            } else {
509
              resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus());
×
510
            }
511
          }
1✔
512
        });
513
      }
514
    }
1✔
515

516
    private long generateHash(List<HashPolicy> hashPolicies, Metadata headers) {
517
      Long hash = null;
1✔
518
      for (HashPolicy policy : hashPolicies) {
1✔
519
        Long newHash = null;
1✔
520
        if (policy.type() == HashPolicy.Type.HEADER) {
1✔
521
          String value = getHeaderValue(headers, policy.headerName());
1✔
522
          if (value != null) {
1✔
523
            if (policy.regEx() != null && policy.regExSubstitution() != null) {
1✔
524
              value = policy.regEx().matcher(value).replaceAll(policy.regExSubstitution());
1✔
525
            }
526
            newHash = hashFunc.hashAsciiString(value);
1✔
527
          }
528
        } else if (policy.type() == HashPolicy.Type.CHANNEL_ID) {
1✔
529
          newHash = hashFunc.hashLong(randomChannelId);
1✔
530
        }
531
        if (newHash != null ) {
1✔
532
          // Rotating the old value prevents duplicate hash rules from cancelling each other out
533
          // and preserves all of the entropy.
534
          long oldHash = hash != null ? ((hash << 1L) | (hash >> 63L)) : 0;
1✔
535
          hash = oldHash ^ newHash;
1✔
536
        }
537
        // If the policy is a terminal policy and a hash has been generated, ignore
538
        // the rest of the hash policies.
539
        if (policy.isTerminal() && hash != null) {
1✔
540
          break;
×
541
        }
542
      }
1✔
543
      return hash == null ? random.nextLong() : hash;
1✔
544
    }
545
  }
546

547
  static final class PassthroughClientInterceptor implements ClientInterceptor {
1✔
548
    @Override
549
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
550
        MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
551
      return next.newCall(method, callOptions);
1✔
552
    }
553
  }
554

555
  private static ClientInterceptor combineInterceptors(final List<ClientInterceptor> interceptors) {
556
    if (interceptors.size() == 0) {
1✔
557
      return new PassthroughClientInterceptor();
1✔
558
    }
559
    if (interceptors.size() == 1) {
1✔
560
      return interceptors.get(0);
1✔
561
    }
562
    return new ClientInterceptor() {
1✔
563
      @Override
564
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
565
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
566
        next = ClientInterceptors.interceptForward(next, interceptors);
1✔
567
        return next.newCall(method, callOptions);
1✔
568
      }
569
    };
570
  }
571

572
  @Nullable
573
  private static String getHeaderValue(Metadata headers, String headerName) {
574
    if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
575
      return null;
×
576
    }
577
    if (headerName.equals("content-type")) {
1✔
578
      return "application/grpc";
×
579
    }
580
    Metadata.Key<String> key;
581
    try {
582
      key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
1✔
583
    } catch (IllegalArgumentException e) {
×
584
      return null;
×
585
    }
1✔
586
    Iterable<String> values = headers.getAll(key);
1✔
587
    return values == null ? null : Joiner.on(",").join(values);
1✔
588
  }
589

590
  private static String prefixedClusterName(String name) {
591
    return "cluster:" + name;
1✔
592
  }
593

594
  private static String prefixedClusterSpecifierPluginName(String pluginName) {
595
    return "cluster_specifier_plugin:" + pluginName;
1✔
596
  }
597

598
  class ResolveState implements XdsDependencyManager.XdsConfigWatcher {
1✔
599
    private final ConfigOrError emptyServiceConfig =
1✔
600
        serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap());
1✔
601
    private final String authority;
602
    private final XdsDependencyManager xdsDependencyManager;
603
    private boolean stopped;
604
    @Nullable
605
    private Set<String> existingClusters;  // clusters to which new requests can be routed
606
    private StatusOr<XdsConfig> lastConfigOrStatus;
607

608
    private ResolveState(String ldsResourceName) {
1✔
609
      authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority;
1✔
610
      xdsDependencyManager =
1✔
611
          new XdsDependencyManager(xdsClient, syncContext, authority, ldsResourceName,
1✔
612
              nameResolverArgs);
1✔
613
    }
1✔
614

615
    void start() {
616
      xdsDependencyManager.start(this);
1✔
617
    }
1✔
618

619
    void refresh() {
620
      xdsDependencyManager.requestReresolution();
×
621
    }
×
622

623
    private void shutdown() {
624
      if (stopped) {
1✔
625
        return;
×
626
      }
627

628
      stopped = true;
1✔
629
      xdsDependencyManager.shutdown();
1✔
630
      updateActiveFilters(null);
1✔
631
    }
1✔
632

633
    @Override
634
    public void onUpdate(StatusOr<XdsConfig> updateOrStatus) {
635
      if (stopped) {
1✔
636
        return;
×
637
      }
638
      logger.log(XdsLogLevel.INFO, "Receive XDS resource update: {0}", updateOrStatus);
1✔
639

640
      lastConfigOrStatus = updateOrStatus;
1✔
641
      if (!updateOrStatus.hasValue()) {
1✔
642
        updateActiveFilters(null);
1✔
643
        cleanUpRoutes(updateOrStatus.getStatus());
1✔
644
        return;
1✔
645
      }
646

647
      // Process Route
648
      XdsConfig update = updateOrStatus.getValue();
1✔
649
      HttpConnectionManager httpConnectionManager = update.getListener().httpConnectionManager();
1✔
650
      if (httpConnectionManager == null) {
1✔
651
        logger.log(XdsLogLevel.INFO, "API Listener: httpConnectionManager does not exist.");
×
652
        updateActiveFilters(null);
×
653
        cleanUpRoutes(updateOrStatus.getStatus());
×
654
        return;
×
655
      }
656

657
      VirtualHost virtualHost = update.getVirtualHost();
1✔
658
      ImmutableList<NamedFilterConfig> filterConfigs = httpConnectionManager.httpFilterConfigs();
1✔
659
      long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano();
1✔
660

661
      updateActiveFilters(filterConfigs);
1✔
662
      updateRoutes(update, virtualHost, streamDurationNano, filterConfigs);
1✔
663
    }
1✔
664

665
    // called in syncContext
666
    private void updateActiveFilters(@Nullable List<NamedFilterConfig> filterConfigs) {
667
      if (filterConfigs == null) {
1✔
668
        filterConfigs = ImmutableList.of();
1✔
669
      }
670
      Set<String> filtersToShutdown = new HashSet<>(activeFilters.keySet());
1✔
671
      for (NamedFilterConfig namedFilter : filterConfigs) {
1✔
672
        String typeUrl = namedFilter.filterConfig.typeUrl();
1✔
673
        String filterKey = namedFilter.filterStateKey();
1✔
674

675
        Filter.Provider provider = filterRegistry.get(typeUrl);
1✔
676
        checkNotNull(provider, "provider %s", typeUrl);
1✔
677
        Filter filter = activeFilters.computeIfAbsent(
1✔
678
            filterKey, k -> provider.newInstance(namedFilter.name));
1✔
679
        checkNotNull(filter, "filter %s", filterKey);
1✔
680
        filtersToShutdown.remove(filterKey);
1✔
681
      }
1✔
682

683
      // Shutdown filters not present in current HCM.
684
      for (String filterKey : filtersToShutdown) {
1✔
685
        Filter filterToShutdown = activeFilters.remove(filterKey);
1✔
686
        checkNotNull(filterToShutdown, "filterToShutdown %s", filterKey);
1✔
687
        filterToShutdown.close();
1✔
688
      }
1✔
689
    }
1✔
690

691
    private void updateRoutes(
692
        XdsConfig xdsConfig,
693
        @Nullable VirtualHost virtualHost,
694
        long httpMaxStreamDurationNano,
695
        @Nullable List<NamedFilterConfig> filterConfigs) {
696
      List<Route> routes = virtualHost.routes();
1✔
697
      ImmutableList.Builder<RouteData> routesData = ImmutableList.builder();
1✔
698

699
      // Populate all clusters to which requests can be routed to through the virtual host.
700
      Set<String> clusters = new HashSet<>();
1✔
701
      // uniqueName -> clusterName
702
      Map<String, String> clusterNameMap = new HashMap<>();
1✔
703
      // uniqueName -> pluginConfig
704
      Map<String, RlsPluginConfig> rlsPluginConfigMap = new HashMap<>();
1✔
705
      for (Route route : routes) {
1✔
706
        RouteAction action = route.routeAction();
1✔
707
        String prefixedName;
708
        if (action == null) {
1✔
709
          routesData.add(new RouteData(route.routeMatch(), null, ImmutableList.of()));
1✔
710
        } else if (action.cluster() != null) {
1✔
711
          prefixedName = prefixedClusterName(action.cluster());
1✔
712
          clusters.add(prefixedName);
1✔
713
          clusterNameMap.put(prefixedName, action.cluster());
1✔
714
          ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null);
1✔
715
          routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters));
1✔
716
        } else if (action.weightedClusters() != null) {
1✔
717
          ImmutableList.Builder<ClientInterceptor> filterList = ImmutableList.builder();
1✔
718
          for (ClusterWeight weightedCluster : action.weightedClusters()) {
1✔
719
            prefixedName = prefixedClusterName(weightedCluster.name());
1✔
720
            clusters.add(prefixedName);
1✔
721
            clusterNameMap.put(prefixedName, weightedCluster.name());
1✔
722
            filterList.add(createFilters(filterConfigs, virtualHost, route, weightedCluster));
1✔
723
          }
1✔
724
          routesData.add(
1✔
725
              new RouteData(route.routeMatch(), route.routeAction(), filterList.build()));
1✔
726
        } else if (action.namedClusterSpecifierPluginConfig() != null) {
1✔
727
          PluginConfig pluginConfig = action.namedClusterSpecifierPluginConfig().config();
1✔
728
          if (pluginConfig instanceof RlsPluginConfig) {
1✔
729
            prefixedName = prefixedClusterSpecifierPluginName(
1✔
730
                action.namedClusterSpecifierPluginConfig().name());
1✔
731
            clusters.add(prefixedName);
1✔
732
            rlsPluginConfigMap.put(prefixedName, (RlsPluginConfig) pluginConfig);
1✔
733
          }
734
          ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null);
1✔
735
          routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters));
1✔
736
        } else {
737
          // Discard route
738
        }
739
      }
1✔
740

741
      // Updates channel's load balancing config whenever the set of selectable clusters changes.
742
      boolean shouldUpdateResult = existingClusters == null;
1✔
743
      Set<String> addedClusters =
744
          existingClusters == null ? clusters : Sets.difference(clusters, existingClusters);
1✔
745
      Set<String> deletedClusters =
746
          existingClusters == null
1✔
747
              ? Collections.emptySet() : Sets.difference(existingClusters, clusters);
1✔
748
      existingClusters = clusters;
1✔
749
      for (String cluster : addedClusters) {
1✔
750
        if (clusterRefs.containsKey(cluster)) {
1✔
751
          clusterRefs.get(cluster).refCount.incrementAndGet();
1✔
752
        } else {
753
          if (clusterNameMap.containsKey(cluster)) {
1✔
754
            assert cluster.startsWith("cluster:");
1✔
755
            XdsConfig.Subscription subscription =
1✔
756
                xdsDependencyManager.subscribeToCluster(cluster.substring("cluster:".length()));
1✔
757
            clusterRefs.put(
1✔
758
                cluster,
759
                ClusterRefState.forCluster(
1✔
760
                    new AtomicInteger(1), clusterNameMap.get(cluster), subscription));
1✔
761
          }
762
          if (rlsPluginConfigMap.containsKey(cluster)) {
1✔
763
            clusterRefs.put(
1✔
764
                cluster,
765
                ClusterRefState.forRlsPlugin(
1✔
766
                    new AtomicInteger(1), rlsPluginConfigMap.get(cluster)));
1✔
767
          }
768
          shouldUpdateResult = true;
1✔
769
        }
770
      }
1✔
771
      for (String cluster : clusters) {
1✔
772
        RlsPluginConfig rlsPluginConfig = rlsPluginConfigMap.get(cluster);
1✔
773
        if (!Objects.equals(rlsPluginConfig, clusterRefs.get(cluster).rlsPluginConfig)) {
1✔
774
          ClusterRefState newClusterRefState =
1✔
775
              ClusterRefState.forRlsPlugin(clusterRefs.get(cluster).refCount, rlsPluginConfig);
1✔
776
          clusterRefs.put(cluster, newClusterRefState);
1✔
777
          shouldUpdateResult = true;
1✔
778
        }
779
      }
1✔
780
      // Update service config to include newly added clusters.
781
      if (shouldUpdateResult && routingConfig != null) {
1✔
782
        updateResolutionResult(xdsConfig);
1✔
783
        shouldUpdateResult = false;
1✔
784
      } else {
785
        // Need to update at least once
786
        shouldUpdateResult = true;
1✔
787
      }
788
      // Make newly added clusters selectable by config selector and deleted clusters no longer
789
      // selectable.
790
      routingConfig = new RoutingConfig(xdsConfig, httpMaxStreamDurationNano, routesData.build());
1✔
791
      for (String cluster : deletedClusters) {
1✔
792
        int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
793
        if (count == 0) {
1✔
794
          clusterRefs.remove(cluster).close();
1✔
795
          shouldUpdateResult = true;
1✔
796
        }
797
      }
1✔
798
      if (shouldUpdateResult) {
1✔
799
        updateResolutionResult(xdsConfig);
1✔
800
      }
801
    }
1✔
802

803
    private ClientInterceptor createFilters(
804
        @Nullable List<NamedFilterConfig> filterConfigs,
805
        VirtualHost virtualHost,
806
        Route route,
807
        @Nullable ClusterWeight weightedCluster) {
808
      if (filterConfigs == null) {
1✔
809
        return new PassthroughClientInterceptor();
1✔
810
      }
811

812
      Map<String, FilterConfig> selectedOverrideConfigs =
1✔
813
          new HashMap<>(virtualHost.filterConfigOverrides());
1✔
814
      selectedOverrideConfigs.putAll(route.filterConfigOverrides());
1✔
815
      if (weightedCluster != null) {
1✔
816
        selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides());
1✔
817
      }
818

819
      ImmutableList.Builder<ClientInterceptor> filterInterceptors = ImmutableList.builder();
1✔
820
      for (NamedFilterConfig namedFilter : filterConfigs) {
1✔
821
        String name = namedFilter.name;
1✔
822
        FilterConfig config = namedFilter.filterConfig;
1✔
823
        FilterConfig overrideConfig = selectedOverrideConfigs.get(name);
1✔
824
        String filterKey = namedFilter.filterStateKey();
1✔
825

826
        Filter filter = activeFilters.get(filterKey);
1✔
827
        checkNotNull(filter, "activeFilters.get(%s)", filterKey);
1✔
828
        ClientInterceptor interceptor =
1✔
829
            filter.buildClientInterceptor(config, overrideConfig, scheduler);
1✔
830

831
        if (interceptor != null) {
1✔
832
          filterInterceptors.add(interceptor);
1✔
833
        }
834
      }
1✔
835

836
      // Combine interceptors produced by different filters into a single one that executes
837
      // them sequentially. The order is preserved.
838
      return combineInterceptors(filterInterceptors.build());
1✔
839
    }
840

841
    private void cleanUpRoutes(Status error) {
842
      routingConfig = new RoutingConfig(error);
1✔
843
      if (existingClusters != null) {
1✔
844
        for (String cluster : existingClusters) {
1✔
845
          int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
846
          if (count == 0) {
1✔
847
            clusterRefs.remove(cluster).close();
1✔
848
          }
849
        }
1✔
850
        existingClusters = null;
1✔
851
      }
852

853
      // Without addresses the default LB (normally pick_first) should become TRANSIENT_FAILURE, and
854
      // the config selector handles the error message itself.
855
      listener.onResult2(ResolutionResult.newBuilder()
1✔
856
          .setAttributes(Attributes.newBuilder()
1✔
857
            .set(InternalConfigSelector.KEY, configSelector)
1✔
858
            .build())
1✔
859
          .setServiceConfig(emptyServiceConfig)
1✔
860
          .build());
1✔
861
    }
1✔
862
  }
863

864
  /**
865
   * VirtualHost-level configuration for request routing.
866
   */
867
  private static class RoutingConfig {
868
    final XdsConfig xdsConfig;
869
    final long fallbackTimeoutNano;
870
    final ImmutableList<RouteData> routes;
871
    final Status errorStatus;
872

873
    private RoutingConfig(
874
        XdsConfig xdsConfig, long fallbackTimeoutNano, ImmutableList<RouteData> routes) {
1✔
875
      this.xdsConfig = checkNotNull(xdsConfig, "xdsConfig");
1✔
876
      this.fallbackTimeoutNano = fallbackTimeoutNano;
1✔
877
      this.routes = checkNotNull(routes, "routes");
1✔
878
      this.errorStatus = null;
1✔
879
    }
1✔
880

881
    private RoutingConfig(Status errorStatus) {
1✔
882
      this.xdsConfig = null;
1✔
883
      this.fallbackTimeoutNano = 0;
1✔
884
      this.routes = null;
1✔
885
      this.errorStatus = checkNotNull(errorStatus, "errorStatus");
1✔
886
      checkArgument(!errorStatus.isOk(), "errorStatus should not be okay");
1✔
887
    }
1✔
888
  }
889

890
  static final class RouteData {
891
    final RouteMatch routeMatch;
892
    /** null implies non-forwarding action. */
893
    @Nullable
894
    final RouteAction routeAction;
895
    /**
896
     * Only one of these interceptors should be used per-RPC. There are only multiple values in the
897
     * list for weighted clusters, in which case the order of the list mirrors the weighted
898
     * clusters.
899
     */
900
    final ImmutableList<ClientInterceptor> filterChoices;
901

902
    RouteData(RouteMatch routeMatch, @Nullable RouteAction routeAction, ClientInterceptor filter) {
903
      this(routeMatch, routeAction, ImmutableList.of(filter));
1✔
904
    }
1✔
905

906
    RouteData(
907
        RouteMatch routeMatch,
908
        @Nullable RouteAction routeAction,
909
        ImmutableList<ClientInterceptor> filterChoices) {
1✔
910
      this.routeMatch = checkNotNull(routeMatch, "routeMatch");
1✔
911
      checkArgument(
1✔
912
          routeAction == null || !filterChoices.isEmpty(),
1✔
913
          "filter may be empty only for non-forwarding action");
914
      this.routeAction = routeAction;
1✔
915
      if (routeAction != null && routeAction.weightedClusters() != null) {
1✔
916
        checkArgument(
1✔
917
            routeAction.weightedClusters().size() == filterChoices.size(),
1✔
918
            "filter choices must match size of weighted clusters");
919
      }
920
      for (ClientInterceptor filter : filterChoices) {
1✔
921
        checkNotNull(filter, "entry in filterChoices is null");
1✔
922
      }
1✔
923
      this.filterChoices = checkNotNull(filterChoices, "filterChoices");
1✔
924
    }
1✔
925
  }
926

927
  private static class ClusterRefState {
928
    final AtomicInteger refCount;
929
    @Nullable
930
    final String traditionalCluster;
931
    @Nullable
932
    final RlsPluginConfig rlsPluginConfig;
933
    @Nullable
934
    final XdsConfig.Subscription subscription;
935

936
    private ClusterRefState(
937
        AtomicInteger refCount, @Nullable String traditionalCluster,
938
        @Nullable RlsPluginConfig rlsPluginConfig, @Nullable XdsConfig.Subscription subscription) {
1✔
939
      this.refCount = refCount;
1✔
940
      checkArgument(traditionalCluster == null ^ rlsPluginConfig == null,
1✔
941
          "There must be exactly one non-null value in traditionalCluster and pluginConfig");
942
      this.traditionalCluster = traditionalCluster;
1✔
943
      this.rlsPluginConfig = rlsPluginConfig;
1✔
944
      this.subscription = subscription;
1✔
945
    }
1✔
946

947
    private Map<String, ?> toLbPolicy() {
948
      if (traditionalCluster != null) {
1✔
949
        return ImmutableMap.of(
1✔
950
            XdsLbPolicies.CDS_POLICY_NAME,
951
            ImmutableMap.of("cluster", traditionalCluster));
1✔
952
      } else {
953
        ImmutableMap<String, ?> rlsConfig = new ImmutableMap.Builder<String, Object>()
1✔
954
            .put("routeLookupConfig", rlsPluginConfig.config())
1✔
955
            .put(
1✔
956
                "childPolicy",
957
                ImmutableList.of(ImmutableMap.of(XdsLbPolicies.CDS_POLICY_NAME, ImmutableMap.of(
1✔
958
                    "is_dynamic", true))))
1✔
959
            .put("childPolicyConfigTargetFieldName", "cluster")
1✔
960
            .buildOrThrow();
1✔
961
        return ImmutableMap.of("rls_experimental", rlsConfig);
1✔
962
      }
963
    }
964

965
    private void close() {
966
      if (subscription != null) {
1✔
967
        subscription.close();
1✔
968
      }
969
    }
1✔
970

971
    static ClusterRefState forCluster(
972
        AtomicInteger refCount, String name, XdsConfig.Subscription subscription) {
973
      return new ClusterRefState(refCount, name, null, checkNotNull(subscription, "subscription"));
1✔
974
    }
975

976
    static ClusterRefState forRlsPlugin(
977
        AtomicInteger refCount,
978
        RlsPluginConfig rlsPluginConfig) {
979
      return new ClusterRefState(refCount, null, rlsPluginConfig, null);
1✔
980
    }
981
  }
982

983
  /** An ObjectPool, except it can throw an exception. */
984
  private interface XdsClientPool {
985
    XdsClient getObject() throws XdsInitializationException;
986

987
    XdsClient returnObject(XdsClient xdsClient);
988
  }
989

990
  private static final class BootstrappingXdsClientPool implements XdsClientPool {
991
    private final XdsClientPoolFactory xdsClientPoolFactory;
992
    private final String target;
993
    private final @Nullable Map<String, ?> bootstrapOverride;
994
    private final @Nullable MetricRecorder metricRecorder;
995
    private ObjectPool<XdsClient> xdsClientPool;
996

997
    BootstrappingXdsClientPool(
998
        XdsClientPoolFactory xdsClientPoolFactory,
999
        String target,
1000
        @Nullable Map<String, ?> bootstrapOverride,
1001
        @Nullable MetricRecorder metricRecorder) {
1✔
1002
      this.xdsClientPoolFactory = checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory");
1✔
1003
      this.target = checkNotNull(target, "target");
1✔
1004
      this.bootstrapOverride = bootstrapOverride;
1✔
1005
      this.metricRecorder = metricRecorder;
1✔
1006
    }
1✔
1007

1008
    @Override
1009
    public XdsClient getObject() throws XdsInitializationException {
1010
      if (xdsClientPool == null) {
1✔
1011
        BootstrapInfo bootstrapInfo;
1012
        if (bootstrapOverride == null) {
1✔
1013
          bootstrapInfo = GrpcBootstrapperImpl.defaultBootstrap();
×
1014
        } else {
1015
          bootstrapInfo = new GrpcBootstrapperImpl().bootstrap(bootstrapOverride);
1✔
1016
        }
1017
        this.xdsClientPool =
1✔
1018
            xdsClientPoolFactory.getOrCreate(target, bootstrapInfo, metricRecorder);
1✔
1019
      }
1020
      return xdsClientPool.getObject();
1✔
1021
    }
1022

1023
    @Override
1024
    public XdsClient returnObject(XdsClient xdsClient) {
1025
      return xdsClientPool.returnObject(xdsClient);
1✔
1026
    }
1027
  }
1028

1029
  private static final class SupplierXdsClientPool implements XdsClientPool {
1030
    private final Supplier<XdsClient> xdsClientSupplier;
1031

1032
    SupplierXdsClientPool(Supplier<XdsClient> xdsClientSupplier) {
×
1033
      this.xdsClientSupplier = checkNotNull(xdsClientSupplier, "xdsClientSupplier");
×
1034
    }
×
1035

1036
    @Override
1037
    public XdsClient getObject() throws XdsInitializationException {
1038
      XdsClient xdsClient = xdsClientSupplier.get();
×
1039
      if (xdsClient == null) {
×
1040
        throw new XdsInitializationException("Caller failed to initialize XDS_CLIENT_SUPPLIER");
×
1041
      }
1042
      return xdsClient;
×
1043
    }
1044

1045
    @Override
1046
    public XdsClient returnObject(XdsClient xdsClient) {
1047
      return null;
×
1048
    }
1049
  }
1050
}
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