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

grpc / grpc-java / #20138

06 Jan 2026 06:03PM UTC coverage: 88.702% (-0.007%) from 88.709%
#20138

push

github

web-flow
xds: Implement proactive connection in RingHashLoadBalancer

Implement proactive connection logic in RingHashLoadBalancer as
outlined in gRFC A61. This address the missing logic where the
balancer should initialize the first IDLE child when a child
balancer reports TRANSIENT_FAILURE and no other children are
connecting.

This behavior, which was previously present before #10610, ensures
that a backup subchannel starts connecting immediately outside of
the picker flow, reducing failover latency.

Fixes #12024

35376 of 39882 relevant lines covered (88.7%)

0.89 hits per line

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

94.74
/../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 final SynchronizationContext syncContext;
360
    private final List<RingEntry> ring;
361
    // Avoid synchronization between pickSubchannel and subchannel's connectivity state change,
362
    // freeze picker's view of subchannel's connectivity state.
363
    // TODO(chengyuanzhang): can be more performance-friendly with
364
    //  IdentityHashMap<Subchannel, ConnectivityStateInfo> and RingEntry contains Subchannel.
365
    private final Map<Endpoint, SubchannelView> pickableSubchannels;  // read-only
366
    @Nullable private final Metadata.Key<String> requestHashHeaderKey;
367
    private final ThreadSafeRandom random;
368
    private final boolean hasEndpointInConnectingState;
369

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

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

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

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

437
      int targetIndex = getTargetIndex(requestHash);
1✔
438

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

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

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

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

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

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

500
  }
501

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

510
    private SubchannelView(ChildLbState childLbState, ConnectivityState state) {
1✔
511
      this.childLbState = childLbState;
1✔
512
      this.connectivityState = state;
1✔
513
    }
1✔
514
  }
515

516
  private static final class RingEntry implements Comparable<RingEntry> {
517
    private final long hash;
518
    private final Endpoint addrKey;
519

520
    private RingEntry(long hash, Endpoint addrKey) {
1✔
521
      this.hash = hash;
1✔
522
      this.addrKey = addrKey;
1✔
523
    }
1✔
524

525
    @Override
526
    public int compareTo(RingEntry entry) {
527
      return Long.compare(hash, entry.hash);
1✔
528
    }
529
  }
530

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

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

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

561
    @Override
562
    public int hashCode() {
563
      return Objects.hash(minRingSize, maxRingSize, requestHashHeader);
1✔
564
    }
565

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