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

grpc / grpc-java / #20365

27 Jul 2026 06:04AM UTC coverage: 89.199% (+0.07%) from 89.125%
#20365

push

github

web-flow
core: Implement LB Delay Observability (Proposal A121) (#12807)

This PR implements **Attempt-Level RPC Delay Observability** across the core channel transport, built-in load balancers, xDS policies, and the OpenTelemetry telemetry plugin, aligned with [gRPC Proposal A121](https://github.com/grpc/proposal/pull/556).

38276 of 42911 relevant lines covered (89.2%)

0.89 hits per line

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

93.9
/../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.CONNECTING;
21
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
22
import static io.grpc.xds.XdsLbPolicies.CDS_POLICY_NAME;
23
import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME;
24

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

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

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

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

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

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

201
  @Override
202
  public void handleNameResolutionError(Status error) {
203
    logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error);
1✔
204
    if (delegate != null) {
1✔
205
      delegate.handleNameResolutionError(error);
1✔
206
    } else {
207
      helper.updateBalancingState(
×
208
          TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
×
209
    }
210
  }
1✔
211

212
  @Override
213
  public void shutdown() {
214
    logger.log(XdsLogLevel.INFO, "Shutdown");
1✔
215
    delegate.shutdown();
1✔
216
    delegate = new GracefulSwitchLoadBalancer(cdsLbHelper);
1✔
217
    addBackendServicePickDetailsLabel = false;
1✔
218
    if (clusterSubscription != null) {
1✔
219
      clusterSubscription.close();
1✔
220
      clusterSubscription = null;
1✔
221
    }
222
  }
1✔
223

224
  @CheckReturnValue // don't forget to return up the stack after the fail call
225
  private Status fail(Status error) {
226
    delegate.shutdown();
1✔
227
    addBackendServicePickDetailsLabel = false;
1✔
228
    helper.updateBalancingState(
1✔
229
        TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
1✔
230
    return Status.OK; // XdsNameResolver isn't a polling NR, so this value doesn't matter
1✔
231
  }
232

233
  private String errorPrefix() {
234
    return "CdsLb for " + clusterName + ": ";
×
235
  }
236

237
  private final class CdsLbHelper extends ForwardingLoadBalancerHelper {
1✔
238
    @Override
239
    public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
240
      if (addBackendServicePickDetailsLabel) {
1✔
241
        newPicker = new BackendServiceMetricLabelSubchannelPicker(newPicker, clusterName);
1✔
242
      }
243
      delegate().updateBalancingState(newState, newPicker);
1✔
244
    }
1✔
245

246
    @Override
247
    protected Helper delegate() {
248
      return helper;
1✔
249
    }
250
  }
251

252
  private static final class BackendServiceMetricLabelSubchannelPicker extends SubchannelPicker {
253
    private final SubchannelPicker delegate;
254
    private final String backendService;
255

256
    private BackendServiceMetricLabelSubchannelPicker(
257
        SubchannelPicker delegate, String backendService) {
1✔
258
      this.delegate = checkNotNull(delegate, "delegate");
1✔
259
      this.backendService = checkNotNull(backendService, "backendService");
1✔
260
    }
1✔
261

262
    @Override
263
    public PickResult pickSubchannel(PickSubchannelArgs args) {
264
      args.getPickDetailsConsumer().addOptionalLabel("grpc.lb.backend_service", backendService);
1✔
265
      return delegate.pickSubchannel(args);
1✔
266
    }
267
  }
268

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

278
  /** Divide two uint32s and produce a fixed-point uint32 result. */
279
  private static long fractionToFixedPoint(long numerator, long denominator) {
280
    long one = 1L << FIXED_POINT_FRACTIONAL_BITS;
1✔
281
    return numerator * one / denominator;
1✔
282
  }
283

284
  /** Multiply two uint32 fixed-point numbers, returning a uint32 fixed-point. */
285
  private static long fixedPointMultiply(long a, long b) {
286
    return (a * b) >> FIXED_POINT_FRACTIONAL_BITS;
1✔
287
  }
288

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

307
  /**
308
   * Generates a string that represents the priority in the LB policy config. The string is unique
309
   * across priorities in all clusters and priorityName(c, p1) < priorityName(c, p2) iff p1 < p2.
310
   * The ordering is undefined for priorities in different clusters.
311
   */
312
  private static String priorityName(String cluster, int priority) {
313
    return cluster + "[child" + priority + "]";
1✔
314
  }
315

316
  /**
317
   * Generates a string that represents the locality in the LB policy config. The string is unique
318
   * across all localities in all clusters.
319
   */
320
  private static String localityName(Locality locality) {
321
    return "{region=\"" + locality.region()
1✔
322
        + "\", zone=\"" + locality.zone()
1✔
323
        + "\", sub_zone=\"" + locality.subZone()
1✔
324
        + "\"}";
325
  }
326

327
  private final class ClusterState {
1✔
328
    private Map<Locality, String> localityPriorityNames = Collections.emptyMap();
1✔
329
    int priorityNameGenId = 1;
1✔
330

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

370
      for (Locality locality : localityLbEndpoints.keySet()) {
1✔
371
        LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality);
1✔
372
        String priorityName = localityPriorityNames.get(locality);
1✔
373
        String localityName = localityName(locality);
1✔
374
        AddressFilter.PathChain pathChain =
1✔
375
            AddressFilter.createPathChain(Arrays.asList(priorityName, localityName));
1✔
376

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

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

462
    private SocketAddress rewriteAddress(SocketAddress addr,
463
        ImmutableMap<String, Object> endpointMetadata,
464
        ImmutableMap<String, Object> localityMetadata) {
465
      if (!(addr instanceof InetSocketAddress)) {
1✔
466
        return addr;
×
467
      }
468

469
      SocketAddress proxyAddress;
470
      try {
471
        proxyAddress = (SocketAddress) endpointMetadata.get(
1✔
472
            "envoy.http11_proxy_transport_socket.proxy_address");
473
        if (proxyAddress == null) {
1✔
474
          proxyAddress = (SocketAddress) localityMetadata.get(
1✔
475
              "envoy.http11_proxy_transport_socket.proxy_address");
476
        }
477
      } catch (ClassCastException e) {
×
478
        return addr;
×
479
      }
1✔
480

481
      if (proxyAddress == null) {
1✔
482
        return addr;
1✔
483
      }
484

485
      return HttpConnectProxiedSocketAddress.newBuilder()
1✔
486
          .setTargetAddress((InetSocketAddress) addr)
1✔
487
          .setProxyAddress(proxyAddress)
1✔
488
          .build();
1✔
489
    }
490

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

526
  private static class ClusterResolutionResult {
527
    // Endpoint addresses.
528
    private final List<EquivalentAddressGroup> addresses;
529
    // Config (include load balancing policy/config) for each priority in the cluster.
530
    private final Map<String, PriorityChildConfig> priorityChildConfigs;
531
    // List of priority names ordered in descending priorities.
532
    private final List<String> priorities;
533

534
    ClusterResolutionResult(List<EquivalentAddressGroup> addresses,
535
        Map<String, PriorityChildConfig> configs, List<String> priorities) {
1✔
536
      this.addresses = addresses;
1✔
537
      this.priorityChildConfigs = configs;
1✔
538
      this.priorities = priorities;
1✔
539
    }
1✔
540
  }
541

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

568
      // If outlier detection has been configured we wrap the child policy in the outlier detection
569
      // load balancer.
570
      if (discovery.outlierDetection() != null) {
1✔
571
        LoadBalancerProvider outlierDetectionProvider = lbRegistry.getProvider(
1✔
572
            "outlier_detection_experimental");
573
        priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(
1✔
574
            outlierDetectionProvider,
575
            buildOutlierDetectionLbConfig(discovery.outlierDetection(), priorityChildPolicy));
1✔
576
      }
577

578
      boolean isEds = discovery.clusterType() == ClusterType.EDS;
1✔
579
      PriorityChildConfig priorityChildConfig =
1✔
580
          new PriorityChildConfig(priorityChildPolicy, isEds /* ignoreReresolution */);
581
      configs.put(priority, priorityChildConfig);
1✔
582
    }
1✔
583
    return configs;
1✔
584
  }
585

586
  /**
587
   * Converts {@link OutlierDetection} that represents the xDS configuration to {@link
588
   * OutlierDetectionLoadBalancerConfig} that the {@link io.grpc.util.OutlierDetectionLoadBalancer}
589
   * understands.
590
   */
591
  private static OutlierDetectionLoadBalancerConfig buildOutlierDetectionLbConfig(
592
      OutlierDetection outlierDetection, Object childConfig) {
593
    OutlierDetectionLoadBalancerConfig.Builder configBuilder
1✔
594
        = new OutlierDetectionLoadBalancerConfig.Builder();
595

596
    configBuilder.setChildConfig(childConfig);
1✔
597

598
    if (outlierDetection.intervalNanos() != null) {
1✔
599
      configBuilder.setIntervalNanos(outlierDetection.intervalNanos());
1✔
600
    }
601
    if (outlierDetection.baseEjectionTimeNanos() != null) {
1✔
602
      configBuilder.setBaseEjectionTimeNanos(outlierDetection.baseEjectionTimeNanos());
1✔
603
    }
604
    if (outlierDetection.maxEjectionTimeNanos() != null) {
1✔
605
      configBuilder.setMaxEjectionTimeNanos(outlierDetection.maxEjectionTimeNanos());
1✔
606
    }
607
    if (outlierDetection.maxEjectionPercent() != null) {
1✔
608
      configBuilder.setMaxEjectionPercent(outlierDetection.maxEjectionPercent());
1✔
609
    }
610

611
    SuccessRateEjection successRate = outlierDetection.successRateEjection();
1✔
612
    if (successRate != null) {
1✔
613
      OutlierDetectionLoadBalancerConfig.SuccessRateEjection.Builder
614
          successRateConfigBuilder = new OutlierDetectionLoadBalancerConfig
1✔
615
          .SuccessRateEjection.Builder();
616

617
      if (successRate.stdevFactor() != null) {
1✔
618
        successRateConfigBuilder.setStdevFactor(successRate.stdevFactor());
1✔
619
      }
620
      if (successRate.enforcementPercentage() != null) {
1✔
621
        successRateConfigBuilder.setEnforcementPercentage(successRate.enforcementPercentage());
1✔
622
      }
623
      if (successRate.minimumHosts() != null) {
1✔
624
        successRateConfigBuilder.setMinimumHosts(successRate.minimumHosts());
1✔
625
      }
626
      if (successRate.requestVolume() != null) {
1✔
627
        successRateConfigBuilder.setRequestVolume(successRate.requestVolume());
1✔
628
      }
629

630
      configBuilder.setSuccessRateEjection(successRateConfigBuilder.build());
1✔
631
    }
632

633
    FailurePercentageEjection failurePercentage = outlierDetection.failurePercentageEjection();
1✔
634
    if (failurePercentage != null) {
1✔
635
      OutlierDetectionLoadBalancerConfig.FailurePercentageEjection.Builder
636
          failurePercentageConfigBuilder = new OutlierDetectionLoadBalancerConfig
1✔
637
          .FailurePercentageEjection.Builder();
638

639
      if (failurePercentage.threshold() != null) {
1✔
640
        failurePercentageConfigBuilder.setThreshold(failurePercentage.threshold());
1✔
641
      }
642
      if (failurePercentage.enforcementPercentage() != null) {
1✔
643
        failurePercentageConfigBuilder.setEnforcementPercentage(
1✔
644
            failurePercentage.enforcementPercentage());
1✔
645
      }
646
      if (failurePercentage.minimumHosts() != null) {
1✔
647
        failurePercentageConfigBuilder.setMinimumHosts(failurePercentage.minimumHosts());
1✔
648
      }
649
      if (failurePercentage.requestVolume() != null) {
1✔
650
        failurePercentageConfigBuilder.setRequestVolume(failurePercentage.requestVolume());
1✔
651
      }
652

653
      configBuilder.setFailurePercentageEjection(failurePercentageConfigBuilder.build());
1✔
654
    }
655

656
    return configBuilder.build();
1✔
657
  }
658
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc