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

grpc / grpc-java / #20353

07 Jul 2026 10:24AM UTC coverage: 89.104% (-0.02%) from 89.122%
#20353

push

github

web-flow
enable child channel plugins (#12578)

Implements https://github.com/grpc/proposal/pull/529

38027 of 42677 relevant lines covered (89.1%)

0.89 hits per line

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

91.67
/../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.ChannelConfigurator;
35
import io.grpc.ClientCall;
36
import io.grpc.ClientInterceptor;
37
import io.grpc.ClientInterceptors;
38
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
39
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
40
import io.grpc.InternalConfigSelector;
41
import io.grpc.InternalLogId;
42
import io.grpc.LoadBalancer.PickSubchannelArgs;
43
import io.grpc.Metadata;
44
import io.grpc.MethodDescriptor;
45
import io.grpc.MetricRecorder;
46
import io.grpc.NameResolver;
47
import io.grpc.Status;
48
import io.grpc.Status.Code;
49
import io.grpc.StatusOr;
50
import io.grpc.SynchronizationContext;
51
import io.grpc.internal.GrpcUtil;
52
import io.grpc.internal.ObjectPool;
53
import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig;
54
import io.grpc.xds.Filter.FilterConfig;
55
import io.grpc.xds.Filter.FilterContext;
56
import io.grpc.xds.Filter.NamedFilterConfig;
57
import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig;
58
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
59
import io.grpc.xds.VirtualHost.Route;
60
import io.grpc.xds.VirtualHost.Route.RouteAction;
61
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
62
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
63
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
64
import io.grpc.xds.VirtualHost.Route.RouteMatch;
65
import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
66
import io.grpc.xds.client.Bootstrapper.AuthorityInfo;
67
import io.grpc.xds.client.Bootstrapper.BootstrapInfo;
68
import io.grpc.xds.client.XdsClient;
69
import io.grpc.xds.client.XdsInitializationException;
70
import io.grpc.xds.client.XdsLogger;
71
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
72
import java.io.InputStream;
73
import java.util.ArrayList;
74
import java.util.Collections;
75
import java.util.HashMap;
76
import java.util.HashSet;
77
import java.util.List;
78
import java.util.Locale;
79
import java.util.Map;
80
import java.util.Objects;
81
import java.util.Set;
82
import java.util.concurrent.ConcurrentHashMap;
83
import java.util.concurrent.ConcurrentMap;
84
import java.util.concurrent.ScheduledExecutorService;
85
import java.util.concurrent.atomic.AtomicInteger;
86
import java.util.function.Supplier;
87
import javax.annotation.Nullable;
88

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

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

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

143

144
  /**
145
   * Constructs a new instance.
146
   *
147
   * @param target the target URI to resolve
148
   * @param targetAuthority the authority component of `target`, possibly the empty string, or null
149
   *     if 'target' has no such component
150
   */
151
  XdsNameResolver(
152
      String target, @Nullable String targetAuthority, String name,
153
      @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser,
154
      SynchronizationContext syncContext, ScheduledExecutorService scheduler,
155
      @Nullable Map<String, ?> bootstrapOverride,
156
      MetricRecorder metricRecorder, Args nameResolverArgs) {
157
    this(target, targetAuthority, name, overrideAuthority, serviceConfigParser,
1✔
158
        syncContext, scheduler,
159
        bootstrapOverride == null
1✔
160
            ? SharedXdsClientPoolProvider.getDefaultProvider()
1✔
161
            : new SharedXdsClientPoolProvider(),
1✔
162
        ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride,
1✔
163
        metricRecorder, nameResolverArgs);
164
  }
1✔
165

166
  @VisibleForTesting
167
  XdsNameResolver(
168
      String target, @Nullable String targetAuthority, String name,
169
      @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser,
170
      SynchronizationContext syncContext, ScheduledExecutorService scheduler,
171
      XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random,
172
      FilterRegistry filterRegistry, @Nullable Map<String, ?> bootstrapOverride,
173
      MetricRecorder metricRecorder, Args nameResolverArgs) {
1✔
174
    this.targetAuthority = targetAuthority;
1✔
175

176
    // The name might have multiple slashes so encode it before verifying.
177
    serviceAuthority = checkNotNull(name, "name");
1✔
178
    this.encodedServiceAuthority =
1✔
179
        GrpcUtil.checkAuthority(GrpcUtil.AuthorityEscaper.encodeAuthority(serviceAuthority));
1✔
180

181
    this.overrideAuthority = overrideAuthority;
1✔
182
    this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser");
1✔
183
    this.syncContext = checkNotNull(syncContext, "syncContext");
1✔
184
    this.scheduler = checkNotNull(scheduler, "scheduler");
1✔
185
    Supplier<XdsClient> xdsClientSupplierArg =
1✔
186
        nameResolverArgs.getArg(XdsNameResolverProvider.XDS_CLIENT_SUPPLIER);
1✔
187
    if (xdsClientSupplierArg != null) {
1✔
188
      this.xdsClientPool = new SupplierXdsClientPool(xdsClientSupplierArg);
×
189
    } else {
190
      checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory");
1✔
191
      this.xdsClientPool = new BootstrappingXdsClientPool(
1✔
192
          xdsClientPoolFactory, target, bootstrapOverride, metricRecorder,
193
          nameResolverArgs.getChildChannelConfigurator());
1✔
194
    }
195
    this.random = checkNotNull(random, "random");
1✔
196
    this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry");
1✔
197
    this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs");
1✔
198

199
    randomChannelId = random.nextLong();
1✔
200
    logId = InternalLogId.allocate("xds-resolver", name);
1✔
201
    logger = XdsLogger.withLogId(logId);
1✔
202
    logger.log(XdsLogLevel.INFO, "Created resolver for {0}", name);
1✔
203
  }
1✔
204

205
  @Override
206
  public String getServiceAuthority() {
207
    return encodedServiceAuthority;
1✔
208
  }
209

210
  @Override
211
  public void start(Listener2 listener) {
212
    this.listener = checkNotNull(listener, "listener");
1✔
213
    try {
214
      xdsClient = xdsClientPool.getObject();
1✔
215
    } catch (Exception e) {
1✔
216
      listener.onError(
1✔
217
          Status.UNAVAILABLE.withDescription("Failed to initialize xDS").withCause(e));
1✔
218
      return;
1✔
219
    }
1✔
220
    BootstrapInfo bootstrapInfo = xdsClient.getBootstrapInfo();
1✔
221
    String listenerNameTemplate;
222
    if (targetAuthority == null || targetAuthority.isEmpty()) {
1✔
223
      // Both https://github.com/grpc/proposal/blob/master/A27-xds-global-load-balancing.md and
224
      // A47-xds-federation.md seem to treat an empty authority the same as an undefined one.
225
      listenerNameTemplate = bootstrapInfo.clientDefaultListenerResourceNameTemplate();
1✔
226
    } else {
227
      AuthorityInfo authorityInfo = bootstrapInfo.authorities().get(targetAuthority);
1✔
228
      if (authorityInfo == null) {
1✔
229
        listener.onError(Status.INVALID_ARGUMENT.withDescription(
1✔
230
            "invalid target URI: target authority not found in the bootstrap"));
231
        return;
1✔
232
      }
233
      listenerNameTemplate = authorityInfo.clientListenerResourceNameTemplate();
1✔
234
    }
235
    String replacement = serviceAuthority;
1✔
236
    if (listenerNameTemplate.startsWith(XDSTP_SCHEME)) {
1✔
237
      replacement = XdsClient.percentEncodePath(replacement);
1✔
238
    }
239
    String ldsResourceName = expandPercentS(listenerNameTemplate, replacement);
1✔
240
    if (!XdsClient.isResourceNameValid(ldsResourceName, XdsListenerResource.getInstance().typeUrl())
1✔
241
    ) {
242
      listener.onError(Status.INVALID_ARGUMENT.withDescription(
×
243
          "invalid listener resource URI for service authority: " + serviceAuthority));
244
      return;
×
245
    }
246
    ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName);
1✔
247
    callCounterProvider = SharedCallCounterMap.getInstance();
1✔
248

249
    resolveState = new ResolveState(ldsResourceName);
1✔
250
    resolveState.start();
1✔
251
  }
1✔
252

253
  @Override
254
  public void refresh() {
255
    if (resolveState != null) {
×
256
      resolveState.refresh();
×
257
    }
258
  }
×
259

260
  private static String expandPercentS(String template, String replacement) {
261
    return template.replace("%s", replacement);
1✔
262
  }
263

264
  @Override
265
  public void shutdown() {
266
    logger.log(XdsLogLevel.INFO, "Shutdown");
1✔
267
    if (resolveState != null) {
1✔
268
      resolveState.shutdown();
1✔
269
    }
270
    if (xdsClient != null) {
1✔
271
      xdsClient = xdsClientPool.returnObject(xdsClient);
1✔
272
    }
273
  }
1✔
274

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

311
  @VisibleForTesting
312
  XdsClient getXdsClient() {
313
    return xdsClient;
1✔
314
  }
315

316
  // called in syncContext
317
  private void updateResolutionResult(XdsConfig xdsConfig) {
318
    syncContext.throwIfNotInThisSynchronizationContext();
1✔
319

320
    ImmutableMap.Builder<String, Object> childPolicy = new ImmutableMap.Builder<>();
1✔
321
    for (String name : clusterRefs.keySet()) {
1✔
322
      Map<String, ?> lbPolicy = clusterRefs.get(name).toLbPolicy();
1✔
323
      childPolicy.put(name, ImmutableMap.of("lbPolicy", ImmutableList.of(lbPolicy)));
1✔
324
    }
1✔
325
    Map<String, ?> rawServiceConfig = ImmutableMap.of(
1✔
326
        "loadBalancingConfig",
327
        ImmutableList.of(ImmutableMap.of(
1✔
328
            XdsLbPolicies.CLUSTER_MANAGER_POLICY_NAME,
329
            ImmutableMap.of("childPolicy", childPolicy.buildOrThrow()))));
1✔
330

331
    if (logger.isLoggable(XdsLogLevel.INFO)) {
1✔
332
      logger.log(
×
333
          XdsLogLevel.INFO, "Generated service config: {0}", new Gson().toJson(rawServiceConfig));
×
334
    }
335
    ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
1✔
336
    Attributes attrs =
337
        Attributes.newBuilder()
1✔
338
            .set(XdsAttributes.XDS_CLIENT, xdsClient)
1✔
339
            .set(XdsAttributes.XDS_CONFIG, xdsConfig)
1✔
340
            .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, resolveState.xdsDependencyManager)
1✔
341
            .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
1✔
342
            .set(InternalConfigSelector.KEY, configSelector)
1✔
343
            .build();
1✔
344
    ResolutionResult result =
345
        ResolutionResult.newBuilder()
1✔
346
            .setAttributes(attrs)
1✔
347
            .setServiceConfig(parsedServiceConfig)
1✔
348
            .build();
1✔
349
    if (!listener.onResult2(result).isOk()) {
1✔
350
      resolveState.xdsDependencyManager.requestReresolution();
×
351
    }
352
  }
1✔
353

354
  /**
355
   * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with
356
   * case-insensitive.
357
   *
358
   * <p>Wildcard pattern rules:
359
   * <ol>
360
   * <li>A single asterisk (*) matches any domain.</li>
361
   * <li>Asterisk (*) is only permitted in the left-most or the right-most part of the pattern,
362
   *     but not both.</li>
363
   * </ol>
364
   */
365
  @VisibleForTesting
366
  static boolean matchHostName(String hostName, String pattern) {
367
    checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."),
1✔
368
        "Invalid host name");
369
    checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."),
1✔
370
        "Invalid pattern/domain name");
371

372
    hostName = hostName.toLowerCase(Locale.US);
1✔
373
    pattern = pattern.toLowerCase(Locale.US);
1✔
374
    // hostName and pattern are now in lower case -- domain names are case-insensitive.
375

376
    if (!pattern.contains("*")) {
1✔
377
      // Not a wildcard pattern -- hostName and pattern must match exactly.
378
      return hostName.equals(pattern);
1✔
379
    }
380
    // Wildcard pattern
381

382
    if (pattern.length() == 1) {
1✔
383
      return true;
×
384
    }
385

386
    int index = pattern.indexOf('*');
1✔
387

388
    // At most one asterisk (*) is allowed.
389
    if (pattern.indexOf('*', index + 1) != -1) {
1✔
390
      return false;
×
391
    }
392

393
    // Asterisk can only match prefix or suffix.
394
    if (index != 0 && index != pattern.length() - 1) {
1✔
395
      return false;
×
396
    }
397

398
    // HostName must be at least as long as the pattern because asterisk has to
399
    // match one or more characters.
400
    if (hostName.length() < pattern.length()) {
1✔
401
      return false;
1✔
402
    }
403

404
    if (index == 0 && hostName.endsWith(pattern.substring(1))) {
1✔
405
      // Prefix matching fails.
406
      return true;
1✔
407
    }
408

409
    // Pattern matches hostname if suffix matching succeeds.
410
    return index == pattern.length() - 1
1✔
411
        && hostName.startsWith(pattern.substring(0, pattern.length() - 1));
1✔
412
  }
413

414
  private final class ConfigSelector extends InternalConfigSelector {
1✔
415
    @Override
416
    public Result selectConfig(PickSubchannelArgs args) {
417
      RoutingConfig routingCfg;
418
      RouteData selectedRoute;
419
      String cluster;
420
      ClientInterceptor filters;
421
      Metadata headers = args.getHeaders();
1✔
422
      String path = "/" + args.getMethodDescriptor().getFullMethodName();
1✔
423
      do {
424
        routingCfg = routingConfig;
1✔
425
        if (routingCfg.errorStatus != null) {
1✔
426
          return Result.forError(routingCfg.errorStatus);
1✔
427
        }
428
        selectedRoute = null;
1✔
429
        for (RouteData route : routingCfg.routes) {
1✔
430
          if (RoutingUtils.matchRoute(route.routeMatch, path, headers, random)) {
1✔
431
            selectedRoute = route;
1✔
432
            break;
1✔
433
          }
434
        }
1✔
435
        if (selectedRoute == null) {
1✔
436
          return Result.forError(
1✔
437
              Status.UNAVAILABLE.withDescription("Could not find xDS route matching RPC"));
1✔
438
        }
439
        if (selectedRoute.routeAction == null) {
1✔
440
          return Result.forError(Status.UNAVAILABLE.withDescription(
1✔
441
              "Could not route RPC to Route with non-forwarding action"));
442
        }
443
        RouteAction action = selectedRoute.routeAction;
1✔
444
        if (action.cluster() != null) {
1✔
445
          cluster = prefixedClusterName(action.cluster());
1✔
446
          filters = selectedRoute.filterChoices.get(0);
1✔
447
        } else if (action.weightedClusters() != null) {
1✔
448
          // XdsRouteConfigureResource verifies the total weight will not be 0 or exceed uint32
449
          long totalWeight = 0;
1✔
450
          for (ClusterWeight weightedCluster : action.weightedClusters()) {
1✔
451
            totalWeight += weightedCluster.weight();
1✔
452
          }
1✔
453
          long select = random.nextLong(totalWeight);
1✔
454
          long accumulator = 0;
1✔
455
          for (int i = 0; ; i++) {
1✔
456
            ClusterWeight weightedCluster = action.weightedClusters().get(i);
1✔
457
            accumulator += weightedCluster.weight();
1✔
458
            if (select < accumulator) {
1✔
459
              cluster = prefixedClusterName(weightedCluster.name());
1✔
460
              filters = selectedRoute.filterChoices.get(i);
1✔
461
              break;
1✔
462
            }
463
          }
464
        } else if (action.namedClusterSpecifierPluginConfig() != null) {
1✔
465
          cluster =
1✔
466
              prefixedClusterSpecifierPluginName(action.namedClusterSpecifierPluginConfig().name());
1✔
467
          filters = selectedRoute.filterChoices.get(0);
1✔
468
        } else {
469
          // updateRoutes() discards routes with unknown actions
470
          throw new AssertionError();
×
471
        }
472
      } while (!retainCluster(cluster));
1✔
473

474
      final RouteAction routeAction = selectedRoute.routeAction;
1✔
475
      Long timeoutNanos = null;
1✔
476
      if (enableTimeout) {
1✔
477
        timeoutNanos = routeAction.timeoutNano();
1✔
478
        if (timeoutNanos == null) {
1✔
479
          timeoutNanos = routingCfg.fallbackTimeoutNano;
1✔
480
        }
481
        if (timeoutNanos <= 0) {
1✔
482
          timeoutNanos = null;
1✔
483
        }
484
      }
485
      RetryPolicy retryPolicy = routeAction.retryPolicy();
1✔
486
      // TODO(chengyuanzhang): avoid service config generation and parsing for each call.
487
      Map<String, ?> rawServiceConfig =
1✔
488
          generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy);
1✔
489
      ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
1✔
490
      Object config = parsedServiceConfig.getConfig();
1✔
491
      if (config == null) {
1✔
492
        releaseCluster(cluster);
×
493
        return Result.forError(
×
494
            parsedServiceConfig.getError().augmentDescription(
×
495
                "Failed to parse service config (method config)"));
496
      }
497
      final String finalCluster = cluster;
1✔
498
      final XdsConfig xdsConfig = routingCfg.xdsConfig;
1✔
499
      final long hash = generateHash(routeAction.hashPolicies(), headers);
1✔
500
      class ClusterSelectionInterceptor implements ClientInterceptor {
1✔
501
        @Override
502
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
503
            final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
504
            final Channel next) {
505
          CallOptions callOptionsForCluster =
1✔
506
              callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster)
1✔
507
                  .withOption(XDS_CONFIG_CALL_OPTION_KEY, xdsConfig)
1✔
508
                  .withOption(RPC_HASH_KEY, hash);
1✔
509
          if (routeAction.autoHostRewrite()) {
1✔
510
            callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true);
1✔
511
          }
512
          return new SimpleForwardingClientCall<ReqT, RespT>(
1✔
513
              next.newCall(method, callOptionsForCluster)) {
1✔
514
            @Override
515
            public void start(Listener<RespT> listener, Metadata headers) {
516
              listener = new SimpleForwardingClientCallListener<RespT>(listener) {
1✔
517
                boolean committed;
518

519
                @Override
520
                public void onHeaders(Metadata headers) {
521
                  committed = true;
1✔
522
                  releaseCluster(finalCluster);
1✔
523
                  delegate().onHeaders(headers);
1✔
524
                }
1✔
525

526
                @Override
527
                public void onClose(Status status, Metadata trailers) {
528
                  if (!committed) {
1✔
529
                    releaseCluster(finalCluster);
1✔
530
                  }
531
                  delegate().onClose(status, trailers);
1✔
532
                }
1✔
533
              };
534
              delegate().start(listener, headers);
1✔
535
            }
1✔
536
          };
537
        }
538
      }
539

540
      return
1✔
541
          Result.newBuilder()
1✔
542
              .setConfig(config)
1✔
543
              .setInterceptor(combineInterceptors(
1✔
544
                  ImmutableList.of(new ClusterSelectionInterceptor(), filters)))
1✔
545
              .build();
1✔
546
    }
547

548
    private boolean retainCluster(String cluster) {
549
      ClusterRefState clusterRefState = clusterRefs.get(cluster);
1✔
550
      if (clusterRefState == null) {
1✔
551
        return false;
×
552
      }
553
      AtomicInteger refCount = clusterRefState.refCount;
1✔
554
      int count;
555
      do {
556
        count = refCount.get();
1✔
557
        if (count == 0) {
1✔
558
          return false;
×
559
        }
560
      } while (!refCount.compareAndSet(count, count + 1));
1✔
561
      return true;
1✔
562
    }
563

564
    private void releaseCluster(final String cluster) {
565
      int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
566
      if (count < 0) {
1✔
567
        throw new AssertionError();
×
568
      }
569
      if (count == 0) {
1✔
570
        syncContext.execute(new Runnable() {
1✔
571
          @Override
572
          public void run() {
573
            if (clusterRefs.get(cluster).refCount.get() != 0) {
1✔
574
              throw new AssertionError();
×
575
            }
576
            clusterRefs.remove(cluster).close();
1✔
577
            if (resolveState.lastConfigOrStatus.hasValue()) {
1✔
578
              updateResolutionResult(resolveState.lastConfigOrStatus.getValue());
1✔
579
            } else {
580
              resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus());
×
581
            }
582
          }
1✔
583
        });
584
      }
585
    }
1✔
586

587
    private long generateHash(List<HashPolicy> hashPolicies, Metadata headers) {
588
      Long hash = null;
1✔
589
      for (HashPolicy policy : hashPolicies) {
1✔
590
        Long newHash = null;
1✔
591
        if (policy.type() == HashPolicy.Type.HEADER) {
1✔
592
          String value = getHeaderValue(headers, policy.headerName());
1✔
593
          if (value != null) {
1✔
594
            if (policy.regEx() != null && policy.regExSubstitution() != null) {
1✔
595
              value = policy.regEx().matcher(value).replaceAll(policy.regExSubstitution());
1✔
596
            }
597
            newHash = hashFunc.hashAsciiString(value);
1✔
598
          }
599
        } else if (policy.type() == HashPolicy.Type.CHANNEL_ID) {
1✔
600
          newHash = hashFunc.hashLong(randomChannelId);
1✔
601
        }
602
        if (newHash != null ) {
1✔
603
          // Rotating the old value prevents duplicate hash rules from cancelling each other out
604
          // and preserves all of the entropy.
605
          long oldHash = hash != null ? ((hash << 1L) | (hash >> 63L)) : 0;
1✔
606
          hash = oldHash ^ newHash;
1✔
607
        }
608
        // If the policy is a terminal policy and a hash has been generated, ignore
609
        // the rest of the hash policies.
610
        if (policy.isTerminal() && hash != null) {
1✔
611
          break;
×
612
        }
613
      }
1✔
614
      return hash == null ? random.nextLong() : hash;
1✔
615
    }
616
  }
617

618
  static final class PassthroughClientInterceptor implements ClientInterceptor {
1✔
619
    @Override
620
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
621
        MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
622
      return next.newCall(method, callOptions);
1✔
623
    }
624
  }
625

626
  private static ClientInterceptor combineInterceptors(final List<ClientInterceptor> interceptors) {
627
    if (interceptors.size() == 0) {
1✔
628
      return new PassthroughClientInterceptor();
×
629
    }
630
    if (interceptors.size() == 1) {
1✔
631
      return interceptors.get(0);
1✔
632
    }
633
    return new ClientInterceptor() {
1✔
634
      @Override
635
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
636
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
637
        next = ClientInterceptors.interceptForward(next, interceptors);
1✔
638
        return next.newCall(method, callOptions);
1✔
639
      }
640
    };
641
  }
642

643
  @Nullable
644
  private static String getHeaderValue(Metadata headers, String headerName) {
645
    if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
646
      return null;
×
647
    }
648
    if (headerName.equals("content-type")) {
1✔
649
      return "application/grpc";
×
650
    }
651
    Metadata.Key<String> key;
652
    try {
653
      key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
1✔
654
    } catch (IllegalArgumentException e) {
×
655
      return null;
×
656
    }
1✔
657
    Iterable<String> values = headers.getAll(key);
1✔
658
    return values == null ? null : Joiner.on(",").join(values);
1✔
659
  }
660

661
  private static String prefixedClusterName(String name) {
662
    return "cluster:" + name;
1✔
663
  }
664

665
  private static String prefixedClusterSpecifierPluginName(String pluginName) {
666
    return "cluster_specifier_plugin:" + pluginName;
1✔
667
  }
668

669
  class ResolveState implements XdsDependencyManager.XdsConfigWatcher {
1✔
670
    private final ConfigOrError emptyServiceConfig =
1✔
671
        serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap());
1✔
672
    private final String authority;
673
    private final XdsDependencyManager xdsDependencyManager;
674
    private boolean stopped;
675
    @Nullable
676
    private Set<String> existingClusters;  // clusters to which new requests can be routed
677
    private StatusOr<XdsConfig> lastConfigOrStatus;
678

679
    private ResolveState(String ldsResourceName) {
1✔
680
      authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority;
1✔
681
      xdsDependencyManager =
1✔
682
          new XdsDependencyManager(xdsClient, syncContext, authority, ldsResourceName,
1✔
683
              nameResolverArgs);
1✔
684
    }
1✔
685

686
    void start() {
687
      xdsDependencyManager.start(this);
1✔
688
    }
1✔
689

690
    void refresh() {
691
      xdsDependencyManager.requestReresolution();
×
692
    }
×
693

694
    private void shutdown() {
695
      if (stopped) {
1✔
696
        return;
×
697
      }
698

699
      stopped = true;
1✔
700
      xdsDependencyManager.shutdown();
1✔
701
      updateActiveFilters(null);
1✔
702
    }
1✔
703

704
    @Override
705
    public void onUpdate(StatusOr<XdsConfig> updateOrStatus) {
706
      if (stopped) {
1✔
707
        return;
×
708
      }
709
      logger.log(XdsLogLevel.INFO, "Receive XDS resource update: {0}", updateOrStatus);
1✔
710

711
      lastConfigOrStatus = updateOrStatus;
1✔
712
      if (!updateOrStatus.hasValue()) {
1✔
713
        updateActiveFilters(null);
1✔
714
        cleanUpRoutes(updateOrStatus.getStatus());
1✔
715
        return;
1✔
716
      }
717

718
      // Process Route
719
      XdsConfig update = updateOrStatus.getValue();
1✔
720
      HttpConnectionManager httpConnectionManager = update.getListener().httpConnectionManager();
1✔
721
      if (httpConnectionManager == null) {
1✔
722
        logger.log(XdsLogLevel.INFO, "API Listener: httpConnectionManager does not exist.");
×
723
        updateActiveFilters(null);
×
724
        cleanUpRoutes(updateOrStatus.getStatus());
×
725
        return;
×
726
      }
727

728
      VirtualHost virtualHost = update.getVirtualHost();
1✔
729
      ImmutableList<NamedFilterConfig> filterConfigs = httpConnectionManager.httpFilterConfigs();
1✔
730
      long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano();
1✔
731

732
      updateActiveFilters(filterConfigs);
1✔
733
      updateRoutes(update, virtualHost, streamDurationNano, filterConfigs);
1✔
734
    }
1✔
735

736
    // called in syncContext
737
    private void updateActiveFilters(@Nullable List<NamedFilterConfig> filterConfigs) {
738
      if (filterConfigs == null) {
1✔
739
        filterConfigs = ImmutableList.of();
1✔
740
      }
741
      Set<String> filtersToShutdown = new HashSet<>(activeFilters.keySet());
1✔
742
      for (NamedFilterConfig namedFilter : filterConfigs) {
1✔
743
        String typeUrl = namedFilter.filterConfig.typeUrl();
1✔
744
        String filterKey = namedFilter.filterStateKey();
1✔
745

746
        Filter.Provider provider = filterRegistry.get(typeUrl);
1✔
747
        checkNotNull(provider, "provider %s", typeUrl);
1✔
748
        Filter filter = activeFilters.computeIfAbsent(
1✔
749
            filterKey, k -> provider.newInstance(
1✔
750
                FilterContext.create(
1✔
751
                    namedFilter.name, nameResolverArgs.getMetricRecorder())));
1✔
752
        checkNotNull(filter, "filter %s", filterKey);
1✔
753
        filtersToShutdown.remove(filterKey);
1✔
754
      }
1✔
755

756
      // Shutdown filters not present in current HCM.
757
      for (String filterKey : filtersToShutdown) {
1✔
758
        Filter filterToShutdown = activeFilters.remove(filterKey);
1✔
759
        checkNotNull(filterToShutdown, "filterToShutdown %s", filterKey);
1✔
760
        filterToShutdown.close();
1✔
761
      }
1✔
762
    }
1✔
763

764
    private void updateRoutes(
765
        XdsConfig xdsConfig,
766
        @Nullable VirtualHost virtualHost,
767
        long httpMaxStreamDurationNano,
768
        @Nullable List<NamedFilterConfig> filterConfigs) {
769
      List<Route> routes = virtualHost.routes();
1✔
770
      ImmutableList.Builder<RouteData> routesData = ImmutableList.builder();
1✔
771

772
      // Populate all clusters to which requests can be routed to through the virtual host.
773
      Set<String> clusters = new HashSet<>();
1✔
774
      // uniqueName -> clusterName
775
      Map<String, String> clusterNameMap = new HashMap<>();
1✔
776
      // uniqueName -> pluginConfig
777
      Map<String, RlsPluginConfig> rlsPluginConfigMap = new HashMap<>();
1✔
778
      for (Route route : routes) {
1✔
779
        RouteAction action = route.routeAction();
1✔
780
        String prefixedName;
781
        if (action == null) {
1✔
782
          routesData.add(new RouteData(route.routeMatch(), null, ImmutableList.of()));
1✔
783
        } else if (action.cluster() != null) {
1✔
784
          prefixedName = prefixedClusterName(action.cluster());
1✔
785
          clusters.add(prefixedName);
1✔
786
          clusterNameMap.put(prefixedName, action.cluster());
1✔
787
          ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null);
1✔
788
          routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters));
1✔
789
        } else if (action.weightedClusters() != null) {
1✔
790
          ImmutableList.Builder<ClientInterceptor> filterList = ImmutableList.builder();
1✔
791
          for (ClusterWeight weightedCluster : action.weightedClusters()) {
1✔
792
            prefixedName = prefixedClusterName(weightedCluster.name());
1✔
793
            clusters.add(prefixedName);
1✔
794
            clusterNameMap.put(prefixedName, weightedCluster.name());
1✔
795
            filterList.add(createFilters(filterConfigs, virtualHost, route, weightedCluster));
1✔
796
          }
1✔
797
          routesData.add(
1✔
798
              new RouteData(route.routeMatch(), route.routeAction(), filterList.build()));
1✔
799
        } else if (action.namedClusterSpecifierPluginConfig() != null) {
1✔
800
          PluginConfig pluginConfig = action.namedClusterSpecifierPluginConfig().config();
1✔
801
          if (pluginConfig instanceof RlsPluginConfig) {
1✔
802
            prefixedName = prefixedClusterSpecifierPluginName(
1✔
803
                action.namedClusterSpecifierPluginConfig().name());
1✔
804
            clusters.add(prefixedName);
1✔
805
            rlsPluginConfigMap.put(prefixedName, (RlsPluginConfig) pluginConfig);
1✔
806
          }
807
          ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null);
1✔
808
          routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters));
1✔
809
        } else {
810
          // Discard route
811
        }
812
      }
1✔
813

814
      // Updates channel's load balancing config whenever the set of selectable clusters changes.
815
      boolean shouldUpdateResult = existingClusters == null;
1✔
816
      Set<String> addedClusters =
817
          existingClusters == null ? clusters : Sets.difference(clusters, existingClusters);
1✔
818
      Set<String> deletedClusters =
819
          existingClusters == null
1✔
820
              ? Collections.emptySet() : Sets.difference(existingClusters, clusters);
1✔
821
      existingClusters = clusters;
1✔
822
      for (String cluster : addedClusters) {
1✔
823
        if (clusterRefs.containsKey(cluster)) {
1✔
824
          clusterRefs.get(cluster).refCount.incrementAndGet();
1✔
825
        } else {
826
          if (clusterNameMap.containsKey(cluster)) {
1✔
827
            assert cluster.startsWith("cluster:");
1✔
828
            XdsConfig.Subscription subscription =
1✔
829
                xdsDependencyManager.subscribeToCluster(cluster.substring("cluster:".length()));
1✔
830
            clusterRefs.put(
1✔
831
                cluster,
832
                ClusterRefState.forCluster(
1✔
833
                    new AtomicInteger(1), clusterNameMap.get(cluster), subscription));
1✔
834
          }
835
          if (rlsPluginConfigMap.containsKey(cluster)) {
1✔
836
            clusterRefs.put(
1✔
837
                cluster,
838
                ClusterRefState.forRlsPlugin(
1✔
839
                    new AtomicInteger(1), rlsPluginConfigMap.get(cluster)));
1✔
840
          }
841
          shouldUpdateResult = true;
1✔
842
        }
843
      }
1✔
844
      for (String cluster : clusters) {
1✔
845
        RlsPluginConfig rlsPluginConfig = rlsPluginConfigMap.get(cluster);
1✔
846
        if (!Objects.equals(rlsPluginConfig, clusterRefs.get(cluster).rlsPluginConfig)) {
1✔
847
          ClusterRefState newClusterRefState =
1✔
848
              ClusterRefState.forRlsPlugin(clusterRefs.get(cluster).refCount, rlsPluginConfig);
1✔
849
          clusterRefs.put(cluster, newClusterRefState);
1✔
850
          shouldUpdateResult = true;
1✔
851
        }
852
      }
1✔
853
      // Update service config to include newly added clusters.
854
      if (shouldUpdateResult && routingConfig != null) {
1✔
855
        updateResolutionResult(xdsConfig);
1✔
856
        shouldUpdateResult = false;
1✔
857
      } else {
858
        // Need to update at least once
859
        shouldUpdateResult = true;
1✔
860
      }
861
      // Make newly added clusters selectable by config selector and deleted clusters no longer
862
      // selectable.
863
      routingConfig = new RoutingConfig(xdsConfig, httpMaxStreamDurationNano, routesData.build());
1✔
864
      for (String cluster : deletedClusters) {
1✔
865
        int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
866
        if (count == 0) {
1✔
867
          clusterRefs.remove(cluster).close();
1✔
868
          shouldUpdateResult = true;
1✔
869
        }
870
      }
1✔
871
      if (shouldUpdateResult) {
1✔
872
        updateResolutionResult(xdsConfig);
1✔
873
      }
874
    }
1✔
875

876
    private ClientInterceptor createFilters(
877
        @Nullable List<NamedFilterConfig> filterConfigs,
878
        VirtualHost virtualHost,
879
        Route route,
880
        @Nullable ClusterWeight weightedCluster) {
881
      if (filterConfigs == null) {
1✔
882
        return new PassthroughClientInterceptor();
1✔
883
      }
884

885
      Map<String, FilterConfig> selectedOverrideConfigs =
1✔
886
          new HashMap<>(virtualHost.filterConfigOverrides());
1✔
887
      selectedOverrideConfigs.putAll(route.filterConfigOverrides());
1✔
888
      if (weightedCluster != null) {
1✔
889
        selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides());
1✔
890
      }
891

892
      ImmutableList.Builder<ClientInterceptor> filterInterceptors = ImmutableList.builder();
1✔
893
      for (NamedFilterConfig namedFilter : filterConfigs) {
1✔
894
        String name = namedFilter.name;
1✔
895
        FilterConfig config = namedFilter.filterConfig;
1✔
896
        FilterConfig overrideConfig = selectedOverrideConfigs.get(name);
1✔
897
        String filterKey = namedFilter.filterStateKey();
1✔
898

899
        Filter filter = activeFilters.get(filterKey);
1✔
900
        checkNotNull(filter, "activeFilters.get(%s)", filterKey);
1✔
901
        ClientInterceptor interceptor =
1✔
902
            filter.buildClientInterceptor(config, overrideConfig, scheduler);
1✔
903

904
        if (interceptor != null) {
1✔
905
          filterInterceptors.add(interceptor);
1✔
906
        }
907
      }
1✔
908

909
      ImmutableList.Builder<ClientInterceptor> withRawMessage = ImmutableList.builder();
1✔
910
      withRawMessage.add(new RawMessageClientInterceptor());
1✔
911
      withRawMessage.addAll(filterInterceptors.build());
1✔
912
      return combineInterceptors(withRawMessage.build());
1✔
913
    }
914

915
    private void cleanUpRoutes(Status error) {
916
      routingConfig = new RoutingConfig(error);
1✔
917
      if (existingClusters != null) {
1✔
918
        for (String cluster : existingClusters) {
1✔
919
          int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
920
          if (count == 0) {
1✔
921
            clusterRefs.remove(cluster).close();
1✔
922
          }
923
        }
1✔
924
        existingClusters = null;
1✔
925
      }
926

927
      // Without addresses the default LB (normally pick_first) should become TRANSIENT_FAILURE, and
928
      // the config selector handles the error message itself.
929
      listener.onResult2(ResolutionResult.newBuilder()
1✔
930
          .setAttributes(Attributes.newBuilder()
1✔
931
              .set(InternalConfigSelector.KEY, configSelector)
1✔
932
              .build())
1✔
933
          .setServiceConfig(emptyServiceConfig)
1✔
934
          .build());
1✔
935
    }
1✔
936
  }
937

938
  /**
939
   * VirtualHost-level configuration for request routing.
940
   */
941
  private static class RoutingConfig {
942
    final XdsConfig xdsConfig;
943
    final long fallbackTimeoutNano;
944
    final ImmutableList<RouteData> routes;
945
    final Status errorStatus;
946

947
    private RoutingConfig(
948
        XdsConfig xdsConfig, long fallbackTimeoutNano, ImmutableList<RouteData> routes) {
1✔
949
      this.xdsConfig = checkNotNull(xdsConfig, "xdsConfig");
1✔
950
      this.fallbackTimeoutNano = fallbackTimeoutNano;
1✔
951
      this.routes = checkNotNull(routes, "routes");
1✔
952
      this.errorStatus = null;
1✔
953
    }
1✔
954

955
    private RoutingConfig(Status errorStatus) {
1✔
956
      this.xdsConfig = null;
1✔
957
      this.fallbackTimeoutNano = 0;
1✔
958
      this.routes = null;
1✔
959
      this.errorStatus = checkNotNull(errorStatus, "errorStatus");
1✔
960
      checkArgument(!errorStatus.isOk(), "errorStatus should not be okay");
1✔
961
    }
1✔
962
  }
963

964
  static final class RouteData {
965
    final RouteMatch routeMatch;
966
    /** null implies non-forwarding action. */
967
    @Nullable
968
    final RouteAction routeAction;
969
    /**
970
     * Only one of these interceptors should be used per-RPC. There are only multiple values in the
971
     * list for weighted clusters, in which case the order of the list mirrors the weighted
972
     * clusters.
973
     */
974
    final ImmutableList<ClientInterceptor> filterChoices;
975

976
    RouteData(RouteMatch routeMatch, @Nullable RouteAction routeAction, ClientInterceptor filter) {
977
      this(routeMatch, routeAction, ImmutableList.of(filter));
1✔
978
    }
1✔
979

980
    RouteData(
981
        RouteMatch routeMatch,
982
        @Nullable RouteAction routeAction,
983
        ImmutableList<ClientInterceptor> filterChoices) {
1✔
984
      this.routeMatch = checkNotNull(routeMatch, "routeMatch");
1✔
985
      checkArgument(
1✔
986
          routeAction == null || !filterChoices.isEmpty(),
1✔
987
          "filter may be empty only for non-forwarding action");
988
      this.routeAction = routeAction;
1✔
989
      if (routeAction != null && routeAction.weightedClusters() != null) {
1✔
990
        checkArgument(
1✔
991
            routeAction.weightedClusters().size() == filterChoices.size(),
1✔
992
            "filter choices must match size of weighted clusters");
993
      }
994
      for (ClientInterceptor filter : filterChoices) {
1✔
995
        checkNotNull(filter, "entry in filterChoices is null");
1✔
996
      }
1✔
997
      this.filterChoices = checkNotNull(filterChoices, "filterChoices");
1✔
998
    }
1✔
999
  }
1000

1001
  private static class ClusterRefState {
1002
    final AtomicInteger refCount;
1003
    @Nullable
1004
    final String traditionalCluster;
1005
    @Nullable
1006
    final RlsPluginConfig rlsPluginConfig;
1007
    @Nullable
1008
    final XdsConfig.Subscription subscription;
1009

1010
    private ClusterRefState(
1011
        AtomicInteger refCount, @Nullable String traditionalCluster,
1012
        @Nullable RlsPluginConfig rlsPluginConfig, @Nullable XdsConfig.Subscription subscription) {
1✔
1013
      this.refCount = refCount;
1✔
1014
      checkArgument(traditionalCluster == null ^ rlsPluginConfig == null,
1✔
1015
          "There must be exactly one non-null value in traditionalCluster and pluginConfig");
1016
      this.traditionalCluster = traditionalCluster;
1✔
1017
      this.rlsPluginConfig = rlsPluginConfig;
1✔
1018
      this.subscription = subscription;
1✔
1019
    }
1✔
1020

1021
    private Map<String, ?> toLbPolicy() {
1022
      if (traditionalCluster != null) {
1✔
1023
        return ImmutableMap.of(
1✔
1024
            XdsLbPolicies.CDS_POLICY_NAME,
1025
            ImmutableMap.of("cluster", traditionalCluster));
1✔
1026
      } else {
1027
        ImmutableMap<String, ?> rlsConfig = new ImmutableMap.Builder<String, Object>()
1✔
1028
            .put("routeLookupConfig", rlsPluginConfig.config())
1✔
1029
            .put(
1✔
1030
                "childPolicy",
1031
                ImmutableList.of(ImmutableMap.of(XdsLbPolicies.CDS_POLICY_NAME, ImmutableMap.of(
1✔
1032
                    "is_dynamic", true))))
1✔
1033
            .put("childPolicyConfigTargetFieldName", "cluster")
1✔
1034
            .buildOrThrow();
1✔
1035
        return ImmutableMap.of("rls_experimental", rlsConfig);
1✔
1036
      }
1037
    }
1038

1039
    private void close() {
1040
      if (subscription != null) {
1✔
1041
        subscription.close();
1✔
1042
      }
1043
    }
1✔
1044

1045
    static ClusterRefState forCluster(
1046
        AtomicInteger refCount, String name, XdsConfig.Subscription subscription) {
1047
      return new ClusterRefState(refCount, name, null, checkNotNull(subscription, "subscription"));
1✔
1048
    }
1049

1050
    static ClusterRefState forRlsPlugin(
1051
        AtomicInteger refCount,
1052
        RlsPluginConfig rlsPluginConfig) {
1053
      return new ClusterRefState(refCount, null, rlsPluginConfig, null);
1✔
1054
    }
1055
  }
1056

1057
  /** An ObjectPool, except it can throw an exception. */
1058
  private interface XdsClientPool {
1059
    XdsClient getObject() throws XdsInitializationException;
1060

1061
    XdsClient returnObject(XdsClient xdsClient);
1062
  }
1063

1064
  private static final class BootstrappingXdsClientPool implements XdsClientPool {
1065
    private final XdsClientPoolFactory xdsClientPoolFactory;
1066
    private final String target;
1067
    private final @Nullable Map<String, ?> bootstrapOverride;
1068
    private final MetricRecorder metricRecorder;
1069
    private ObjectPool<XdsClient> xdsClientPool;
1070
    private final ChannelConfigurator channelConfigurator;
1071

1072
    BootstrappingXdsClientPool(
1073
        XdsClientPoolFactory xdsClientPoolFactory,
1074
        String target,
1075
        @Nullable Map<String, ?> bootstrapOverride,
1076
        MetricRecorder metricRecorder,
1077
        ChannelConfigurator channelConfigurator) {
1✔
1078
      this.xdsClientPoolFactory = checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory");
1✔
1079
      this.target = checkNotNull(target, "target");
1✔
1080
      this.bootstrapOverride = bootstrapOverride;
1✔
1081
      this.metricRecorder = metricRecorder;
1✔
1082
      this.channelConfigurator = checkNotNull(channelConfigurator, "channelConfigurator");
1✔
1083
    }
1✔
1084

1085
    @Override
1086
    public XdsClient getObject() throws XdsInitializationException {
1087
      if (xdsClientPool == null) {
1✔
1088
        BootstrapInfo bootstrapInfo;
1089
        if (bootstrapOverride == null) {
1✔
1090
          bootstrapInfo = GrpcBootstrapperImpl.defaultBootstrap();
×
1091
        } else {
1092
          bootstrapInfo = new GrpcBootstrapperImpl().bootstrap(bootstrapOverride);
1✔
1093
        }
1094
        this.xdsClientPool =
1✔
1095
            xdsClientPoolFactory.getOrCreate(
1✔
1096
                target, bootstrapInfo, metricRecorder, channelConfigurator);
1097
      }
1098
      return xdsClientPool.getObject();
1✔
1099
    }
1100

1101
    @Override
1102
    public XdsClient returnObject(XdsClient xdsClient) {
1103
      return xdsClientPool.returnObject(xdsClient);
1✔
1104
    }
1105
  }
1106

1107
  private static final class SupplierXdsClientPool implements XdsClientPool {
1108
    private final Supplier<XdsClient> xdsClientSupplier;
1109

1110
    SupplierXdsClientPool(Supplier<XdsClient> xdsClientSupplier) {
×
1111
      this.xdsClientSupplier = checkNotNull(xdsClientSupplier, "xdsClientSupplier");
×
1112
    }
×
1113

1114
    @Override
1115
    public XdsClient getObject() throws XdsInitializationException {
1116
      XdsClient xdsClient = xdsClientSupplier.get();
×
1117
      if (xdsClient == null) {
×
1118
        throw new XdsInitializationException("Caller failed to initialize XDS_CLIENT_SUPPLIER");
×
1119
      }
1120
      return xdsClient;
×
1121
    }
1122

1123
    @Override
1124
    public XdsClient returnObject(XdsClient xdsClient) {
1125
      return null;
×
1126
    }
1127
  }
1128

1129
  static final class RawMessageClientInterceptor implements ClientInterceptor {
1✔
1130
    private static final MethodDescriptor.Marshaller<InputStream> RAW_MARSHALLER =
1✔
1131
        new MethodDescriptor.Marshaller<InputStream>() {
1✔
1132
          @Override
1133
          public InputStream stream(InputStream value) {
1134
            return value;
1✔
1135
          }
1136

1137
          @Override
1138
          public InputStream parse(InputStream stream) {
1139
            return stream;
1✔
1140
          }
1141
        };
1142

1143
    @Override
1144
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
1145
        final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
1146
      MethodDescriptor<InputStream, InputStream> rawMethod =
1✔
1147
          method.toBuilder(RAW_MARSHALLER, RAW_MARSHALLER).build();
1✔
1148
      final ClientCall<InputStream, InputStream> rawCall = next.newCall(rawMethod, callOptions);
1✔
1149
      return new ClientCall<ReqT, RespT>() {
1✔
1150
        @Override
1151
        public void start(final Listener<RespT> responseListener, Metadata headers) {
1152
          rawCall.start(new Listener<InputStream>() {
1✔
1153
            @Override
1154
            public void onHeaders(Metadata headers) {
1155
              responseListener.onHeaders(headers);
1✔
1156
            }
1✔
1157

1158
            @Override
1159
            public void onMessage(InputStream message) {
1160
              responseListener.onMessage(method.getResponseMarshaller().parse(message));
1✔
1161
            }
1✔
1162

1163
            @Override
1164
            public void onClose(Status status, Metadata trailers) {
1165
              responseListener.onClose(status, trailers);
1✔
1166
            }
1✔
1167

1168
            @Override
1169
            public void onReady() {
1170
              responseListener.onReady();
1✔
1171
            }
1✔
1172
          }, headers);
1173
        }
1✔
1174

1175
        @Override
1176
        public void request(int numMessages) {
1177
          rawCall.request(numMessages);
1✔
1178
        }
1✔
1179

1180
        @Override
1181
        public void cancel(@Nullable String message, @Nullable Throwable cause) {
1182
          rawCall.cancel(message, cause);
1✔
1183
        }
1✔
1184

1185
        @Override
1186
        public void halfClose() {
1187
          rawCall.halfClose();
1✔
1188
        }
1✔
1189

1190
        @Override
1191
        public void sendMessage(ReqT message) {
1192
          rawCall.sendMessage(method.getRequestMarshaller().stream(message));
1✔
1193
        }
1✔
1194

1195
        @Override
1196
        public boolean isReady() {
1197
          return rawCall.isReady();
1✔
1198
        }
1199

1200
        @Override
1201
        public void setMessageCompression(boolean enabled) {
1202
          rawCall.setMessageCompression(enabled);
×
1203
        }
×
1204

1205
        @Override
1206
        public Attributes getAttributes() {
1207
          return rawCall.getAttributes();
×
1208
        }
1209
      };
1210
    }
1211
  }
1212
}
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