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

grpc / grpc-java / #20346

03 Jul 2026 10:23AM UTC coverage: 89.106% (-0.01%) from 89.118%
#20346

push

github

web-flow
xds: Move backend service label plumbing to CDS (#12882)

Addresses #12431.

This moves backend service metric label plumbing from `xds_cluster_impl`
to `cds`, matching the A75/A89 aggregate cluster behavior.

Previously, `xds_cluster_impl` added the `grpc.lb.backend_service`
pick-details label and propagated `NameResolver.ATTR_BACKEND_SERVICE` to
its child policy. That works for simple clusters, but it is the wrong
layer after A75 because aggregate clusters need the leaf CDS cluster
name, not the aggregate root cluster name.

This change makes CDS responsible for backend service plumbing:

- non-aggregate CDS adds the `grpc.lb.backend_service` pick-details
label using the CDS cluster name
- non-aggregate CDS propagates `NameResolver.ATTR_BACKEND_SERVICE` to
its child policy, so WRR can consume the backend service context
- aggregate root CDS does not add the backend service attribute or
pick-details label
- leaf CDS instances under an aggregate cluster add their own leaf
cluster name
- `xds_cluster_impl` no longer owns backend service pick-details label
or child attribute propagation

The existing `InternalEquivalentAddressGroup.ATTR_BACKEND_SERVICE` path
is left unchanged for endpoint/subchannel metrics.

Tests cover:

- non-aggregate CDS propagating `NameResolver.ATTR_BACKEND_SERVICE`
- non-aggregate CDS adding `grpc.lb.backend_service`
- aggregate root CDS not adding the root cluster name
- aggregate leaf CDS instances propagating leaf cluster names
- `xds_cluster_impl` no longer adding backend service labels or
attributes
- picker tests using a non-null `PickDetailsConsumer`, matching the real
`PickSubchannelArgs` contract

37992 of 42637 relevant lines covered (89.11%)

0.89 hits per line

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

93.87
/../xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java
1
/*
2
 * Copyright 2020 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.checkNotNull;
20
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
21
import static io.grpc.xds.XdsLbPolicies.CDS_POLICY_NAME;
22
import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME;
23

24
import com.google.common.collect.ImmutableMap;
25
import com.google.common.primitives.UnsignedInts;
26
import com.google.errorprone.annotations.CheckReturnValue;
27
import io.grpc.Attributes;
28
import io.grpc.ConnectivityState;
29
import io.grpc.EquivalentAddressGroup;
30
import io.grpc.HttpConnectProxiedSocketAddress;
31
import io.grpc.InternalEquivalentAddressGroup;
32
import io.grpc.InternalLogId;
33
import io.grpc.LoadBalancer;
34
import io.grpc.LoadBalancerProvider;
35
import io.grpc.LoadBalancerRegistry;
36
import io.grpc.NameResolver;
37
import io.grpc.Status;
38
import io.grpc.StatusOr;
39
import io.grpc.internal.GrpcUtil;
40
import io.grpc.util.ForwardingLoadBalancerHelper;
41
import io.grpc.util.GracefulSwitchLoadBalancer;
42
import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig;
43
import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig;
44
import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig;
45
import io.grpc.xds.Endpoints.DropOverload;
46
import io.grpc.xds.Endpoints.LbEndpoint;
47
import io.grpc.xds.Endpoints.LocalityLbEndpoints;
48
import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection;
49
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
50
import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection;
51
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
52
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig;
53
import io.grpc.xds.XdsClusterResource.CdsUpdate;
54
import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType;
55
import io.grpc.xds.XdsConfig.Subscription;
56
import io.grpc.xds.XdsConfig.XdsClusterConfig;
57
import io.grpc.xds.XdsConfig.XdsClusterConfig.AggregateConfig;
58
import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig;
59
import io.grpc.xds.XdsEndpointResource.EdsUpdate;
60
import io.grpc.xds.client.Locality;
61
import io.grpc.xds.client.XdsLogger;
62
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
63
import io.grpc.xds.internal.XdsInternalAttributes;
64
import java.net.InetSocketAddress;
65
import java.net.SocketAddress;
66
import java.util.ArrayList;
67
import java.util.Arrays;
68
import java.util.Collections;
69
import java.util.HashMap;
70
import java.util.HashSet;
71
import java.util.List;
72
import java.util.Map;
73
import java.util.Set;
74
import java.util.TreeMap;
75

76
/**
77
 * Load balancer for cds_experimental LB policy. One instance per top-level cluster.
78
 * The top-level cluster may be a plain EDS/logical-DNS cluster or an aggregate cluster formed
79
 * by a group of sub-clusters in a tree hierarchy.
80
 */
81
final class CdsLoadBalancer2 extends LoadBalancer {
82
  static boolean pickFirstWeightedShuffling =
1✔
83
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_PF_WEIGHTED_SHUFFLING", true);
1✔
84

85
  private final XdsLogger logger;
86
  private final Helper helper;
87
  private final LoadBalancerRegistry lbRegistry;
88
  private final ClusterState clusterState = new ClusterState();
1✔
89
  private final CdsLbHelper cdsLbHelper = new CdsLbHelper();
1✔
90
  private GracefulSwitchLoadBalancer delegate;
91
  private boolean addBackendServicePickDetailsLabel;
92
  // Following fields are effectively final.
93
  private String clusterName;
94
  private Subscription clusterSubscription;
95

96
  CdsLoadBalancer2(Helper helper, LoadBalancerRegistry lbRegistry) {
1✔
97
    this.helper = checkNotNull(helper, "helper");
1✔
98
    this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry");
1✔
99
    this.delegate = new GracefulSwitchLoadBalancer(cdsLbHelper);
1✔
100
    logger = XdsLogger.withLogId(InternalLogId.allocate("cds-lb", helper.getAuthority()));
1✔
101
    logger.log(XdsLogLevel.INFO, "Created");
1✔
102
  }
1✔
103

104
  @Override
105
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
106
    logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
1✔
107
    if (this.clusterName == null) {
1✔
108
      CdsConfig config = (CdsConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
109
      logger.log(XdsLogLevel.INFO, "Config: {0}", config);
1✔
110
      if (config.isDynamic) {
1✔
111
        clusterSubscription = resolvedAddresses.getAttributes()
1✔
112
            .get(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY)
1✔
113
            .subscribeToCluster(config.name);
1✔
114
      }
115
      this.clusterName = config.name;
1✔
116
    }
117
    XdsConfig xdsConfig = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CONFIG);
1✔
118
    StatusOr<XdsClusterConfig> clusterConfigOr = xdsConfig.getClusters().get(clusterName);
1✔
119
    if (clusterConfigOr == null) {
1✔
120
      if (clusterSubscription == null) {
1✔
121
        // Should be impossible, because XdsDependencyManager wouldn't have generated this
122
        return fail(Status.INTERNAL.withDescription(
×
123
            errorPrefix() + "Unable to find non-dynamic cluster"));
×
124
      }
125
      // The dynamic cluster must not have loaded yet
126
      return Status.OK;
1✔
127
    }
128
    if (!clusterConfigOr.hasValue()) {
1✔
129
      return fail(clusterConfigOr.getStatus());
1✔
130
    }
131
    XdsClusterConfig clusterConfig = clusterConfigOr.getValue();
1✔
132

133
    if (clusterConfig.getChildren() instanceof EndpointConfig) {
1✔
134
      addBackendServicePickDetailsLabel = true;
1✔
135
      StatusOr<EdsUpdate> edsUpdate = getEdsUpdate(xdsConfig, clusterName);
1✔
136
      StatusOr<ClusterResolutionResult> statusOrResult = clusterState.edsUpdateToResult(
1✔
137
          clusterName,
138
          clusterConfig.getClusterResource(),
1✔
139
          clusterConfig.getClusterResource().lbPolicyConfig(),
1✔
140
          edsUpdate);
141
      if (!statusOrResult.hasValue()) {
1✔
142
        Status status = Status.UNAVAILABLE
1✔
143
            .withDescription(statusOrResult.getStatus().getDescription())
1✔
144
            .withCause(statusOrResult.getStatus().getCause());
1✔
145
        delegate.handleNameResolutionError(status);
1✔
146
        return status;
1✔
147
      }
148
      ClusterResolutionResult result = statusOrResult.getValue();
1✔
149
      List<EquivalentAddressGroup> addresses = result.addresses;
1✔
150
      if (addresses.isEmpty()) {
1✔
151
        Status status = Status.UNAVAILABLE
1✔
152
            .withDescription("No usable endpoint from cluster: " + clusterName);
1✔
153
        delegate.handleNameResolutionError(status);
1✔
154
        return status;
1✔
155
      }
156
      Object gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(
1✔
157
          lbRegistry.getProvider(PRIORITY_POLICY_NAME),
1✔
158
          new PriorityLbConfig(
159
              Collections.unmodifiableMap(result.priorityChildConfigs),
1✔
160
              Collections.unmodifiableList(result.priorities)));
1✔
161
      return delegate.acceptResolvedAddresses(
1✔
162
          resolvedAddresses.toBuilder()
1✔
163
            .setLoadBalancingPolicyConfig(gracefulConfig)
1✔
164
            .setAddresses(Collections.unmodifiableList(addresses))
1✔
165
            .setAttributes(resolvedAddresses.getAttributes().toBuilder()
1✔
166
                .set(NameResolver.ATTR_BACKEND_SERVICE, clusterName)
1✔
167
                .build())
1✔
168
            .build());
1✔
169
    } else if (clusterConfig.getChildren() instanceof AggregateConfig) {
1✔
170
      addBackendServicePickDetailsLabel = false;
1✔
171
      Map<String, PriorityChildConfig> priorityChildConfigs = new HashMap<>();
1✔
172
      List<String> leafClusters = ((AggregateConfig) clusterConfig.getChildren()).getLeafNames();
1✔
173
      for (String childCluster: leafClusters) {
1✔
174
        priorityChildConfigs.put(childCluster,
1✔
175
                new PriorityChildConfig(
176
                        GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(
1✔
177
                                lbRegistry.getProvider(CDS_POLICY_NAME),
1✔
178
                                new CdsConfig(childCluster)),
179
                        false));
180
      }
1✔
181
      Object gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(
1✔
182
          lbRegistry.getProvider(PRIORITY_POLICY_NAME),
1✔
183
          new PriorityLoadBalancerProvider.PriorityLbConfig(
184
              Collections.unmodifiableMap(priorityChildConfigs), leafClusters));
1✔
185
      return delegate.acceptResolvedAddresses(
1✔
186
          resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(gracefulConfig).build());
1✔
187
    } else {
188
      return fail(Status.INTERNAL.withDescription(
×
189
              errorPrefix() + "Unexpected cluster children type: "
×
190
                      + clusterConfig.getChildren().getClass()));
×
191
    }
192
  }
193

194
  @Override
195
  public void handleNameResolutionError(Status error) {
196
    logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error);
1✔
197
    if (delegate != null) {
1✔
198
      delegate.handleNameResolutionError(error);
1✔
199
    } else {
200
      helper.updateBalancingState(
×
201
          TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
×
202
    }
203
  }
1✔
204

205
  @Override
206
  public void shutdown() {
207
    logger.log(XdsLogLevel.INFO, "Shutdown");
1✔
208
    delegate.shutdown();
1✔
209
    delegate = new GracefulSwitchLoadBalancer(cdsLbHelper);
1✔
210
    addBackendServicePickDetailsLabel = false;
1✔
211
    if (clusterSubscription != null) {
1✔
212
      clusterSubscription.close();
1✔
213
      clusterSubscription = null;
1✔
214
    }
215
  }
1✔
216

217
  @CheckReturnValue // don't forget to return up the stack after the fail call
218
  private Status fail(Status error) {
219
    delegate.shutdown();
1✔
220
    addBackendServicePickDetailsLabel = false;
1✔
221
    helper.updateBalancingState(
1✔
222
        TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
1✔
223
    return Status.OK; // XdsNameResolver isn't a polling NR, so this value doesn't matter
1✔
224
  }
225

226
  private String errorPrefix() {
227
    return "CdsLb for " + clusterName + ": ";
×
228
  }
229

230
  private final class CdsLbHelper extends ForwardingLoadBalancerHelper {
1✔
231
    @Override
232
    public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
233
      if (addBackendServicePickDetailsLabel) {
1✔
234
        newPicker = new BackendServiceMetricLabelSubchannelPicker(newPicker, clusterName);
1✔
235
      }
236
      delegate().updateBalancingState(newState, newPicker);
1✔
237
    }
1✔
238

239
    @Override
240
    protected Helper delegate() {
241
      return helper;
1✔
242
    }
243
  }
244

245
  private static final class BackendServiceMetricLabelSubchannelPicker extends SubchannelPicker {
246
    private final SubchannelPicker delegate;
247
    private final String backendService;
248

249
    private BackendServiceMetricLabelSubchannelPicker(
250
        SubchannelPicker delegate, String backendService) {
1✔
251
      this.delegate = checkNotNull(delegate, "delegate");
1✔
252
      this.backendService = checkNotNull(backendService, "backendService");
1✔
253
    }
1✔
254

255
    @Override
256
    public PickResult pickSubchannel(PickSubchannelArgs args) {
257
      args.getPickDetailsConsumer().addOptionalLabel("grpc.lb.backend_service", backendService);
1✔
258
      return delegate.pickSubchannel(args);
1✔
259
    }
260
  }
261

262
  /**
263
   * The number of bits assigned to the fractional part of fixed-point values. We normalize weights
264
   * to a fixed-point number between 0 and 1, representing that item's proportion of traffic (1 ==
265
   * 100% of traffic). We reserve at least one bit for the whole number so that we don't need to
266
   * special case a single item, and so that we can round up very low values without risking uint32
267
   * overflow of the sum of weights.
268
   */
269
  private static final int FIXED_POINT_FRACTIONAL_BITS = 31;
270

271
  /** Divide two uint32s and produce a fixed-point uint32 result. */
272
  private static long fractionToFixedPoint(long numerator, long denominator) {
273
    long one = 1L << FIXED_POINT_FRACTIONAL_BITS;
1✔
274
    return numerator * one / denominator;
1✔
275
  }
276

277
  /** Multiply two uint32 fixed-point numbers, returning a uint32 fixed-point. */
278
  private static long fixedPointMultiply(long a, long b) {
279
    return (a * b) >> FIXED_POINT_FRACTIONAL_BITS;
1✔
280
  }
281

282
  private static StatusOr<EdsUpdate> getEdsUpdate(XdsConfig xdsConfig, String cluster) {
283
    StatusOr<XdsClusterConfig> clusterConfig = xdsConfig.getClusters().get(cluster);
1✔
284
    if (clusterConfig == null) {
1✔
285
      return StatusOr.fromStatus(Status.INTERNAL
×
286
          .withDescription("BUG: cluster resolver could not find cluster in xdsConfig"));
×
287
    }
288
    if (!clusterConfig.hasValue()) {
1✔
289
      return StatusOr.fromStatus(clusterConfig.getStatus());
×
290
    }
291
    if (!(clusterConfig.getValue().getChildren() instanceof XdsClusterConfig.EndpointConfig)) {
1✔
292
      return StatusOr.fromStatus(Status.INTERNAL
×
293
          .withDescription("BUG: cluster resolver cluster with children of unknown type"));
×
294
    }
295
    XdsClusterConfig.EndpointConfig endpointConfig =
1✔
296
        (XdsClusterConfig.EndpointConfig) clusterConfig.getValue().getChildren();
1✔
297
    return endpointConfig.getEndpoint();
1✔
298
  }
299

300
  /**
301
   * Generates a string that represents the priority in the LB policy config. The string is unique
302
   * across priorities in all clusters and priorityName(c, p1) < priorityName(c, p2) iff p1 < p2.
303
   * The ordering is undefined for priorities in different clusters.
304
   */
305
  private static String priorityName(String cluster, int priority) {
306
    return cluster + "[child" + priority + "]";
1✔
307
  }
308

309
  /**
310
   * Generates a string that represents the locality in the LB policy config. The string is unique
311
   * across all localities in all clusters.
312
   */
313
  private static String localityName(Locality locality) {
314
    return "{region=\"" + locality.region()
1✔
315
        + "\", zone=\"" + locality.zone()
1✔
316
        + "\", sub_zone=\"" + locality.subZone()
1✔
317
        + "\"}";
318
  }
319

320
  private final class ClusterState {
1✔
321
    private Map<Locality, String> localityPriorityNames = Collections.emptyMap();
1✔
322
    int priorityNameGenId = 1;
1✔
323

324
    StatusOr<ClusterResolutionResult> edsUpdateToResult(
325
        String clusterName,
326
        CdsUpdate discovery,
327
        Object lbConfig,
328
        StatusOr<EdsUpdate> updateOr) {
329
      if (!updateOr.hasValue()) {
1✔
330
        return StatusOr.fromStatus(updateOr.getStatus());
1✔
331
      }
332
      EdsUpdate update = updateOr.getValue();
1✔
333
      logger.log(XdsLogLevel.DEBUG, "Received endpoint update {0}", update);
1✔
334
      if (logger.isLoggable(XdsLogLevel.INFO)) {
1✔
335
        logger.log(XdsLogLevel.INFO, "Cluster {0}: {1} localities, {2} drop categories",
×
336
            clusterName, update.localityLbEndpointsMap.size(),
×
337
            update.dropPolicies.size());
×
338
      }
339
      Map<Locality, LocalityLbEndpoints> localityLbEndpoints =
1✔
340
          update.localityLbEndpointsMap;
341
      List<DropOverload> dropOverloads = update.dropPolicies;
1✔
342
      List<EquivalentAddressGroup> addresses = new ArrayList<>();
1✔
343
      Map<String, Map<Locality, Integer>> prioritizedLocalityWeights = new HashMap<>();
1✔
344
      List<String> sortedPriorityNames =
1✔
345
          generatePriorityNames(clusterName, localityLbEndpoints);
1✔
346
      Map<String, Long> priorityLocalityWeightSums;
347
      if (pickFirstWeightedShuffling) {
1✔
348
        priorityLocalityWeightSums = new HashMap<>(sortedPriorityNames.size() * 2);
1✔
349
        for (Locality locality : localityLbEndpoints.keySet()) {
1✔
350
          LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality);
1✔
351
          String priorityName = localityPriorityNames.get(locality);
1✔
352
          Long sum = priorityLocalityWeightSums.get(priorityName);
1✔
353
          if (sum == null) {
1✔
354
            sum = 0L;
1✔
355
          }
356
          long weight = UnsignedInts.toLong(localityLbInfo.localityWeight());
1✔
357
          priorityLocalityWeightSums.put(priorityName, sum + weight);
1✔
358
        }
1✔
359
      } else {
360
        priorityLocalityWeightSums = null;
1✔
361
      }
362

363
      for (Locality locality : localityLbEndpoints.keySet()) {
1✔
364
        LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality);
1✔
365
        String priorityName = localityPriorityNames.get(locality);
1✔
366
        String localityName = localityName(locality);
1✔
367
        AddressFilter.PathChain pathChain =
1✔
368
            AddressFilter.createPathChain(Arrays.asList(priorityName, localityName));
1✔
369

370
        boolean discard = true;
1✔
371
        // These sums _should_ fit in uint32, but XdsEndpointResource isn't actually verifying that
372
        // is true today. Since we are using long to avoid signedness trouble, the math happens to
373
        // still work if it turns out the sums exceed uint32.
374
        long localityWeightSum = 0;
1✔
375
        long endpointWeightSum = 0;
1✔
376
        if (pickFirstWeightedShuffling) {
1✔
377
          localityWeightSum = priorityLocalityWeightSums.get(priorityName);
1✔
378
          for (LbEndpoint endpoint : localityLbInfo.endpoints()) {
1✔
379
            if (endpoint.isHealthy()) {
1✔
380
              endpointWeightSum += UnsignedInts.toLong(endpoint.loadBalancingWeight());
1✔
381
            }
382
          }
1✔
383
        }
384
        for (LbEndpoint endpoint : localityLbInfo.endpoints()) {
1✔
385
          if (endpoint.isHealthy()) {
1✔
386
            discard = false;
1✔
387
            long weight;
388
            if (pickFirstWeightedShuffling) {
1✔
389
              // Combine locality and endpoint weights as defined by gRFC A113
390
              long localityWeight = fractionToFixedPoint(
1✔
391
                  UnsignedInts.toLong(localityLbInfo.localityWeight()), localityWeightSum);
1✔
392
              long endpointWeight = fractionToFixedPoint(
1✔
393
                  UnsignedInts.toLong(endpoint.loadBalancingWeight()), endpointWeightSum);
1✔
394
              weight = fixedPointMultiply(localityWeight, endpointWeight);
1✔
395
              if (weight == 0) {
1✔
396
                weight = 1;
×
397
              }
398
            } else {
1✔
399
              weight = localityLbInfo.localityWeight();
1✔
400
              if (endpoint.loadBalancingWeight() != 0) {
1✔
401
                weight *= endpoint.loadBalancingWeight();
1✔
402
              }
403
            }
404

405
            Attributes attr =
1✔
406
                endpoint.eag().getAttributes().toBuilder()
1✔
407
                    .set(InternalEquivalentAddressGroup.ATTR_BACKEND_SERVICE, clusterName)
1✔
408
                    .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY, locality)
1✔
409
                    .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName)
1✔
410
                    .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT,
1✔
411
                        localityLbInfo.localityWeight())
1✔
412
                    .set(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT, weight)
1✔
413
                    .set(XdsInternalAttributes.ATTR_ADDRESS_NAME, endpoint.hostname())
1✔
414
                    .set(AddressFilter.PATH_CHAIN_KEY, pathChain)
1✔
415
                    .build();
1✔
416
            EquivalentAddressGroup eag;
417
            if (discovery.isHttp11ProxyAvailable()) {
1✔
418
              List<SocketAddress> rewrittenAddresses = new ArrayList<>();
1✔
419
              for (SocketAddress addr : endpoint.eag().getAddresses()) {
1✔
420
                rewrittenAddresses.add(rewriteAddress(
1✔
421
                    addr, endpoint.endpointMetadata(), localityLbInfo.localityMetadata()));
1✔
422
              }
1✔
423
              eag = new EquivalentAddressGroup(rewrittenAddresses, attr);
1✔
424
            } else {
1✔
425
              eag = new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr);
1✔
426
            }
427
            addresses.add(eag);
1✔
428
          }
429
        }
1✔
430
        if (discard) {
1✔
431
          logger.log(XdsLogLevel.INFO,
1✔
432
              "Discard locality {0} with 0 healthy endpoints", locality);
433
          continue;
1✔
434
        }
435
        if (!prioritizedLocalityWeights.containsKey(priorityName)) {
1✔
436
          prioritizedLocalityWeights.put(priorityName, new HashMap<Locality, Integer>());
1✔
437
        }
438
        prioritizedLocalityWeights.get(priorityName).put(
1✔
439
            locality, localityLbInfo.localityWeight());
1✔
440
      }
1✔
441
      if (prioritizedLocalityWeights.isEmpty()) {
1✔
442
        // Will still update the result, as if the cluster resource is revoked.
443
        logger.log(XdsLogLevel.INFO,
1✔
444
            "Cluster {0} has no usable priority/locality/endpoint", clusterName);
445
      }
446
      sortedPriorityNames.retainAll(prioritizedLocalityWeights.keySet());
1✔
447
      Map<String, PriorityChildConfig> priorityChildConfigs =
1✔
448
          generatePriorityChildConfigs(
1✔
449
              clusterName, discovery, lbConfig, lbRegistry,
1✔
450
              prioritizedLocalityWeights, dropOverloads);
451
      return StatusOr.fromValue(new ClusterResolutionResult(addresses, priorityChildConfigs,
1✔
452
          sortedPriorityNames));
453
    }
454

455
    private SocketAddress rewriteAddress(SocketAddress addr,
456
        ImmutableMap<String, Object> endpointMetadata,
457
        ImmutableMap<String, Object> localityMetadata) {
458
      if (!(addr instanceof InetSocketAddress)) {
1✔
459
        return addr;
×
460
      }
461

462
      SocketAddress proxyAddress;
463
      try {
464
        proxyAddress = (SocketAddress) endpointMetadata.get(
1✔
465
            "envoy.http11_proxy_transport_socket.proxy_address");
466
        if (proxyAddress == null) {
1✔
467
          proxyAddress = (SocketAddress) localityMetadata.get(
1✔
468
              "envoy.http11_proxy_transport_socket.proxy_address");
469
        }
470
      } catch (ClassCastException e) {
×
471
        return addr;
×
472
      }
1✔
473

474
      if (proxyAddress == null) {
1✔
475
        return addr;
1✔
476
      }
477

478
      return HttpConnectProxiedSocketAddress.newBuilder()
1✔
479
          .setTargetAddress((InetSocketAddress) addr)
1✔
480
          .setProxyAddress(proxyAddress)
1✔
481
          .build();
1✔
482
    }
483

484
    private List<String> generatePriorityNames(String name,
485
        Map<Locality, LocalityLbEndpoints> localityLbEndpoints) {
486
      TreeMap<Integer, List<Locality>> todo = new TreeMap<>();
1✔
487
      for (Locality locality : localityLbEndpoints.keySet()) {
1✔
488
        int priority = localityLbEndpoints.get(locality).priority();
1✔
489
        if (!todo.containsKey(priority)) {
1✔
490
          todo.put(priority, new ArrayList<>());
1✔
491
        }
492
        todo.get(priority).add(locality);
1✔
493
      }
1✔
494
      Map<Locality, String> newNames = new HashMap<>();
1✔
495
      Set<String> usedNames = new HashSet<>();
1✔
496
      List<String> ret = new ArrayList<>();
1✔
497
      for (Integer priority: todo.keySet()) {
1✔
498
        String foundName = "";
1✔
499
        for (Locality locality : todo.get(priority)) {
1✔
500
          if (localityPriorityNames.containsKey(locality)
1✔
501
              && usedNames.add(localityPriorityNames.get(locality))) {
1✔
502
            foundName = localityPriorityNames.get(locality);
1✔
503
            break;
1✔
504
          }
505
        }
1✔
506
        if ("".equals(foundName)) {
1✔
507
          foundName = priorityName(name, priorityNameGenId++);
1✔
508
        }
509
        for (Locality locality : todo.get(priority)) {
1✔
510
          newNames.put(locality, foundName);
1✔
511
        }
1✔
512
        ret.add(foundName);
1✔
513
      }
1✔
514
      localityPriorityNames = newNames;
1✔
515
      return ret;
1✔
516
    }
517
  }
518

519
  private static class ClusterResolutionResult {
520
    // Endpoint addresses.
521
    private final List<EquivalentAddressGroup> addresses;
522
    // Config (include load balancing policy/config) for each priority in the cluster.
523
    private final Map<String, PriorityChildConfig> priorityChildConfigs;
524
    // List of priority names ordered in descending priorities.
525
    private final List<String> priorities;
526

527
    ClusterResolutionResult(List<EquivalentAddressGroup> addresses,
528
        Map<String, PriorityChildConfig> configs, List<String> priorities) {
1✔
529
      this.addresses = addresses;
1✔
530
      this.priorityChildConfigs = configs;
1✔
531
      this.priorities = priorities;
1✔
532
    }
1✔
533
  }
534

535
  /**
536
   * Generates configs to be used in the priority LB policy for priorities in a cluster.
537
   *
538
   * <p>priority LB -> cluster_impl LB (one per priority) -> (weighted_target LB
539
   * -> round_robin / least_request_experimental (one per locality)) / ring_hash_experimental
540
   */
541
  private static Map<String, PriorityChildConfig> generatePriorityChildConfigs(
542
      String clusterName,
543
      CdsUpdate discovery,
544
      Object endpointLbConfig,
545
      LoadBalancerRegistry lbRegistry,
546
      Map<String, Map<Locality, Integer>> prioritizedLocalityWeights,
547
      List<DropOverload> dropOverloads) {
548
    Map<String, PriorityChildConfig> configs = new HashMap<>();
1✔
549
    for (String priority : prioritizedLocalityWeights.keySet()) {
1✔
550
      ClusterImplConfig clusterImplConfig =
1✔
551
          new ClusterImplConfig(
552
              clusterName, discovery.edsServiceName(), discovery.lrsServerInfo(),
1✔
553
              discovery.maxConcurrentRequests(), dropOverloads, endpointLbConfig,
1✔
554
              discovery.upstreamTlsContext(), discovery.filterMetadata(),
1✔
555
              discovery.backendMetricPropagation());
1✔
556
      LoadBalancerProvider clusterImplLbProvider =
1✔
557
          lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME);
1✔
558
      Object priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(
1✔
559
          clusterImplLbProvider, clusterImplConfig);
560

561
      // If outlier detection has been configured we wrap the child policy in the outlier detection
562
      // load balancer.
563
      if (discovery.outlierDetection() != null) {
1✔
564
        LoadBalancerProvider outlierDetectionProvider = lbRegistry.getProvider(
1✔
565
            "outlier_detection_experimental");
566
        priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(
1✔
567
            outlierDetectionProvider,
568
            buildOutlierDetectionLbConfig(discovery.outlierDetection(), priorityChildPolicy));
1✔
569
      }
570

571
      boolean isEds = discovery.clusterType() == ClusterType.EDS;
1✔
572
      PriorityChildConfig priorityChildConfig =
1✔
573
          new PriorityChildConfig(priorityChildPolicy, isEds /* ignoreReresolution */);
574
      configs.put(priority, priorityChildConfig);
1✔
575
    }
1✔
576
    return configs;
1✔
577
  }
578

579
  /**
580
   * Converts {@link OutlierDetection} that represents the xDS configuration to {@link
581
   * OutlierDetectionLoadBalancerConfig} that the {@link io.grpc.util.OutlierDetectionLoadBalancer}
582
   * understands.
583
   */
584
  private static OutlierDetectionLoadBalancerConfig buildOutlierDetectionLbConfig(
585
      OutlierDetection outlierDetection, Object childConfig) {
586
    OutlierDetectionLoadBalancerConfig.Builder configBuilder
1✔
587
        = new OutlierDetectionLoadBalancerConfig.Builder();
588

589
    configBuilder.setChildConfig(childConfig);
1✔
590

591
    if (outlierDetection.intervalNanos() != null) {
1✔
592
      configBuilder.setIntervalNanos(outlierDetection.intervalNanos());
1✔
593
    }
594
    if (outlierDetection.baseEjectionTimeNanos() != null) {
1✔
595
      configBuilder.setBaseEjectionTimeNanos(outlierDetection.baseEjectionTimeNanos());
1✔
596
    }
597
    if (outlierDetection.maxEjectionTimeNanos() != null) {
1✔
598
      configBuilder.setMaxEjectionTimeNanos(outlierDetection.maxEjectionTimeNanos());
1✔
599
    }
600
    if (outlierDetection.maxEjectionPercent() != null) {
1✔
601
      configBuilder.setMaxEjectionPercent(outlierDetection.maxEjectionPercent());
1✔
602
    }
603

604
    SuccessRateEjection successRate = outlierDetection.successRateEjection();
1✔
605
    if (successRate != null) {
1✔
606
      OutlierDetectionLoadBalancerConfig.SuccessRateEjection.Builder
607
          successRateConfigBuilder = new OutlierDetectionLoadBalancerConfig
1✔
608
          .SuccessRateEjection.Builder();
609

610
      if (successRate.stdevFactor() != null) {
1✔
611
        successRateConfigBuilder.setStdevFactor(successRate.stdevFactor());
1✔
612
      }
613
      if (successRate.enforcementPercentage() != null) {
1✔
614
        successRateConfigBuilder.setEnforcementPercentage(successRate.enforcementPercentage());
1✔
615
      }
616
      if (successRate.minimumHosts() != null) {
1✔
617
        successRateConfigBuilder.setMinimumHosts(successRate.minimumHosts());
1✔
618
      }
619
      if (successRate.requestVolume() != null) {
1✔
620
        successRateConfigBuilder.setRequestVolume(successRate.requestVolume());
1✔
621
      }
622

623
      configBuilder.setSuccessRateEjection(successRateConfigBuilder.build());
1✔
624
    }
625

626
    FailurePercentageEjection failurePercentage = outlierDetection.failurePercentageEjection();
1✔
627
    if (failurePercentage != null) {
1✔
628
      OutlierDetectionLoadBalancerConfig.FailurePercentageEjection.Builder
629
          failurePercentageConfigBuilder = new OutlierDetectionLoadBalancerConfig
1✔
630
          .FailurePercentageEjection.Builder();
631

632
      if (failurePercentage.threshold() != null) {
1✔
633
        failurePercentageConfigBuilder.setThreshold(failurePercentage.threshold());
1✔
634
      }
635
      if (failurePercentage.enforcementPercentage() != null) {
1✔
636
        failurePercentageConfigBuilder.setEnforcementPercentage(
1✔
637
            failurePercentage.enforcementPercentage());
1✔
638
      }
639
      if (failurePercentage.minimumHosts() != null) {
1✔
640
        failurePercentageConfigBuilder.setMinimumHosts(failurePercentage.minimumHosts());
1✔
641
      }
642
      if (failurePercentage.requestVolume() != null) {
1✔
643
        failurePercentageConfigBuilder.setRequestVolume(failurePercentage.requestVolume());
1✔
644
      }
645

646
      configBuilder.setFailurePercentageEjection(failurePercentageConfigBuilder.build());
1✔
647
    }
648

649
    return configBuilder.build();
1✔
650
  }
651
}
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