• 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

94.78
/../xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java
1
/*
2
 * Copyright 2021 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 com.google.common.base.Preconditions.checkState;
22
import static io.grpc.ConnectivityState.CONNECTING;
23
import static io.grpc.ConnectivityState.IDLE;
24
import static io.grpc.ConnectivityState.READY;
25
import static io.grpc.ConnectivityState.SHUTDOWN;
26
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
27

28
import com.google.common.annotations.VisibleForTesting;
29
import com.google.common.base.Joiner;
30
import com.google.common.base.MoreObjects;
31
import com.google.common.collect.HashMultiset;
32
import com.google.common.collect.Multiset;
33
import com.google.common.primitives.UnsignedInteger;
34
import io.grpc.Attributes;
35
import io.grpc.ConnectivityState;
36
import io.grpc.EquivalentAddressGroup;
37
import io.grpc.InternalLogId;
38
import io.grpc.LoadBalancer;
39
import io.grpc.Metadata;
40
import io.grpc.Status;
41
import io.grpc.SynchronizationContext;
42
import io.grpc.util.MultiChildLoadBalancer;
43
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
44
import io.grpc.xds.client.XdsLogger;
45
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
46
import java.net.SocketAddress;
47
import java.util.ArrayList;
48
import java.util.Collection;
49
import java.util.Collections;
50
import java.util.HashMap;
51
import java.util.HashSet;
52
import java.util.List;
53
import java.util.Map;
54
import java.util.Objects;
55
import java.util.Set;
56
import java.util.stream.Collectors;
57
import javax.annotation.Nullable;
58

59
/**
60
 * A {@link LoadBalancer} that provides consistent hashing based load balancing to upstream hosts.
61
 * It implements the "Ketama" hashing that maps hosts onto a circle (the "ring") by hashing its
62
 * addresses. Each request is routed to a host by hashing some property of the request and finding
63
 * the nearest corresponding host clockwise around the ring. Each host is placed on the ring some
64
 * number of times proportional to its weight. With the ring partitioned appropriately, the
65
 * addition or removal of one host from a set of N hosts will affect only 1/N requests.
66
 */
67
final class RingHashLoadBalancer extends MultiChildLoadBalancer {
68
  private static final Status RPC_HASH_NOT_FOUND =
1✔
69
      Status.INTERNAL.withDescription("RPC hash not found. Probably a bug because xds resolver"
1✔
70
          + " config selector always generates a hash.");
71
  private static final XxHash64 hashFunc = XxHash64.INSTANCE;
1✔
72

73
  private final LoadBalancer.Factory lazyLbFactory =
1✔
74
      new LazyLoadBalancer.Factory(pickFirstLbProvider);
75
  private final XdsLogger logger;
76
  private final SynchronizationContext syncContext;
77
  private final ThreadSafeRandom random;
78
  private List<RingEntry> ring;
79
  @Nullable private Metadata.Key<String> requestHashHeaderKey;
80

81
  RingHashLoadBalancer(Helper helper) {
82
    this(helper, ThreadSafeRandomImpl.instance);
1✔
83
  }
1✔
84

85
  @VisibleForTesting
86
  RingHashLoadBalancer(Helper helper, ThreadSafeRandom random) {
87
    super(helper);
1✔
88
    syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext");
1✔
89
    logger = XdsLogger.withLogId(InternalLogId.allocate("ring_hash_lb", helper.getAuthority()));
1✔
90
    logger.log(XdsLogLevel.INFO, "Created");
1✔
91
    this.random = checkNotNull(random, "random");
1✔
92
  }
1✔
93

94
  @Override
95
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
96
    logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
1✔
97
    List<EquivalentAddressGroup> addrList = resolvedAddresses.getAddresses();
1✔
98
    Status addressValidityStatus = validateAddrList(addrList);
1✔
99
    if (!addressValidityStatus.isOk()) {
1✔
100
      return addressValidityStatus;
1✔
101
    }
102

103
    // Now do the ringhash specific logic with weights and building the ring
104
    RingHashConfig config = (RingHashConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
105
    if (config == null) {
1✔
106
      throw new IllegalArgumentException("Missing RingHash configuration");
×
107
    }
108
    requestHashHeaderKey =
1✔
109
        config.requestHashHeader.isEmpty()
1✔
110
            ? null
1✔
111
            : Metadata.Key.of(config.requestHashHeader, Metadata.ASCII_STRING_MARSHALLER);
1✔
112
    Map<EquivalentAddressGroup, Long> serverWeights = new HashMap<>();
1✔
113
    long totalWeight = 0L;
1✔
114
    for (EquivalentAddressGroup eag : addrList) {
1✔
115
      Long weight = eag.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT);
1✔
116
      // Support two ways of server weighing: either multiple instances of the same address
117
      // or each address contains a per-address weight attribute. If a weight is not provided,
118
      // each occurrence of the address will be counted a weight value of one.
119
      if (weight == null) {
1✔
120
        weight = 1L;
×
121
      }
122
      totalWeight += weight;
1✔
123
      EquivalentAddressGroup addrKey = stripAttrs(eag);
1✔
124
      if (serverWeights.containsKey(addrKey)) {
1✔
125
        serverWeights.put(addrKey, serverWeights.get(addrKey) + weight);
×
126
      } else {
127
        serverWeights.put(addrKey, weight);
1✔
128
      }
129
    }
1✔
130
    // Calculate scale
131
    long minWeight = Collections.min(serverWeights.values());
1✔
132
    double normalizedMinWeight = (double) minWeight / totalWeight;
1✔
133
    // Scale up the number of hashes per host such that the least-weighted host gets a whole
134
    // number of hashes on the the ring. Other hosts might not end up with whole numbers, and
135
    // that's fine (the ring-building algorithm can handle this). This preserves the original
136
    // implementation's behavior: when weights aren't provided, all hosts should get an equal
137
    // number of hashes. In the case where this number exceeds the max_ring_size, it's scaled
138
    // back down to fit.
139
    double scale = Math.min(
1✔
140
        Math.ceil(normalizedMinWeight * config.minRingSize) / normalizedMinWeight,
1✔
141
        (double) config.maxRingSize);
142

143
    // Build the ring
144
    ring = buildRing(serverWeights, totalWeight, scale);
1✔
145

146
    return super.acceptResolvedAddresses(resolvedAddresses);
1✔
147
  }
148

149

150
  /**
151
   * Updates the overall balancing state by aggregating the connectivity states of all subchannels.
152
   *
153
   * <p>Aggregation rules (in order of dominance):
154
   * <ol>
155
   *   <li>If there is at least one subchannel in READY state, overall state is READY</li>
156
   *   <li>If there are <em>2 or more</em> subchannels in TRANSIENT_FAILURE, overall state is
157
   *   TRANSIENT_FAILURE (to allow timely failover to another policy)</li>
158
   *   <li>If there is at least one subchannel in CONNECTING state, overall state is
159
   *   CONNECTING</li>
160
   *   <li> If there is one subchannel in TRANSIENT_FAILURE state and there is
161
   *    more than one subchannel, report CONNECTING </li>
162
   *   <li>If there is at least one subchannel in IDLE state, overall state is IDLE</li>
163
   *   <li>Otherwise, overall state is TRANSIENT_FAILURE</li>
164
   * </ol>
165
   */
166
  @Override
167
  protected void updateOverallBalancingState() {
168
    checkState(!getChildLbStates().isEmpty(), "no subchannel has been created");
1✔
169
    if (this.currentConnectivityState == SHUTDOWN) {
1✔
170
      // Ignore changes that happen after shutdown is called
171
      logger.log(XdsLogLevel.DEBUG, "UpdateOverallBalancingState called after shutdown");
×
172
      return;
×
173
    }
174

175
    // Calculate the current overall state to report
176
    int numIdle = 0;
1✔
177
    int numReady = 0;
1✔
178
    int numConnecting = 0;
1✔
179
    int numTF = 0;
1✔
180

181
    forloop:
182
    for (ChildLbState childLbState : getChildLbStates()) {
1✔
183
      ConnectivityState state = childLbState.getCurrentState();
1✔
184
      switch (state) {
1✔
185
        case READY:
186
          numReady++;
1✔
187
          break forloop;
1✔
188
        case CONNECTING:
189
          numConnecting++;
1✔
190
          break;
1✔
191
        case IDLE:
192
          numIdle++;
1✔
193
          break;
1✔
194
        case TRANSIENT_FAILURE:
195
          numTF++;
1✔
196
          break;
1✔
197
        default:
198
          // ignore it
199
      }
200
    }
1✔
201

202
    ConnectivityState overallState;
203
    if (numReady > 0) {
1✔
204
      overallState = READY;
1✔
205
    } else if (numTF >= 2) {
1✔
206
      overallState = TRANSIENT_FAILURE;
1✔
207
    } else if (numConnecting > 0) {
1✔
208
      overallState = CONNECTING;
1✔
209
    } else if (numTF == 1 && getChildLbStates().size() > 1) {
1✔
210
      overallState = CONNECTING;
1✔
211
    } else if (numIdle > 0) {
1✔
212
      overallState = IDLE;
1✔
213
    } else {
214
      overallState = TRANSIENT_FAILURE;
×
215
    }
216

217
    // gRFC A61: if the aggregated connectivity state is TRANSIENT_FAILURE or CONNECTING and
218
    // there are no endpoints in CONNECTING state, the ring_hash policy will choose one of
219
    // the endpoints in IDLE state (if any) to trigger a connection attempt on
220
    if (numReady == 0 && numTF > 0 && numConnecting == 0 && numIdle > 0) {
1✔
221
      triggerIdleChildConnection();
1✔
222
    }
223

224
    RingHashPicker picker =
1✔
225
        new RingHashPicker(syncContext, ring, getChildLbStates(), requestHashHeaderKey, random);
1✔
226
    getHelper().updateBalancingState(overallState, picker);
1✔
227
    this.currentConnectivityState = overallState;
1✔
228
  }
1✔
229

230

231
  /**
232
   * Triggers a connection attempt for the first IDLE child load balancer.
233
   */
234
  private void triggerIdleChildConnection() {
235
    for (ChildLbState child : getChildLbStates()) {
1✔
236
      if (child.getCurrentState() == ConnectivityState.IDLE) {
1✔
237
        child.getLb().requestConnection();
1✔
238
        return;
1✔
239
      }
240
    }
1✔
241
  }
×
242

243
  @Override
244
  protected ChildLbState createChildLbState(Object key) {
245
    return new ChildLbState(key, lazyLbFactory);
1✔
246
  }
247

248
  private Status validateAddrList(List<EquivalentAddressGroup> addrList) {
249
    if (addrList.isEmpty()) {
1✔
250
      Status unavailableStatus = Status.UNAVAILABLE.withDescription("Ring hash lb error: EDS "
×
251
              + "resolution was successful, but returned server addresses are empty.");
252
      handleNameResolutionError(unavailableStatus);
×
253
      return unavailableStatus;
×
254
    }
255

256
    String dupAddrString = validateNoDuplicateAddresses(addrList);
1✔
257
    if (dupAddrString != null) {
1✔
258
      Status unavailableStatus = Status.UNAVAILABLE.withDescription("Ring hash lb error: EDS "
1✔
259
              + "resolution was successful, but there were duplicate addresses: " + dupAddrString);
260
      handleNameResolutionError(unavailableStatus);
1✔
261
      return unavailableStatus;
1✔
262
    }
263

264
    long totalWeight = 0;
1✔
265
    for (EquivalentAddressGroup eag : addrList) {
1✔
266
      Long weight = eag.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT);
1✔
267

268
      if (weight == null) {
1✔
269
        weight = 1L;
×
270
      }
271

272
      if (weight < 0) {
1✔
273
        Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
274
            String.format("Ring hash lb error: EDS resolution was successful, but returned a "
1✔
275
                        + "negative weight for %s.", stripAttrs(eag)));
1✔
276
        handleNameResolutionError(unavailableStatus);
1✔
277
        return unavailableStatus;
1✔
278
      }
279
      if (weight > UnsignedInteger.MAX_VALUE.longValue()) {
1✔
280
        Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
281
            String.format("Ring hash lb error: EDS resolution was successful, but returned a weight"
1✔
282
                + " too large to fit in an unsigned int for %s.", stripAttrs(eag)));
1✔
283
        handleNameResolutionError(unavailableStatus);
1✔
284
        return unavailableStatus;
1✔
285
      }
286
      totalWeight += weight;
1✔
287
    }
1✔
288

289
    if (totalWeight > UnsignedInteger.MAX_VALUE.longValue()) {
1✔
290
      Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
291
          String.format(
1✔
292
              "Ring hash lb error: EDS resolution was successful, but returned a sum of weights too"
293
                  + " large to fit in an unsigned int (%d).", totalWeight));
1✔
294
      handleNameResolutionError(unavailableStatus);
1✔
295
      return unavailableStatus;
1✔
296
    }
297

298
    return Status.OK;
1✔
299
  }
300

301
  @Nullable
302
  private String validateNoDuplicateAddresses(List<EquivalentAddressGroup> addrList) {
303
    Set<SocketAddress> addresses = new HashSet<>();
1✔
304
    Multiset<String> dups = HashMultiset.create();
1✔
305
    for (EquivalentAddressGroup eag : addrList) {
1✔
306
      for (SocketAddress address : eag.getAddresses()) {
1✔
307
        if (!addresses.add(address)) {
1✔
308
          dups.add(address.toString());
1✔
309
        }
310
      }
1✔
311
    }
1✔
312

313
    if (!dups.isEmpty()) {
1✔
314
      return dups.entrySet().stream()
1✔
315
          .map((dup) ->
1✔
316
              String.format("Address: %s, count: %d", dup.getElement(), dup.getCount() + 1))
1✔
317
          .collect(Collectors.joining("; "));
1✔
318
    }
319

320
    return null;
1✔
321
  }
322

323
  private static List<RingEntry> buildRing(
324
      Map<EquivalentAddressGroup, Long> serverWeights, long totalWeight, double scale) {
325
    List<RingEntry> ring = new ArrayList<>();
1✔
326
    double currentHashes = 0.0;
1✔
327
    double targetHashes = 0.0;
1✔
328
    for (Map.Entry<EquivalentAddressGroup, Long> entry : serverWeights.entrySet()) {
1✔
329
      Endpoint endpoint = new Endpoint(entry.getKey());
1✔
330
      double normalizedWeight = (double) entry.getValue() / totalWeight;
1✔
331
      // Per GRFC A61 use the first address for the hash
332
      StringBuilder sb = new StringBuilder(entry.getKey().getAddresses().get(0).toString());
1✔
333
      sb.append('_');
1✔
334
      int lengthWithoutCounter = sb.length();
1✔
335
      targetHashes += scale * normalizedWeight;
1✔
336
      long i = 0L;
1✔
337
      while (currentHashes < targetHashes) {
1✔
338
        sb.append(i);
1✔
339
        long hash = hashFunc.hashAsciiString(sb.toString());
1✔
340
        ring.add(new RingEntry(hash, endpoint));
1✔
341
        i++;
1✔
342
        currentHashes++;
1✔
343
        sb.setLength(lengthWithoutCounter);
1✔
344
      }
1✔
345
    }
1✔
346
    Collections.sort(ring);
1✔
347
    return Collections.unmodifiableList(ring);
1✔
348
  }
349

350
  @SuppressWarnings("ReferenceEquality")
351
  public static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) {
352
    if (eag.getAttributes() == Attributes.EMPTY) {
1✔
353
      return eag;
×
354
    }
355
    return new EquivalentAddressGroup(eag.getAddresses());
1✔
356
  }
357

358
  private static final class RingHashPicker extends SubchannelPicker {
359
    private static final PickResult RING_HASH_CONNECTING_RESULT =
1✔
360
        PickResult.withNoResult("connecting", "ring_hash: waiting for connection");
1✔
361
    private final SynchronizationContext syncContext;
362
    private final List<RingEntry> ring;
363
    // Avoid synchronization between pickSubchannel and subchannel's connectivity state change,
364
    // freeze picker's view of subchannel's connectivity state.
365
    // TODO(chengyuanzhang): can be more performance-friendly with
366
    //  IdentityHashMap<Subchannel, ConnectivityStateInfo> and RingEntry contains Subchannel.
367
    private final Map<Endpoint, SubchannelView> pickableSubchannels;  // read-only
368
    @Nullable private final Metadata.Key<String> requestHashHeaderKey;
369
    private final ThreadSafeRandom random;
370
    private final boolean hasEndpointInConnectingState;
371

372
    private RingHashPicker(
373
        SynchronizationContext syncContext, List<RingEntry> ring,
374
        Collection<ChildLbState> children, Metadata.Key<String> requestHashHeaderKey,
375
        ThreadSafeRandom random) {
1✔
376
      this.syncContext = syncContext;
1✔
377
      this.ring = ring;
1✔
378
      this.requestHashHeaderKey = requestHashHeaderKey;
1✔
379
      this.random = random;
1✔
380
      pickableSubchannels = new HashMap<>(children.size());
1✔
381
      boolean hasConnectingState = false;
1✔
382
      for (ChildLbState childLbState : children) {
1✔
383
        pickableSubchannels.put((Endpoint)childLbState.getKey(),
1✔
384
            new SubchannelView(childLbState, childLbState.getCurrentState()));
1✔
385
        if (childLbState.getCurrentState() == CONNECTING) {
1✔
386
          hasConnectingState = true;
1✔
387
        }
388
      }
1✔
389
      this.hasEndpointInConnectingState = hasConnectingState;
1✔
390
    }
1✔
391

392
    // Find the ring entry with hash next to (clockwise) the RPC's hash (binary search).
393
    private int getTargetIndex(long requestHash) {
394
      if (ring.size() <= 1) {
1✔
395
        return 0;
×
396
      }
397

398
      int low = 0;
1✔
399
      int high = ring.size() - 1;
1✔
400
      int mid = (low + high) / 2;
1✔
401
      do {
402
        long midVal = ring.get(mid).hash;
1✔
403
        long midValL = mid == 0 ? 0 : ring.get(mid - 1).hash;
1✔
404
        if (requestHash <= midVal && requestHash > midValL) {
1✔
405
          break;
1✔
406
        }
407
        if (midVal < requestHash) {
1✔
408
          low = mid + 1;
1✔
409
        } else {
410
          high =  mid - 1;
1✔
411
        }
412
        mid = (low + high) / 2;
1✔
413
      } while (mid < ring.size() && low <= high);
1✔
414
      return mid;
1✔
415
    }
416

417
    @Override
418
    public PickResult pickSubchannel(PickSubchannelArgs args) {
419
      // Determine request hash.
420
      boolean usingRandomHash = false;
1✔
421
      long requestHash;
422
      if (requestHashHeaderKey == null) {
1✔
423
        // Set by the xDS config selector.
424
        Long rpcHashFromCallOptions = args.getCallOptions().getOption(XdsNameResolver.RPC_HASH_KEY);
1✔
425
        if (rpcHashFromCallOptions == null) {
1✔
426
          return PickResult.withError(RPC_HASH_NOT_FOUND);
×
427
        }
428
        requestHash = rpcHashFromCallOptions;
1✔
429
      } else {
1✔
430
        Iterable<String> headerValues = args.getHeaders().getAll(requestHashHeaderKey);
1✔
431
        if (headerValues != null) {
1✔
432
          requestHash = hashFunc.hashAsciiString(Joiner.on(",").join(headerValues));
1✔
433
        } else {
434
          requestHash = random.nextLong();
1✔
435
          usingRandomHash = true;
1✔
436
        }
437
      }
438

439
      int targetIndex = getTargetIndex(requestHash);
1✔
440

441
      if (!usingRandomHash) {
1✔
442
        // Per gRFC A61, because of sticky-TF with PickFirst's auto reconnect on TF, we ignore
443
        // all TF subchannels and find the first ring entry in READY, CONNECTING or IDLE.  If
444
        // CONNECTING or IDLE we return a pick with no results.  Additionally, if that entry is in
445
        // IDLE, we initiate a connection.
446
        for (int i = 0; i < ring.size(); i++) {
1✔
447
          int index = (targetIndex + i) % ring.size();
1✔
448
          SubchannelView subchannelView = pickableSubchannels.get(ring.get(index).addrKey);
1✔
449
          ChildLbState childLbState = subchannelView.childLbState;
1✔
450

451
          if (subchannelView.connectivityState  == READY) {
1✔
452
            return childLbState.getCurrentPicker().pickSubchannel(args);
1✔
453
          }
454

455
          // RPCs can be buffered if the next subchannel is pending (per A62). Otherwise, RPCs
456
          // are failed unless there is a READY connection.
457
          if (subchannelView.connectivityState == CONNECTING) {
1✔
458
            return RING_HASH_CONNECTING_RESULT;
1✔
459
          }
460

461
          if (subchannelView.connectivityState == IDLE) {
1✔
462
            syncContext.execute(() -> {
1✔
463
              if (childLbState.getCurrentState() == IDLE) {
1✔
464
                childLbState.getLb().requestConnection();
1✔
465
              }
466
            });
1✔
467

468
            // Indicates that this should be retried after backoff
469
            return RING_HASH_CONNECTING_RESULT;
1✔
470
          }
471
        }
472
      } else {
473
        // Using a random hash. Find and use the first READY ring entry, triggering at most one
474
        // entry to attempt connection.
475
        boolean requestedConnection = hasEndpointInConnectingState;
1✔
476
        for (int i = 0; i < ring.size(); i++) {
1✔
477
          int index = (targetIndex + i) % ring.size();
1✔
478
          SubchannelView subchannelView = pickableSubchannels.get(ring.get(index).addrKey);
1✔
479
          ChildLbState childLbState = subchannelView.childLbState;
1✔
480
          if (subchannelView.connectivityState == READY) {
1✔
481
            return childLbState.getCurrentPicker().pickSubchannel(args);
1✔
482
          }
483
          if (!requestedConnection && subchannelView.connectivityState == IDLE) {
1✔
484
            syncContext.execute(() -> {
1✔
485
              if (childLbState.getCurrentState() == IDLE) {
1✔
486
                childLbState.getLb().requestConnection();
1✔
487
              }
488
            });
1✔
489
            requestedConnection = true;
1✔
490
          }
491
        }
492
        if (requestedConnection) {
1✔
493
          return RING_HASH_CONNECTING_RESULT;
1✔
494
        }
495
      }
496

497
      // return the pick from the original subchannel hit by hash, which is probably an error
498
      ChildLbState originalSubchannel =
1✔
499
          pickableSubchannels.get(ring.get(targetIndex).addrKey).childLbState;
1✔
500
      return originalSubchannel.getCurrentPicker().pickSubchannel(args);
1✔
501
    }
502

503
  }
504

505
  /**
506
   * An unmodifiable view of a subchannel with state not subject to its real connectivity
507
   * state changes.
508
   */
509
  private static final class SubchannelView {
510
    private final ChildLbState childLbState;
511
    private final ConnectivityState connectivityState;
512

513
    private SubchannelView(ChildLbState childLbState, ConnectivityState state) {
1✔
514
      this.childLbState = childLbState;
1✔
515
      this.connectivityState = state;
1✔
516
    }
1✔
517
  }
518

519
  private static final class RingEntry implements Comparable<RingEntry> {
520
    private final long hash;
521
    private final Endpoint addrKey;
522

523
    private RingEntry(long hash, Endpoint addrKey) {
1✔
524
      this.hash = hash;
1✔
525
      this.addrKey = addrKey;
1✔
526
    }
1✔
527

528
    @Override
529
    public int compareTo(RingEntry entry) {
530
      return Long.compare(hash, entry.hash);
1✔
531
    }
532
  }
533

534
  /**
535
   * Configures the ring property. The larger the ring is (that is, the more hashes there are
536
   * for each provided host) the better the request distribution will reflect the desired weights.
537
   */
538
  static final class RingHashConfig {
539
    final long minRingSize;
540
    final long maxRingSize;
541
    final String requestHashHeader;
542

543
    RingHashConfig(long minRingSize, long maxRingSize, String requestHashHeader) {
1✔
544
      checkArgument(minRingSize > 0, "minRingSize <= 0");
1✔
545
      checkArgument(maxRingSize > 0, "maxRingSize <= 0");
1✔
546
      checkArgument(minRingSize <= maxRingSize, "minRingSize > maxRingSize");
1✔
547
      checkNotNull(requestHashHeader);
1✔
548
      this.minRingSize = minRingSize;
1✔
549
      this.maxRingSize = maxRingSize;
1✔
550
      this.requestHashHeader = requestHashHeader;
1✔
551
    }
1✔
552

553
    @Override
554
    public boolean equals(Object o) {
555
      if (!(o instanceof RingHashConfig)) {
1✔
556
        return false;
1✔
557
      }
558
      RingHashConfig that = (RingHashConfig) o;
1✔
559
      return this.minRingSize == that.minRingSize
1✔
560
          && this.maxRingSize == that.maxRingSize
561
          && Objects.equals(this.requestHashHeader, that.requestHashHeader);
1✔
562
    }
563

564
    @Override
565
    public int hashCode() {
566
      return Objects.hash(minRingSize, maxRingSize, requestHashHeader);
1✔
567
    }
568

569
    @Override
570
    public String toString() {
571
      return MoreObjects.toStringHelper(this)
1✔
572
          .add("minRingSize", minRingSize)
1✔
573
          .add("maxRingSize", maxRingSize)
1✔
574
          .add("requestHashHeader", requestHashHeader)
1✔
575
          .toString();
1✔
576
    }
577
  }
578
}
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