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

grpc / grpc-java / #19743

19 Mar 2025 08:47PM UTC coverage: 88.588% (-0.02%) from 88.608%
#19743

push

github

ejona86
xds: Assert XdsNR's cluster ref counting is consistent

It is much harder to debug refcounting problems when we ignore
impossible situations. So make such impossible cases complain loudly so
the bug is obvious.

34592 of 39048 relevant lines covered (88.59%)

0.89 hits per line

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

95.0
/../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.XdsLogger;
68
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
69
import java.net.URI;
70
import java.util.ArrayList;
71
import java.util.Collections;
72
import java.util.HashMap;
73
import java.util.HashSet;
74
import java.util.List;
75
import java.util.Locale;
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 javax.annotation.Nullable;
84

85
/**
86
 * A {@link NameResolver} for resolving gRPC target names with "xds:" scheme.
87
 *
88
 * <p>Resolving a gRPC target involves contacting the control plane management server via xDS
89
 * protocol to retrieve service information and produce a service config to the caller.
90
 *
91
 * @see XdsNameResolverProvider
92
 */
93
final class XdsNameResolver extends NameResolver {
94

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 XdsClientPoolFactory xdsClientPoolFactory;
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 MetricRecorder metricRecorder;
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 ObjectPool<XdsClient> xdsClientPool;
140
  private XdsClient xdsClient;
141
  private CallCounterProvider callCounterProvider;
142
  private ResolveState resolveState;
143

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

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

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

172
    this.overrideAuthority = overrideAuthority;
1✔
173
    this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser");
1✔
174
    this.syncContext = checkNotNull(syncContext, "syncContext");
1✔
175
    this.scheduler = checkNotNull(scheduler, "scheduler");
1✔
176
    this.xdsClientPoolFactory = bootstrapOverride == null ? checkNotNull(xdsClientPoolFactory,
1✔
177
            "xdsClientPoolFactory") : new SharedXdsClientPoolProvider();
1✔
178
    this.xdsClientPoolFactory.setBootstrapOverride(bootstrapOverride);
1✔
179
    this.random = checkNotNull(random, "random");
1✔
180
    this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry");
1✔
181
    this.metricRecorder = metricRecorder;
1✔
182
    this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs");
1✔
183

184
    randomChannelId = random.nextLong();
1✔
185
    logId = InternalLogId.allocate("xds-resolver", name);
1✔
186
    logger = XdsLogger.withLogId(logId);
1✔
187
    logger.log(XdsLogLevel.INFO, "Created resolver for {0}", name);
1✔
188
  }
1✔
189

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

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

233
    resolveState = new ResolveState(ldsResourceName); // auto starts
1✔
234
  }
1✔
235

236
  private static String expandPercentS(String template, String replacement) {
237
    return template.replace("%s", replacement);
1✔
238
  }
239

240
  @Override
241
  public void shutdown() {
242
    logger.log(XdsLogLevel.INFO, "Shutdown");
1✔
243
    if (resolveState != null) {
1✔
244
      resolveState.shutdown();
1✔
245
    }
246
    if (xdsClient != null) {
1✔
247
      xdsClient = xdsClientPool.returnObject(xdsClient);
1✔
248
    }
249
  }
1✔
250

251
  @VisibleForTesting
252
  static Map<String, ?> generateServiceConfigWithMethodConfig(
253
      @Nullable Long timeoutNano, @Nullable RetryPolicy retryPolicy) {
254
    if (timeoutNano == null
1✔
255
        && (retryPolicy == null || retryPolicy.retryableStatusCodes().isEmpty())) {
1✔
256
      return Collections.emptyMap();
1✔
257
    }
258
    ImmutableMap.Builder<String, Object> methodConfig = ImmutableMap.builder();
1✔
259
    methodConfig.put(
1✔
260
        "name", Collections.singletonList(Collections.emptyMap()));
1✔
261
    if (retryPolicy != null && !retryPolicy.retryableStatusCodes().isEmpty()) {
1✔
262
      ImmutableMap.Builder<String, Object> rawRetryPolicy = ImmutableMap.builder();
1✔
263
      rawRetryPolicy.put("maxAttempts", (double) retryPolicy.maxAttempts());
1✔
264
      rawRetryPolicy.put("initialBackoff", Durations.toString(retryPolicy.initialBackoff()));
1✔
265
      rawRetryPolicy.put("maxBackoff", Durations.toString(retryPolicy.maxBackoff()));
1✔
266
      rawRetryPolicy.put("backoffMultiplier", 2D);
1✔
267
      List<String> codes = new ArrayList<>(retryPolicy.retryableStatusCodes().size());
1✔
268
      for (Code code : retryPolicy.retryableStatusCodes()) {
1✔
269
        codes.add(code.name());
1✔
270
      }
1✔
271
      rawRetryPolicy.put(
1✔
272
          "retryableStatusCodes", Collections.unmodifiableList(codes));
1✔
273
      if (retryPolicy.perAttemptRecvTimeout() != null) {
1✔
274
        rawRetryPolicy.put(
×
275
            "perAttemptRecvTimeout", Durations.toString(retryPolicy.perAttemptRecvTimeout()));
×
276
      }
277
      methodConfig.put("retryPolicy", rawRetryPolicy.buildOrThrow());
1✔
278
    }
279
    if (timeoutNano != null) {
1✔
280
      String timeout = timeoutNano / 1_000_000_000.0 + "s";
1✔
281
      methodConfig.put("timeout", timeout);
1✔
282
    }
283
    return Collections.singletonMap(
1✔
284
        "methodConfig", Collections.singletonList(methodConfig.buildOrThrow()));
1✔
285
  }
286

287
  @VisibleForTesting
288
  XdsClient getXdsClient() {
289
    return xdsClient;
1✔
290
  }
291

292
  // called in syncContext
293
  private void updateResolutionResult(XdsConfig xdsConfig) {
294
    syncContext.throwIfNotInThisSynchronizationContext();
1✔
295

296
    ImmutableMap.Builder<String, Object> childPolicy = new ImmutableMap.Builder<>();
1✔
297
    for (String name : clusterRefs.keySet()) {
1✔
298
      Map<String, ?> lbPolicy = clusterRefs.get(name).toLbPolicy();
1✔
299
      childPolicy.put(name, ImmutableMap.of("lbPolicy", ImmutableList.of(lbPolicy)));
1✔
300
    }
1✔
301
    Map<String, ?> rawServiceConfig = ImmutableMap.of(
1✔
302
        "loadBalancingConfig",
303
        ImmutableList.of(ImmutableMap.of(
1✔
304
            XdsLbPolicies.CLUSTER_MANAGER_POLICY_NAME,
305
            ImmutableMap.of("childPolicy", childPolicy.buildOrThrow()))));
1✔
306

307
    if (logger.isLoggable(XdsLogLevel.INFO)) {
1✔
308
      logger.log(
×
309
          XdsLogLevel.INFO, "Generated service config: {0}", new Gson().toJson(rawServiceConfig));
×
310
    }
311
    ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
1✔
312
    Attributes attrs =
313
        Attributes.newBuilder()
1✔
314
            .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
1✔
315
            .set(XdsAttributes.XDS_CONFIG, xdsConfig)
1✔
316
            .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, resolveState.xdsDependencyManager)
1✔
317
            .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
1✔
318
            .set(InternalConfigSelector.KEY, configSelector)
1✔
319
            .build();
1✔
320
    ResolutionResult result =
321
        ResolutionResult.newBuilder()
1✔
322
            .setAttributes(attrs)
1✔
323
            .setServiceConfig(parsedServiceConfig)
1✔
324
            .build();
1✔
325
    listener.onResult2(result);
1✔
326
  }
1✔
327

328
  /**
329
   * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with
330
   * case-insensitive.
331
   *
332
   * <p>Wildcard pattern rules:
333
   * <ol>
334
   * <li>A single asterisk (*) matches any domain.</li>
335
   * <li>Asterisk (*) is only permitted in the left-most or the right-most part of the pattern,
336
   *     but not both.</li>
337
   * </ol>
338
   */
339
  @VisibleForTesting
340
  static boolean matchHostName(String hostName, String pattern) {
341
    checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."),
1✔
342
        "Invalid host name");
343
    checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."),
1✔
344
        "Invalid pattern/domain name");
345

346
    hostName = hostName.toLowerCase(Locale.US);
1✔
347
    pattern = pattern.toLowerCase(Locale.US);
1✔
348
    // hostName and pattern are now in lower case -- domain names are case-insensitive.
349

350
    if (!pattern.contains("*")) {
1✔
351
      // Not a wildcard pattern -- hostName and pattern must match exactly.
352
      return hostName.equals(pattern);
1✔
353
    }
354
    // Wildcard pattern
355

356
    if (pattern.length() == 1) {
1✔
357
      return true;
×
358
    }
359

360
    int index = pattern.indexOf('*');
1✔
361

362
    // At most one asterisk (*) is allowed.
363
    if (pattern.indexOf('*', index + 1) != -1) {
1✔
364
      return false;
×
365
    }
366

367
    // Asterisk can only match prefix or suffix.
368
    if (index != 0 && index != pattern.length() - 1) {
1✔
369
      return false;
×
370
    }
371

372
    // HostName must be at least as long as the pattern because asterisk has to
373
    // match one or more characters.
374
    if (hostName.length() < pattern.length()) {
1✔
375
      return false;
1✔
376
    }
377

378
    if (index == 0 && hostName.endsWith(pattern.substring(1))) {
1✔
379
      // Prefix matching fails.
380
      return true;
1✔
381
    }
382

383
    // Pattern matches hostname if suffix matching succeeds.
384
    return index == pattern.length() - 1
1✔
385
        && hostName.startsWith(pattern.substring(0, pattern.length() - 1));
1✔
386
  }
387

388
  private final class ConfigSelector extends InternalConfigSelector {
1✔
389
    @Override
390
    public Result selectConfig(PickSubchannelArgs args) {
391
      RoutingConfig routingCfg;
392
      RouteData selectedRoute;
393
      String cluster;
394
      ClientInterceptor filters;
395
      Metadata headers = args.getHeaders();
1✔
396
      String path = "/" + args.getMethodDescriptor().getFullMethodName();
1✔
397
      do {
398
        routingCfg = routingConfig;
1✔
399
        if (routingCfg.errorStatus != null) {
1✔
400
          return Result.forError(routingCfg.errorStatus);
1✔
401
        }
402
        selectedRoute = null;
1✔
403
        for (RouteData route : routingCfg.routes) {
1✔
404
          if (RoutingUtils.matchRoute(route.routeMatch, path, headers, random)) {
1✔
405
            selectedRoute = route;
1✔
406
            break;
1✔
407
          }
408
        }
1✔
409
        if (selectedRoute == null) {
1✔
410
          return Result.forError(
1✔
411
              Status.UNAVAILABLE.withDescription("Could not find xDS route matching RPC"));
1✔
412
        }
413
        if (selectedRoute.routeAction == null) {
1✔
414
          return Result.forError(Status.UNAVAILABLE.withDescription(
1✔
415
              "Could not route RPC to Route with non-forwarding action"));
416
        }
417
        RouteAction action = selectedRoute.routeAction;
1✔
418
        if (action.cluster() != null) {
1✔
419
          cluster = prefixedClusterName(action.cluster());
1✔
420
          filters = selectedRoute.filterChoices.get(0);
1✔
421
        } else if (action.weightedClusters() != null) {
1✔
422
          // XdsRouteConfigureResource verifies the total weight will not be 0 or exceed uint32
423
          long totalWeight = 0;
1✔
424
          for (ClusterWeight weightedCluster : action.weightedClusters()) {
1✔
425
            totalWeight += weightedCluster.weight();
1✔
426
          }
1✔
427
          long select = random.nextLong(totalWeight);
1✔
428
          long accumulator = 0;
1✔
429
          for (int i = 0; ; i++) {
1✔
430
            ClusterWeight weightedCluster = action.weightedClusters().get(i);
1✔
431
            accumulator += weightedCluster.weight();
1✔
432
            if (select < accumulator) {
1✔
433
              cluster = prefixedClusterName(weightedCluster.name());
1✔
434
              filters = selectedRoute.filterChoices.get(i);
1✔
435
              break;
1✔
436
            }
437
          }
438
        } else if (action.namedClusterSpecifierPluginConfig() != null) {
1✔
439
          cluster =
1✔
440
              prefixedClusterSpecifierPluginName(action.namedClusterSpecifierPluginConfig().name());
1✔
441
          filters = selectedRoute.filterChoices.get(0);
1✔
442
        } else {
443
          // updateRoutes() discards routes with unknown actions
444
          throw new AssertionError();
×
445
        }
446
      } while (!retainCluster(cluster));
1✔
447

448
      final RouteAction routeAction = selectedRoute.routeAction;
1✔
449
      Long timeoutNanos = null;
1✔
450
      if (enableTimeout) {
1✔
451
        timeoutNanos = routeAction.timeoutNano();
1✔
452
        if (timeoutNanos == null) {
1✔
453
          timeoutNanos = routingCfg.fallbackTimeoutNano;
1✔
454
        }
455
        if (timeoutNanos <= 0) {
1✔
456
          timeoutNanos = null;
1✔
457
        }
458
      }
459
      RetryPolicy retryPolicy = routeAction.retryPolicy();
1✔
460
      // TODO(chengyuanzhang): avoid service config generation and parsing for each call.
461
      Map<String, ?> rawServiceConfig =
1✔
462
          generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy);
1✔
463
      ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
1✔
464
      Object config = parsedServiceConfig.getConfig();
1✔
465
      if (config == null) {
1✔
466
        releaseCluster(cluster);
×
467
        return Result.forError(
×
468
            parsedServiceConfig.getError().augmentDescription(
×
469
                "Failed to parse service config (method config)"));
470
      }
471
      final String finalCluster = cluster;
1✔
472
      final XdsConfig xdsConfig = routingCfg.xdsConfig;
1✔
473
      final long hash = generateHash(routeAction.hashPolicies(), headers);
1✔
474
      class ClusterSelectionInterceptor implements ClientInterceptor {
1✔
475
        @Override
476
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
477
            final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
478
            final Channel next) {
479
          CallOptions callOptionsForCluster =
1✔
480
              callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster)
1✔
481
                  .withOption(XDS_CONFIG_CALL_OPTION_KEY, xdsConfig)
1✔
482
                  .withOption(RPC_HASH_KEY, hash);
1✔
483
          if (routeAction.autoHostRewrite()) {
1✔
484
            callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true);
1✔
485
          }
486
          return new SimpleForwardingClientCall<ReqT, RespT>(
1✔
487
              next.newCall(method, callOptionsForCluster)) {
1✔
488
            @Override
489
            public void start(Listener<RespT> listener, Metadata headers) {
490
              listener = new SimpleForwardingClientCallListener<RespT>(listener) {
1✔
491
                boolean committed;
492

493
                @Override
494
                public void onHeaders(Metadata headers) {
495
                  committed = true;
1✔
496
                  releaseCluster(finalCluster);
1✔
497
                  delegate().onHeaders(headers);
1✔
498
                }
1✔
499

500
                @Override
501
                public void onClose(Status status, Metadata trailers) {
502
                  if (!committed) {
1✔
503
                    releaseCluster(finalCluster);
1✔
504
                  }
505
                  delegate().onClose(status, trailers);
1✔
506
                }
1✔
507
              };
508
              delegate().start(listener, headers);
1✔
509
            }
1✔
510
          };
511
        }
512
      }
513

514
      return
1✔
515
          Result.newBuilder()
1✔
516
              .setConfig(config)
1✔
517
              .setInterceptor(combineInterceptors(
1✔
518
                  ImmutableList.of(filters, new ClusterSelectionInterceptor())))
1✔
519
              .build();
1✔
520
    }
521

522
    private boolean retainCluster(String cluster) {
523
      ClusterRefState clusterRefState = clusterRefs.get(cluster);
1✔
524
      if (clusterRefState == null) {
1✔
525
        return false;
×
526
      }
527
      AtomicInteger refCount = clusterRefState.refCount;
1✔
528
      int count;
529
      do {
530
        count = refCount.get();
1✔
531
        if (count == 0) {
1✔
532
          return false;
×
533
        }
534
      } while (!refCount.compareAndSet(count, count + 1));
1✔
535
      return true;
1✔
536
    }
537

538
    private void releaseCluster(final String cluster) {
539
      int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
540
      if (count < 0) {
1✔
541
        throw new AssertionError();
×
542
      }
543
      if (count == 0) {
1✔
544
        syncContext.execute(new Runnable() {
1✔
545
          @Override
546
          public void run() {
547
            if (clusterRefs.get(cluster).refCount.get() != 0) {
1✔
548
              throw new AssertionError();
×
549
            }
550
            clusterRefs.remove(cluster);
1✔
551
            if (resolveState.lastConfigOrStatus.hasValue()) {
1✔
552
              updateResolutionResult(resolveState.lastConfigOrStatus.getValue());
1✔
553
            } else {
554
              resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus());
×
555
            }
556
          }
1✔
557
        });
558
      }
559
    }
1✔
560

561
    private long generateHash(List<HashPolicy> hashPolicies, Metadata headers) {
562
      Long hash = null;
1✔
563
      for (HashPolicy policy : hashPolicies) {
1✔
564
        Long newHash = null;
1✔
565
        if (policy.type() == HashPolicy.Type.HEADER) {
1✔
566
          String value = getHeaderValue(headers, policy.headerName());
1✔
567
          if (value != null) {
1✔
568
            if (policy.regEx() != null && policy.regExSubstitution() != null) {
1✔
569
              value = policy.regEx().matcher(value).replaceAll(policy.regExSubstitution());
1✔
570
            }
571
            newHash = hashFunc.hashAsciiString(value);
1✔
572
          }
573
        } else if (policy.type() == HashPolicy.Type.CHANNEL_ID) {
1✔
574
          newHash = hashFunc.hashLong(randomChannelId);
1✔
575
        }
576
        if (newHash != null ) {
1✔
577
          // Rotating the old value prevents duplicate hash rules from cancelling each other out
578
          // and preserves all of the entropy.
579
          long oldHash = hash != null ? ((hash << 1L) | (hash >> 63L)) : 0;
1✔
580
          hash = oldHash ^ newHash;
1✔
581
        }
582
        // If the policy is a terminal policy and a hash has been generated, ignore
583
        // the rest of the hash policies.
584
        if (policy.isTerminal() && hash != null) {
1✔
585
          break;
×
586
        }
587
      }
1✔
588
      return hash == null ? random.nextLong() : hash;
1✔
589
    }
590
  }
591

592
  static final class PassthroughClientInterceptor implements ClientInterceptor {
1✔
593
    @Override
594
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
595
        MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
596
      return next.newCall(method, callOptions);
1✔
597
    }
598
  }
599

600
  private static ClientInterceptor combineInterceptors(final List<ClientInterceptor> interceptors) {
601
    if (interceptors.size() == 0) {
1✔
602
      return new PassthroughClientInterceptor();
1✔
603
    }
604
    if (interceptors.size() == 1) {
1✔
605
      return interceptors.get(0);
1✔
606
    }
607
    return new ClientInterceptor() {
1✔
608
      @Override
609
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
610
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
611
        next = ClientInterceptors.interceptForward(next, interceptors);
1✔
612
        return next.newCall(method, callOptions);
1✔
613
      }
614
    };
615
  }
616

617
  @Nullable
618
  private static String getHeaderValue(Metadata headers, String headerName) {
619
    if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
620
      return null;
×
621
    }
622
    if (headerName.equals("content-type")) {
1✔
623
      return "application/grpc";
×
624
    }
625
    Metadata.Key<String> key;
626
    try {
627
      key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
1✔
628
    } catch (IllegalArgumentException e) {
×
629
      return null;
×
630
    }
1✔
631
    Iterable<String> values = headers.getAll(key);
1✔
632
    return values == null ? null : Joiner.on(",").join(values);
1✔
633
  }
634

635
  private static String prefixedClusterName(String name) {
636
    return "cluster:" + name;
1✔
637
  }
638

639
  private static String prefixedClusterSpecifierPluginName(String pluginName) {
640
    return "cluster_specifier_plugin:" + pluginName;
1✔
641
  }
642

643
  class ResolveState implements XdsDependencyManager.XdsConfigWatcher {
644
    private final ConfigOrError emptyServiceConfig =
1✔
645
        serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap());
1✔
646
    private final String authority;
647
    private final XdsDependencyManager xdsDependencyManager;
648
    private boolean stopped;
649
    @Nullable
650
    private Set<String> existingClusters;  // clusters to which new requests can be routed
651
    private StatusOr<XdsConfig> lastConfigOrStatus;
652

653
    private ResolveState(String ldsResourceName) {
1✔
654
      authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority;
1✔
655
      xdsDependencyManager =
1✔
656
          new XdsDependencyManager(xdsClient, this, syncContext, authority, ldsResourceName,
1✔
657
              nameResolverArgs, scheduler);
1✔
658
    }
1✔
659

660
    private void shutdown() {
661
      if (stopped) {
1✔
662
        return;
×
663
      }
664

665
      stopped = true;
1✔
666
      xdsDependencyManager.shutdown();
1✔
667
      updateActiveFilters(null);
1✔
668
    }
1✔
669

670
    @Override
671
    public void onUpdate(StatusOr<XdsConfig> updateOrStatus) {
672
      if (stopped) {
1✔
673
        return;
×
674
      }
675
      logger.log(XdsLogLevel.INFO, "Receive XDS resource update: {0}", updateOrStatus);
1✔
676

677
      lastConfigOrStatus = updateOrStatus;
1✔
678
      if (!updateOrStatus.hasValue()) {
1✔
679
        updateActiveFilters(null);
1✔
680
        cleanUpRoutes(updateOrStatus.getStatus());
1✔
681
        return;
1✔
682
      }
683

684
      // Process Route
685
      XdsConfig update = updateOrStatus.getValue();
1✔
686
      HttpConnectionManager httpConnectionManager = update.getListener().httpConnectionManager();
1✔
687
      VirtualHost virtualHost = update.getVirtualHost();
1✔
688
      ImmutableList<NamedFilterConfig> filterConfigs = httpConnectionManager.httpFilterConfigs();
1✔
689
      long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano();
1✔
690

691
      updateActiveFilters(filterConfigs);
1✔
692
      updateRoutes(update, virtualHost, streamDurationNano, filterConfigs);
1✔
693
    }
1✔
694

695
    // called in syncContext
696
    private void updateActiveFilters(@Nullable List<NamedFilterConfig> filterConfigs) {
697
      if (filterConfigs == null) {
1✔
698
        filterConfigs = ImmutableList.of();
1✔
699
      }
700
      Set<String> filtersToShutdown = new HashSet<>(activeFilters.keySet());
1✔
701
      for (NamedFilterConfig namedFilter : filterConfigs) {
1✔
702
        String typeUrl = namedFilter.filterConfig.typeUrl();
1✔
703
        String filterKey = namedFilter.filterStateKey();
1✔
704

705
        Filter.Provider provider = filterRegistry.get(typeUrl);
1✔
706
        checkNotNull(provider, "provider %s", typeUrl);
1✔
707
        Filter filter = activeFilters.computeIfAbsent(filterKey, k -> provider.newInstance());
1✔
708
        checkNotNull(filter, "filter %s", filterKey);
1✔
709
        filtersToShutdown.remove(filterKey);
1✔
710
      }
1✔
711

712
      // Shutdown filters not present in current HCM.
713
      for (String filterKey : filtersToShutdown) {
1✔
714
        Filter filterToShutdown = activeFilters.remove(filterKey);
1✔
715
        checkNotNull(filterToShutdown, "filterToShutdown %s", filterKey);
1✔
716
        filterToShutdown.close();
1✔
717
      }
1✔
718
    }
1✔
719

720
    private void updateRoutes(
721
        XdsConfig xdsConfig,
722
        @Nullable VirtualHost virtualHost,
723
        long httpMaxStreamDurationNano,
724
        @Nullable List<NamedFilterConfig> filterConfigs) {
725
      List<Route> routes = virtualHost.routes();
1✔
726
      ImmutableList.Builder<RouteData> routesData = ImmutableList.builder();
1✔
727

728
      // Populate all clusters to which requests can be routed to through the virtual host.
729
      Set<String> clusters = new HashSet<>();
1✔
730
      // uniqueName -> clusterName
731
      Map<String, String> clusterNameMap = new HashMap<>();
1✔
732
      // uniqueName -> pluginConfig
733
      Map<String, RlsPluginConfig> rlsPluginConfigMap = new HashMap<>();
1✔
734
      for (Route route : routes) {
1✔
735
        RouteAction action = route.routeAction();
1✔
736
        String prefixedName;
737
        if (action == null) {
1✔
738
          routesData.add(new RouteData(route.routeMatch(), null, ImmutableList.of()));
1✔
739
        } else if (action.cluster() != null) {
1✔
740
          prefixedName = prefixedClusterName(action.cluster());
1✔
741
          clusters.add(prefixedName);
1✔
742
          clusterNameMap.put(prefixedName, action.cluster());
1✔
743
          ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null);
1✔
744
          routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters));
1✔
745
        } else if (action.weightedClusters() != null) {
1✔
746
          ImmutableList.Builder<ClientInterceptor> filterList = ImmutableList.builder();
1✔
747
          for (ClusterWeight weightedCluster : action.weightedClusters()) {
1✔
748
            prefixedName = prefixedClusterName(weightedCluster.name());
1✔
749
            clusters.add(prefixedName);
1✔
750
            clusterNameMap.put(prefixedName, weightedCluster.name());
1✔
751
            filterList.add(createFilters(filterConfigs, virtualHost, route, weightedCluster));
1✔
752
          }
1✔
753
          routesData.add(
1✔
754
              new RouteData(route.routeMatch(), route.routeAction(), filterList.build()));
1✔
755
        } else if (action.namedClusterSpecifierPluginConfig() != null) {
1✔
756
          PluginConfig pluginConfig = action.namedClusterSpecifierPluginConfig().config();
1✔
757
          if (pluginConfig instanceof RlsPluginConfig) {
1✔
758
            prefixedName = prefixedClusterSpecifierPluginName(
1✔
759
                action.namedClusterSpecifierPluginConfig().name());
1✔
760
            clusters.add(prefixedName);
1✔
761
            rlsPluginConfigMap.put(prefixedName, (RlsPluginConfig) pluginConfig);
1✔
762
          }
763
          ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null);
1✔
764
          routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters));
1✔
765
        } else {
766
          // Discard route
767
        }
768
      }
1✔
769

770
      // Updates channel's load balancing config whenever the set of selectable clusters changes.
771
      boolean shouldUpdateResult = existingClusters == null;
1✔
772
      Set<String> addedClusters =
773
          existingClusters == null ? clusters : Sets.difference(clusters, existingClusters);
1✔
774
      Set<String> deletedClusters =
775
          existingClusters == null
1✔
776
              ? Collections.emptySet() : Sets.difference(existingClusters, clusters);
1✔
777
      existingClusters = clusters;
1✔
778
      for (String cluster : addedClusters) {
1✔
779
        if (clusterRefs.containsKey(cluster)) {
1✔
780
          clusterRefs.get(cluster).refCount.incrementAndGet();
1✔
781
        } else {
782
          if (clusterNameMap.containsKey(cluster)) {
1✔
783
            clusterRefs.put(
1✔
784
                cluster,
785
                ClusterRefState.forCluster(new AtomicInteger(1), clusterNameMap.get(cluster)));
1✔
786
          }
787
          if (rlsPluginConfigMap.containsKey(cluster)) {
1✔
788
            clusterRefs.put(
1✔
789
                cluster,
790
                ClusterRefState.forRlsPlugin(
1✔
791
                    new AtomicInteger(1), rlsPluginConfigMap.get(cluster)));
1✔
792
          }
793
          shouldUpdateResult = true;
1✔
794
        }
795
      }
1✔
796
      for (String cluster : clusters) {
1✔
797
        RlsPluginConfig rlsPluginConfig = rlsPluginConfigMap.get(cluster);
1✔
798
        if (!Objects.equals(rlsPluginConfig, clusterRefs.get(cluster).rlsPluginConfig)) {
1✔
799
          ClusterRefState newClusterRefState =
1✔
800
              ClusterRefState.forRlsPlugin(clusterRefs.get(cluster).refCount, rlsPluginConfig);
1✔
801
          clusterRefs.put(cluster, newClusterRefState);
1✔
802
          shouldUpdateResult = true;
1✔
803
        }
804
      }
1✔
805
      // Update service config to include newly added clusters.
806
      if (shouldUpdateResult && routingConfig != null) {
1✔
807
        updateResolutionResult(xdsConfig);
1✔
808
        shouldUpdateResult = false;
1✔
809
      }
810
      // Make newly added clusters selectable by config selector and deleted clusters no longer
811
      // selectable.
812
      routingConfig = new RoutingConfig(xdsConfig, httpMaxStreamDurationNano, routesData.build());
1✔
813
      for (String cluster : deletedClusters) {
1✔
814
        int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
815
        if (count == 0) {
1✔
816
          clusterRefs.remove(cluster);
1✔
817
          shouldUpdateResult = true;
1✔
818
        }
819
      }
1✔
820
      if (shouldUpdateResult) {
1✔
821
        updateResolutionResult(xdsConfig);
1✔
822
      }
823
    }
1✔
824

825
    private ClientInterceptor createFilters(
826
        @Nullable List<NamedFilterConfig> filterConfigs,
827
        VirtualHost virtualHost,
828
        Route route,
829
        @Nullable ClusterWeight weightedCluster) {
830
      if (filterConfigs == null) {
1✔
831
        return new PassthroughClientInterceptor();
1✔
832
      }
833

834
      Map<String, FilterConfig> selectedOverrideConfigs =
1✔
835
          new HashMap<>(virtualHost.filterConfigOverrides());
1✔
836
      selectedOverrideConfigs.putAll(route.filterConfigOverrides());
1✔
837
      if (weightedCluster != null) {
1✔
838
        selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides());
1✔
839
      }
840

841
      ImmutableList.Builder<ClientInterceptor> filterInterceptors = ImmutableList.builder();
1✔
842
      for (NamedFilterConfig namedFilter : filterConfigs) {
1✔
843
        String name = namedFilter.name;
1✔
844
        FilterConfig config = namedFilter.filterConfig;
1✔
845
        FilterConfig overrideConfig = selectedOverrideConfigs.get(name);
1✔
846
        String filterKey = namedFilter.filterStateKey();
1✔
847

848
        Filter filter = activeFilters.get(filterKey);
1✔
849
        checkNotNull(filter, "activeFilters.get(%s)", filterKey);
1✔
850
        ClientInterceptor interceptor =
1✔
851
            filter.buildClientInterceptor(config, overrideConfig, scheduler);
1✔
852

853
        if (interceptor != null) {
1✔
854
          filterInterceptors.add(interceptor);
1✔
855
        }
856
      }
1✔
857

858
      // Combine interceptors produced by different filters into a single one that executes
859
      // them sequentially. The order is preserved.
860
      return combineInterceptors(filterInterceptors.build());
1✔
861
    }
862

863
    private void cleanUpRoutes(Status error) {
864
      routingConfig = new RoutingConfig(error);
1✔
865
      if (existingClusters != null) {
1✔
866
        for (String cluster : existingClusters) {
1✔
867
          int count = clusterRefs.get(cluster).refCount.decrementAndGet();
1✔
868
          if (count == 0) {
1✔
869
            clusterRefs.remove(cluster);
1✔
870
          }
871
        }
1✔
872
        existingClusters = null;
1✔
873
      }
874

875
      // Without addresses the default LB (normally pick_first) should become TRANSIENT_FAILURE, and
876
      // the config selector handles the error message itself.
877
      listener.onResult2(ResolutionResult.newBuilder()
1✔
878
          .setAttributes(Attributes.newBuilder()
1✔
879
            .set(InternalConfigSelector.KEY, configSelector)
1✔
880
            .build())
1✔
881
          .setServiceConfig(emptyServiceConfig)
1✔
882
          .build());
1✔
883
    }
1✔
884
  }
885

886
  /**
887
   * VirtualHost-level configuration for request routing.
888
   */
889
  private static class RoutingConfig {
890
    final XdsConfig xdsConfig;
891
    final long fallbackTimeoutNano;
892
    final ImmutableList<RouteData> routes;
893
    final Status errorStatus;
894

895
    private RoutingConfig(
896
        XdsConfig xdsConfig, long fallbackTimeoutNano, ImmutableList<RouteData> routes) {
1✔
897
      this.xdsConfig = checkNotNull(xdsConfig, "xdsConfig");
1✔
898
      this.fallbackTimeoutNano = fallbackTimeoutNano;
1✔
899
      this.routes = checkNotNull(routes, "routes");
1✔
900
      this.errorStatus = null;
1✔
901
    }
1✔
902

903
    private RoutingConfig(Status errorStatus) {
1✔
904
      this.xdsConfig = null;
1✔
905
      this.fallbackTimeoutNano = 0;
1✔
906
      this.routes = null;
1✔
907
      this.errorStatus = checkNotNull(errorStatus, "errorStatus");
1✔
908
      checkArgument(!errorStatus.isOk(), "errorStatus should not be okay");
1✔
909
    }
1✔
910
  }
911

912
  static final class RouteData {
913
    final RouteMatch routeMatch;
914
    /** null implies non-forwarding action. */
915
    @Nullable
916
    final RouteAction routeAction;
917
    /**
918
     * Only one of these interceptors should be used per-RPC. There are only multiple values in the
919
     * list for weighted clusters, in which case the order of the list mirrors the weighted
920
     * clusters.
921
     */
922
    final ImmutableList<ClientInterceptor> filterChoices;
923

924
    RouteData(RouteMatch routeMatch, @Nullable RouteAction routeAction, ClientInterceptor filter) {
925
      this(routeMatch, routeAction, ImmutableList.of(filter));
1✔
926
    }
1✔
927

928
    RouteData(
929
        RouteMatch routeMatch,
930
        @Nullable RouteAction routeAction,
931
        ImmutableList<ClientInterceptor> filterChoices) {
1✔
932
      this.routeMatch = checkNotNull(routeMatch, "routeMatch");
1✔
933
      checkArgument(
1✔
934
          routeAction == null || !filterChoices.isEmpty(),
1✔
935
          "filter may be empty only for non-forwarding action");
936
      this.routeAction = routeAction;
1✔
937
      if (routeAction != null && routeAction.weightedClusters() != null) {
1✔
938
        checkArgument(
1✔
939
            routeAction.weightedClusters().size() == filterChoices.size(),
1✔
940
            "filter choices must match size of weighted clusters");
941
      }
942
      for (ClientInterceptor filter : filterChoices) {
1✔
943
        checkNotNull(filter, "entry in filterChoices is null");
1✔
944
      }
1✔
945
      this.filterChoices = checkNotNull(filterChoices, "filterChoices");
1✔
946
    }
1✔
947
  }
948

949
  private static class ClusterRefState {
950
    final AtomicInteger refCount;
951
    @Nullable
952
    final String traditionalCluster;
953
    @Nullable
954
    final RlsPluginConfig rlsPluginConfig;
955

956
    private ClusterRefState(
957
        AtomicInteger refCount, @Nullable String traditionalCluster,
958
        @Nullable RlsPluginConfig rlsPluginConfig) {
1✔
959
      this.refCount = refCount;
1✔
960
      checkArgument(traditionalCluster == null ^ rlsPluginConfig == null,
1✔
961
          "There must be exactly one non-null value in traditionalCluster and pluginConfig");
962
      this.traditionalCluster = traditionalCluster;
1✔
963
      this.rlsPluginConfig = rlsPluginConfig;
1✔
964
    }
1✔
965

966
    private Map<String, ?> toLbPolicy() {
967
      if (traditionalCluster != null) {
1✔
968
        return ImmutableMap.of(
1✔
969
            XdsLbPolicies.CDS_POLICY_NAME,
970
            ImmutableMap.of("cluster", traditionalCluster));
1✔
971
      } else {
972
        ImmutableMap<String, ?> rlsConfig = new ImmutableMap.Builder<String, Object>()
1✔
973
            .put("routeLookupConfig", rlsPluginConfig.config())
1✔
974
            .put(
1✔
975
                "childPolicy",
976
                ImmutableList.of(ImmutableMap.of(XdsLbPolicies.CDS_POLICY_NAME, ImmutableMap.of())))
1✔
977
            .put("childPolicyConfigTargetFieldName", "cluster")
1✔
978
            .buildOrThrow();
1✔
979
        return ImmutableMap.of("rls_experimental", rlsConfig);
1✔
980
      }
981
    }
982

983
    static ClusterRefState forCluster(AtomicInteger refCount, String name) {
984
      return new ClusterRefState(refCount, name, null);
1✔
985
    }
986

987
    static ClusterRefState forRlsPlugin(AtomicInteger refCount, RlsPluginConfig rlsPluginConfig) {
988
      return new ClusterRefState(refCount, null, rlsPluginConfig);
1✔
989
    }
990
  }
991
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc