• 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

98.1
/../core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java
1
/*
2
 * Copyright 2023 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.internal;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static io.grpc.ConnectivityState.CONNECTING;
21
import static io.grpc.ConnectivityState.IDLE;
22
import static io.grpc.ConnectivityState.READY;
23
import static io.grpc.ConnectivityState.SHUTDOWN;
24
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
25

26
import com.google.common.annotations.VisibleForTesting;
27
import com.google.common.collect.ImmutableList;
28
import com.google.common.collect.Lists;
29
import com.google.errorprone.annotations.CheckReturnValue;
30
import io.grpc.Attributes;
31
import io.grpc.ConnectivityState;
32
import io.grpc.ConnectivityStateInfo;
33
import io.grpc.EquivalentAddressGroup;
34
import io.grpc.InternalEquivalentAddressGroup;
35
import io.grpc.LoadBalancer;
36
import io.grpc.Status;
37
import io.grpc.SynchronizationContext.ScheduledHandle;
38
import java.net.Inet4Address;
39
import java.net.InetSocketAddress;
40
import java.net.SocketAddress;
41
import java.util.ArrayList;
42
import java.util.Collections;
43
import java.util.HashMap;
44
import java.util.HashSet;
45
import java.util.List;
46
import java.util.Map;
47
import java.util.Random;
48
import java.util.Set;
49
import java.util.concurrent.TimeUnit;
50
import java.util.concurrent.atomic.AtomicBoolean;
51
import java.util.logging.Level;
52
import java.util.logging.Logger;
53
import javax.annotation.Nullable;
54

55
/**
56
 * A {@link LoadBalancer} that provides no load-balancing over the addresses from the {@link
57
 * io.grpc.NameResolver}. The channel's default behavior is used, which is walking down the address
58
 * list and sticking to the first that works.
59
 */
60
final class PickFirstLeafLoadBalancer extends LoadBalancer {
61
  private static final Logger log = Logger.getLogger(PickFirstLeafLoadBalancer.class.getName());
1✔
62
  @VisibleForTesting
63
  static final int CONNECTION_DELAY_INTERVAL_MS = 250;
64
  private final boolean enableHappyEyeballs = !isSerializingRetries()
1✔
65
      && PickFirstLoadBalancerProvider.isEnabledHappyEyeballs();
1✔
66
  static boolean weightedShuffling =
1✔
67
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_PF_WEIGHTED_SHUFFLING", true);
1✔
68
  private final Helper helper;
69
  private final Map<SocketAddress, SubchannelData> subchannels = new HashMap<>();
1✔
70
  private final Index addressIndex = new Index(ImmutableList.of(), this.enableHappyEyeballs);
1✔
71
  private int numTf = 0;
1✔
72
  private boolean firstPass = true;
1✔
73
  @Nullable
1✔
74
  private ScheduledHandle scheduleConnectionTask = null;
75
  private ConnectivityState rawConnectivityState = IDLE;
1✔
76
  private ConnectivityState concludedState = IDLE;
1✔
77
  private boolean notAPetiolePolicy = true; // means not under a petiole policy
1✔
78
  private final BackoffPolicy.Provider bkoffPolProvider = new ExponentialBackoffPolicy.Provider();
1✔
79
  private BackoffPolicy reconnectPolicy;
80
  @Nullable
1✔
81
  private ScheduledHandle reconnectTask = null;
82
  private final boolean serializingRetries = isSerializingRetries();
1✔
83

84
  PickFirstLeafLoadBalancer(Helper helper) {
1✔
85
    this.helper = checkNotNull(helper, "helper");
1✔
86
  }
1✔
87

88
  static boolean isSerializingRetries() {
89
    return GrpcUtil.getFlag("GRPC_SERIALIZE_RETRIES", false);
1✔
90
  }
91

92
  @Override
93
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
94
    if (rawConnectivityState == SHUTDOWN) {
1✔
95
      return Status.FAILED_PRECONDITION.withDescription("Already shut down");
1✔
96
    }
97

98
    // Check whether this is a petiole policy, which is based off of an address attribute
99
    Boolean isPetiolePolicy = resolvedAddresses.getAttributes().get(IS_PETIOLE_POLICY);
1✔
100
    this.notAPetiolePolicy = isPetiolePolicy == null || !isPetiolePolicy;
1✔
101

102
    List<EquivalentAddressGroup> servers = resolvedAddresses.getAddresses();
1✔
103

104
    // Validate the address list
105
    if (servers.isEmpty()) {
1✔
106
      Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
107
              "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses()
1✔
108
                      + ", attrs=" + resolvedAddresses.getAttributes());
1✔
109
      handleNameResolutionError(unavailableStatus);
1✔
110
      return unavailableStatus;
1✔
111
    }
112
    for (EquivalentAddressGroup eag : servers) {
1✔
113
      if (eag == null) {
1✔
114
        Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
115
            "NameResolver returned address list with null endpoint. addrs="
116
                + resolvedAddresses.getAddresses() + ", attrs="
1✔
117
                + resolvedAddresses.getAttributes());
1✔
118
        handleNameResolutionError(unavailableStatus);
1✔
119
        return unavailableStatus;
1✔
120
      }
121
    }
1✔
122

123
    // Since we have a new set of addresses, we are again at first pass
124
    firstPass = true;
1✔
125

126
    List<EquivalentAddressGroup> cleanServers = deDupAddresses(servers);
1✔
127

128
    // We can optionally be configured to shuffle the address list. This can help better distribute
129
    // the load.
130
    if (resolvedAddresses.getLoadBalancingPolicyConfig()
1✔
131
        instanceof PickFirstLeafLoadBalancerConfig) {
132
      PickFirstLeafLoadBalancerConfig config
1✔
133
          = (PickFirstLeafLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
134
      if (config.shuffleAddressList != null && config.shuffleAddressList) {
1✔
135
        cleanServers = shuffle(
1✔
136
            cleanServers, config.randomSeed != null ? new Random(config.randomSeed) : new Random());
1✔
137
      }
138
    }
139

140
    final ImmutableList<EquivalentAddressGroup> newImmutableAddressGroups =
1✔
141
        ImmutableList.copyOf(cleanServers);
1✔
142

143
    if (rawConnectivityState == READY
1✔
144
        || (rawConnectivityState == CONNECTING
145
          && (!enableHappyEyeballs || addressIndex.isValid()))) {
1✔
146
      // If the previous ready (or connecting) subchannel exists in new address list,
147
      // keep this connection and don't create new subchannels. Happy Eyeballs is excluded when
148
      // connecting, because it allows multiple attempts simultaneously, thus is fine to start at
149
      // the beginning.
150
      SocketAddress previousAddress = addressIndex.getCurrentAddress();
1✔
151
      addressIndex.updateGroups(newImmutableAddressGroups);
1✔
152
      if (addressIndex.seekTo(previousAddress)) {
1✔
153
        SubchannelData subchannelData = subchannels.get(previousAddress);
1✔
154
        subchannelData.getSubchannel().updateAddresses(addressIndex.getCurrentEagAsList());
1✔
155
        shutdownRemovedAddresses(newImmutableAddressGroups);
1✔
156
        return Status.OK;
1✔
157
      }
158
      // Previous ready subchannel not in the new list of addresses
159
    } else {
1✔
160
      addressIndex.updateGroups(newImmutableAddressGroups);
1✔
161
    }
162

163
    // No old addresses means first time through, so we will do an explicit move to CONNECTING
164
    // which is what we implicitly started with
165
    boolean noOldAddrs = shutdownRemovedAddresses(newImmutableAddressGroups);
1✔
166

167
    if (noOldAddrs) {
1✔
168
      // Make tests happy; they don't properly assume starting in CONNECTING
169
      rawConnectivityState = CONNECTING;
1✔
170
      updateBalancingState(
1✔
171
          CONNECTING,
172
          new FixedResultPicker(
173
              PickResult.withNoResult("connecting", "pick_first: address list updated")));
1✔
174
    }
175

176
    if (rawConnectivityState == READY) {
1✔
177
      // connect from beginning when prompted
178
      rawConnectivityState = IDLE;
1✔
179
      updateBalancingState(IDLE, new RequestConnectionPicker(this));
1✔
180

181
    } else if (rawConnectivityState == CONNECTING || rawConnectivityState == TRANSIENT_FAILURE) {
1✔
182
      // start connection attempt at first address
183
      cancelScheduleTask();
1✔
184
      requestConnection();
1✔
185
    }
186

187
    return Status.OK;
1✔
188
  }
189

190
  /**
191
   * Compute the difference between the flattened new addresses and the old addresses that had been
192
   * made into subchannels and then shutdown the matching subchannels.
193
   * @return true if there were no old addresses
194
   */
195
  private boolean shutdownRemovedAddresses(
196
      ImmutableList<EquivalentAddressGroup> newImmutableAddressGroups) {
197

198
    Set<SocketAddress> oldAddrs = new HashSet<>(subchannels.keySet());
1✔
199

200
    // Flatten the new EAGs addresses
201
    Set<SocketAddress> newAddrs = new HashSet<>();
1✔
202
    for (EquivalentAddressGroup endpoint : newImmutableAddressGroups) {
1✔
203
      newAddrs.addAll(endpoint.getAddresses());
1✔
204
    }
1✔
205

206
    // Shut them down and remove them
207
    for (SocketAddress oldAddr : oldAddrs) {
1✔
208
      if (!newAddrs.contains(oldAddr)) {
1✔
209
        subchannels.remove(oldAddr).getSubchannel().shutdown();
1✔
210
      }
211
    }
1✔
212
    return oldAddrs.isEmpty();
1✔
213
  }
214

215
  private static List<EquivalentAddressGroup> deDupAddresses(List<EquivalentAddressGroup> groups) {
216
    Set<SocketAddress> seenAddresses = new HashSet<>();
1✔
217
    List<EquivalentAddressGroup> newGroups = new ArrayList<>();
1✔
218

219
    for (EquivalentAddressGroup group : groups) {
1✔
220
      List<SocketAddress> addrs = new ArrayList<>();
1✔
221
      for (SocketAddress addr : group.getAddresses()) {
1✔
222
        if (seenAddresses.add(addr)) {
1✔
223
          addrs.add(addr);
1✔
224
        }
225
      }
1✔
226
      if (!addrs.isEmpty()) {
1✔
227
        newGroups.add(new EquivalentAddressGroup(addrs, group.getAttributes()));
1✔
228
      }
229
    }
1✔
230

231
    return newGroups;
1✔
232
  }
233

234
  // Also used by PickFirstLoadBalancer
235
  @CheckReturnValue
236
  static List<EquivalentAddressGroup> shuffle(List<EquivalentAddressGroup> eags, Random random) {
237
    if (weightedShuffling) {
1✔
238
      List<WeightEntry> weightedEntries = new ArrayList<>(eags.size());
1✔
239
      for (EquivalentAddressGroup eag : eags) {
1✔
240
        weightedEntries.add(new WeightEntry(eag, eagToWeight(eag, random)));
1✔
241
      }
1✔
242
      Collections.sort(weightedEntries, Collections.reverseOrder() /* descending */);
1✔
243
      return Lists.transform(weightedEntries, entry -> entry.eag);
1✔
244
    } else {
245
      List<EquivalentAddressGroup> eagsCopy = new ArrayList<>(eags);
1✔
246
      Collections.shuffle(eagsCopy, random);
1✔
247
      return eagsCopy;
1✔
248
    }
249
  }
250

251
  private static double eagToWeight(EquivalentAddressGroup eag, Random random) {
252
    Long weight = eag.getAttributes().get(InternalEquivalentAddressGroup.ATTR_WEIGHT);
1✔
253
    if (weight == null) {
1✔
254
      weight = 1L;
1✔
255
    }
256
    return Math.pow(random.nextDouble(), 1.0 / weight);
1✔
257
  }
258

259
  private static final class WeightEntry implements Comparable<WeightEntry> {
260
    final EquivalentAddressGroup eag;
261
    final double weight;
262

263
    public WeightEntry(EquivalentAddressGroup eag, double weight) {
1✔
264
      this.eag = eag;
1✔
265
      this.weight = weight;
1✔
266
    }
1✔
267

268
    @Override
269
    public int compareTo(WeightEntry entry) {
270
      return Double.compare(this.weight, entry.weight);
1✔
271
    }
272
  }
273

274
  @Override
275
  public void handleNameResolutionError(Status error) {
276
    if (rawConnectivityState == SHUTDOWN) {
1✔
277
      return;
×
278
    }
279

280
    for (SubchannelData subchannelData : subchannels.values()) {
1✔
281
      subchannelData.getSubchannel().shutdown();
1✔
282
    }
1✔
283
    subchannels.clear();
1✔
284
    addressIndex.updateGroups(ImmutableList.of());
1✔
285
    rawConnectivityState = TRANSIENT_FAILURE;
1✔
286
    updateBalancingState(TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
1✔
287
  }
1✔
288

289
  void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo stateInfo) {
290
    ConnectivityState newState = stateInfo.getState();
1✔
291

292
    // Shutdown channels/previously relevant subchannels can still callback with state updates.
293
    // To prevent pickers from returning these obsolete subchannels, this logic
294
    // is included to check if the current list of active subchannels includes this subchannel.
295
    if (subchannelData != subchannels.get(getAddress(subchannelData.subchannel))) {
1✔
296
      return;
1✔
297
    }
298

299
    if (newState == SHUTDOWN) {
1✔
300
      return;
1✔
301
    }
302

303
    if (newState == IDLE && subchannelData.state == READY) {
1✔
304
      helper.refreshNameResolution();
1✔
305
    }
306

307
    // If we are transitioning from a TRANSIENT_FAILURE to CONNECTING or IDLE we ignore this state
308
    // transition and still keep the LB in TRANSIENT_FAILURE state. This is referred to as "sticky
309
    // transient failure". Only a subchannel state change to READY will get the LB out of
310
    // TRANSIENT_FAILURE. If the state is IDLE we additionally request a new connection so that we
311
    // keep retrying for a connection.
312

313
    // With the new pick first implementation, individual subchannels will have their own backoff
314
    // on a per-address basis. Thus, iterative requests for connections will not be requested
315
    // once the first pass through is complete.
316
    // However, every time there is an address update, we will perform a pass through for the new
317
    // addresses in the updated list.
318
    subchannelData.updateState(newState);
1✔
319
    if (rawConnectivityState == TRANSIENT_FAILURE || concludedState == TRANSIENT_FAILURE)  {
1✔
320
      if (newState == CONNECTING) {
1✔
321
        // each subchannel is responsible for its own backoff
322
        return;
1✔
323
      } else if (newState == IDLE) {
1✔
324
        requestConnection();
1✔
325
        return;
1✔
326
      }
327
    }
328

329
    switch (newState) {
1✔
330
      case IDLE:
331
        // Shutdown when ready: connect from beginning when prompted
332
        addressIndex.reset();
1✔
333
        rawConnectivityState = IDLE;
1✔
334
        updateBalancingState(IDLE, new RequestConnectionPicker(this));
1✔
335
        break;
1✔
336

337
      case CONNECTING:
338
        rawConnectivityState = CONNECTING;
1✔
339
        // If we get a newly resolved address list via acceptResolvedAddresses,
340
        // as we are in CONNECTING, we will try to .updateAddresses the currently
341
        // connecting subchannel if it exists in the new list.
342
        // As such, We need to make sure that with transitioning to CONNECTING the subchannel for
343
        // the current address of a valid index exists.
344
        if ((!enableHappyEyeballs && !addressIndex.isValid())
1✔
345
            || (addressIndex.isValid() && !subchannels.containsKey(
1✔
346
                addressIndex.getCurrentAddress()))) {
1✔
347
          addressIndex.seekTo(getAddress(subchannelData.subchannel));
1✔
348
        }
349
        updateBalancingState(
1✔
350
            CONNECTING,
351
            new FixedResultPicker(
352
                PickResult.withNoResult("connecting", "pick_first: attempting to connect")));
1✔
353
        break;
1✔
354

355
      case READY:
356
        shutdownRemaining(subchannelData);
1✔
357
        addressIndex.seekTo(getAddress(subchannelData.subchannel));
1✔
358
        rawConnectivityState = READY;
1✔
359
        updateHealthCheckedState(subchannelData);
1✔
360
        break;
1✔
361

362
      case TRANSIENT_FAILURE:
363
        // If we are looking at current channel, request a connection if possible
364
        if (addressIndex.isValid()
1✔
365
            && subchannels.get(addressIndex.getCurrentAddress()) == subchannelData) {
1✔
366
          if (addressIndex.increment()) {
1✔
367
            cancelScheduleTask();
1✔
368
            requestConnection(); // is recursive so might hit the end of the addresses
1✔
369
          } else {
370
            if (subchannels.size() >= addressIndex.size()) {
1✔
371
              scheduleBackoff();
1✔
372
            } else {
373
              // We must have done a seek to the middle of the list lets start over from the
374
              // beginning
375
              addressIndex.reset();
1✔
376
              requestConnection();
1✔
377
            }
378
          }
379
        }
380

381
        if (isPassComplete()) {
1✔
382
          rawConnectivityState = TRANSIENT_FAILURE;
1✔
383
          updateBalancingState(TRANSIENT_FAILURE,
1✔
384
              new FixedResultPicker(PickResult.withError(stateInfo.getStatus())));
1✔
385

386
          // Refresh Name Resolution, but only when all 3 conditions are met
387
          // * We are at the end of addressIndex
388
          // * have had status reported for all subchannels.
389
          // * And one of the following conditions:
390
          //    * Have had enough TF reported since we completed first pass
391
          //    * Just completed the first pass
392
          if (++numTf >= addressIndex.size() || firstPass) {
1✔
393
            firstPass = false;
1✔
394
            numTf = 0;
1✔
395
            helper.refreshNameResolution();
1✔
396
          }
397
        }
398
        break;
399

400
      default:
401
        throw new IllegalArgumentException("Unsupported state:" + newState);
×
402
    }
403
  }
1✔
404

405
  /**
406
   * Only called after all addresses attempted and failed (TRANSIENT_FAILURE).
407
   */
408
  private void scheduleBackoff() {
409
    if (!serializingRetries) {
1✔
410
      return;
1✔
411
    }
412

413
    class EndOfCurrentBackoff implements Runnable {
1✔
414
      @Override
415
      public void run() {
416
        reconnectTask = null;
1✔
417
        addressIndex.reset();
1✔
418
        requestConnection();
1✔
419
      }
1✔
420
    }
421

422
    // Just allow the previous one to trigger when ready if we're already in backoff
423
    if (reconnectTask != null) {
1✔
424
      return;
×
425
    }
426

427
    if (reconnectPolicy == null) {
1✔
428
      reconnectPolicy = bkoffPolProvider.get();
1✔
429
    }
430
    long delayNanos = reconnectPolicy.nextBackoffNanos();
1✔
431
    reconnectTask = helper.getSynchronizationContext().schedule(
1✔
432
        new EndOfCurrentBackoff(),
433
        delayNanos,
434
        TimeUnit.NANOSECONDS,
435
        helper.getScheduledExecutorService());
1✔
436
  }
1✔
437

438
  private void updateHealthCheckedState(SubchannelData subchannelData) {
439
    if (subchannelData.state != READY) {
1✔
440
      return;
1✔
441
    }
442

443
    if (notAPetiolePolicy || subchannelData.getHealthState() == READY) {
1✔
444
      updateBalancingState(READY,
1✔
445
          new FixedResultPicker(PickResult.withSubchannel(subchannelData.subchannel)));
1✔
446
    } else if (subchannelData.getHealthState() == TRANSIENT_FAILURE) {
1✔
447
      updateBalancingState(TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(
1✔
448
          subchannelData.healthStateInfo.getStatus())));
1✔
449
    } else if (concludedState != TRANSIENT_FAILURE) {
1✔
450
      updateBalancingState(subchannelData.getHealthState(), new FixedResultPicker(
1✔
451
          PickResult.withNoResult("connecting",
1✔
452
              "health check state: " + subchannelData.getHealthState())));
1✔
453
    }
454
  }
1✔
455

456
  private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) {
457
    // an optimization: de-dup IDLE or CONNECTING notification.
458
    if (state == concludedState && (state == IDLE || state == CONNECTING)) {
1✔
459
      return;
1✔
460
    }
461
    concludedState = state;
1✔
462
    helper.updateBalancingState(state, picker);
1✔
463
  }
1✔
464

465
  @Override
466
  public void shutdown() {
467
    log.log(Level.FINE,
1✔
468
        "Shutting down, currently have {} subchannels created", subchannels.size());
1✔
469
    rawConnectivityState = SHUTDOWN;
1✔
470
    concludedState = SHUTDOWN;
1✔
471
    cancelScheduleTask();
1✔
472
    if (reconnectTask != null) {
1✔
473
      reconnectTask.cancel();
1✔
474
      reconnectTask = null;
1✔
475
    }
476
    reconnectPolicy = null;
1✔
477

478
    for (SubchannelData subchannelData : subchannels.values()) {
1✔
479
      subchannelData.getSubchannel().shutdown();
1✔
480
    }
1✔
481

482
    subchannels.clear();
1✔
483
  }
1✔
484

485
  /**
486
  * Shuts down remaining subchannels. Called when a subchannel becomes ready, which means
487
  * that all other subchannels must be shutdown.
488
  */
489
  private void shutdownRemaining(SubchannelData activeSubchannelData) {
490
    if (reconnectTask != null) {
1✔
491
      reconnectTask.cancel();
1✔
492
      reconnectTask = null;
1✔
493
    }
494
    reconnectPolicy = null;
1✔
495

496
    cancelScheduleTask();
1✔
497
    for (SubchannelData subchannelData : subchannels.values()) {
1✔
498
      if (!subchannelData.getSubchannel().equals(activeSubchannelData.subchannel)) {
1✔
499
        subchannelData.getSubchannel().shutdown();
1✔
500
      }
501
    }
1✔
502
    subchannels.clear();
1✔
503
    activeSubchannelData.updateState(READY);
1✔
504
    subchannels.put(getAddress(activeSubchannelData.subchannel), activeSubchannelData);
1✔
505
  }
1✔
506

507
  /**
508
   * Requests a connection to the next applicable address' subchannel, creating one if necessary.
509
   * Schedules a connection to next address in list as well.
510
   * If the current channel has already attempted a connection, we attempt a connection
511
   * to the next address/subchannel in our list.  We assume that createNewSubchannel will never
512
   * return null.
513
   */
514
  @Override
515
  public void requestConnection() {
516
    if (!addressIndex.isValid() || rawConnectivityState == SHUTDOWN || reconnectTask != null) {
1✔
517
      return;
1✔
518
    }
519

520
    SocketAddress currentAddress = addressIndex.getCurrentAddress();
1✔
521
    SubchannelData subchannelData = subchannels.get(currentAddress);
1✔
522
    if (subchannelData == null) {
1✔
523
      subchannelData = createNewSubchannel(currentAddress, addressIndex.getCurrentEagAttributes());
1✔
524
    }
525

526
    ConnectivityState subchannelState = subchannelData.getState();
1✔
527
    switch (subchannelState) {
1✔
528
      case IDLE:
529
        subchannelData.subchannel.requestConnection();
1✔
530
        subchannelData.updateState(CONNECTING);
1✔
531
        scheduleNextConnection();
1✔
532
        break;
1✔
533
      case CONNECTING:
534
        scheduleNextConnection();
1✔
535
        break;
1✔
536
      case TRANSIENT_FAILURE:
537
        if (!serializingRetries) {
1✔
538
          addressIndex.increment();
1✔
539
          requestConnection();
1✔
540
        } else {
541
          if (!addressIndex.isValid()) {
1✔
542
            scheduleBackoff();
×
543
          } else {
544
            subchannelData.subchannel.requestConnection();
1✔
545
            subchannelData.updateState(CONNECTING);
1✔
546
          }
547
        }
548
        break;
1✔
549
      default:
550
        // Wait for current subchannel to change state
551
    }
552
  }
1✔
553

554

555
  /**
556
  * Happy Eyeballs
557
  * Schedules connection attempt to happen after a delay to the next available address.
558
  */
559
  private void scheduleNextConnection() {
560
    if (!enableHappyEyeballs
1✔
561
        || (scheduleConnectionTask != null && scheduleConnectionTask.isPending())) {
1✔
562
      return;
1✔
563
    }
564

565
    class StartNextConnection implements Runnable {
1✔
566
      @Override
567
      public void run() {
568
        scheduleConnectionTask = null;
1✔
569
        if (addressIndex.increment()) {
1✔
570
          requestConnection();
1✔
571
        }
572
      }
1✔
573
    }
574

575
    scheduleConnectionTask = helper.getSynchronizationContext().schedule(
1✔
576
        new StartNextConnection(),
577
        CONNECTION_DELAY_INTERVAL_MS,
578
        TimeUnit.MILLISECONDS,
579
        helper.getScheduledExecutorService());
1✔
580
  }
1✔
581

582
  private void cancelScheduleTask() {
583
    if (scheduleConnectionTask != null) {
1✔
584
      scheduleConnectionTask.cancel();
1✔
585
      scheduleConnectionTask = null;
1✔
586
    }
587
  }
1✔
588

589
  private SubchannelData createNewSubchannel(SocketAddress addr, Attributes attrs) {
590
    HealthListener hcListener = new HealthListener();
1✔
591
    final Subchannel subchannel = helper.createSubchannel(
1✔
592
        CreateSubchannelArgs.newBuilder()
1✔
593
            .setAddresses(Lists.newArrayList(
1✔
594
                new EquivalentAddressGroup(addr, attrs)))
595
            .addOption(HEALTH_CONSUMER_LISTENER_ARG_KEY, hcListener)
1✔
596
            .addOption(LoadBalancer.DISABLE_SUBCHANNEL_RECONNECT_KEY, serializingRetries)
1✔
597
            .build());
1✔
598
    if (subchannel == null) {
1✔
599
      log.warning("Was not able to create subchannel for " + addr);
×
600
      throw new IllegalStateException("Can't create subchannel");
×
601
    }
602
    SubchannelData subchannelData = new SubchannelData(subchannel, IDLE);
1✔
603
    hcListener.subchannelData = subchannelData;
1✔
604
    subchannels.put(addr, subchannelData);
1✔
605
    Attributes scAttrs = subchannel.getAttributes();
1✔
606
    if (notAPetiolePolicy || scAttrs.get(LoadBalancer.HAS_HEALTH_PRODUCER_LISTENER_KEY) == null) {
1✔
607
      subchannelData.healthStateInfo = ConnectivityStateInfo.forNonError(READY);
1✔
608
    }
609
    subchannel.start(stateInfo -> processSubchannelState(subchannelData, stateInfo));
1✔
610
    return subchannelData;
1✔
611
  }
612

613
  private boolean isPassComplete() {
614
    if (subchannels.size() < addressIndex.size()) {
1✔
615
      return false;
1✔
616
    }
617
    for (SubchannelData sc : subchannels.values()) {
1✔
618
      if (!sc.isCompletedConnectivityAttempt() ) {
1✔
619
        return false;
1✔
620
      }
621
    }
1✔
622
    return true;
1✔
623
  }
624

625
  private final class HealthListener implements SubchannelStateListener {
1✔
626
    private SubchannelData subchannelData;
627

628
    @Override
629
    public void onSubchannelState(ConnectivityStateInfo newState) {
630
      if (notAPetiolePolicy) {
1✔
631
        log.log(Level.WARNING,
1✔
632
            "Ignoring health status {0} for subchannel {1} as this is not under a petiole policy",
633
            new Object[]{newState, subchannelData.subchannel});
1✔
634
        return;
1✔
635
      }
636

637
      log.log(Level.FINE, "Received health status {0} for subchannel {1}",
1✔
638
          new Object[]{newState, subchannelData.subchannel});
1✔
639
      subchannelData.healthStateInfo = newState;
1✔
640
      if (addressIndex.isValid()
1✔
641
          && subchannelData == subchannels.get(addressIndex.getCurrentAddress())) {
1✔
642
        updateHealthCheckedState(subchannelData);
1✔
643
      }
644
    }
1✔
645
  }
646

647
  private SocketAddress getAddress(Subchannel subchannel) {
648
    return subchannel.getAddresses().getAddresses().get(0);
1✔
649
  }
650

651
  @VisibleForTesting
652
  ConnectivityState getConcludedConnectivityState() {
653
    return this.concludedState;
1✔
654
  }
655

656
  @VisibleForTesting
657
  ConnectivityState getRawConnectivityState() {
658
    return this.rawConnectivityState;
1✔
659
  }
660

661
  /**
662
   * Picker that requests connection during the first pick, and returns noResult.
663
   */
664
  private final class RequestConnectionPicker extends SubchannelPicker {
665
    private final PickFirstLeafLoadBalancer pickFirstLeafLoadBalancer;
666
    private final AtomicBoolean connectionRequested = new AtomicBoolean(false);
1✔
667

668
    RequestConnectionPicker(PickFirstLeafLoadBalancer pickFirstLeafLoadBalancer) {
1✔
669
      this.pickFirstLeafLoadBalancer =
1✔
670
          checkNotNull(pickFirstLeafLoadBalancer, "pickFirstLeafLoadBalancer");
1✔
671
    }
1✔
672

673
    @Override
674
    public PickResult pickSubchannel(PickSubchannelArgs args) {
675
      if (connectionRequested.compareAndSet(false, true)) {
1✔
676
        helper.getSynchronizationContext().execute(pickFirstLeafLoadBalancer::requestConnection);
1✔
677
      }
678
      return PickResult.withNoResult(
1✔
679
          "connecting", "pick_first: requesting connection");
680
    }
681
  }
682

683
  /**
684
   * This contains both an ordered list of addresses and a pointer(i.e. index) to the current entry.
685
   * All updates should be done in a synchronization context.
686
   */
687
  @VisibleForTesting
688
  static final class Index {
689
    private List<UnwrappedEag> orderedAddresses;
690
    private int activeElement = 0;
1✔
691
    private boolean enableHappyEyeballs;
692

693
    Index(List<EquivalentAddressGroup> groups, boolean enableHappyEyeballs) {
1✔
694
      this.enableHappyEyeballs = enableHappyEyeballs;
1✔
695
      updateGroups(groups);
1✔
696
    }
1✔
697

698
    public boolean isValid() {
699
      return activeElement < orderedAddresses.size();
1✔
700
    }
701

702
    public boolean isAtBeginning() {
703
      return activeElement == 0;
1✔
704
    }
705

706
    /**
707
     * Move to next address in group.  If last address in group move to first address of next group.
708
     * @return false if went off end of the list, otherwise true
709
     */
710
    public boolean increment() {
711
      if (!isValid()) {
1✔
712
        return false;
1✔
713
      }
714

715
      activeElement++;
1✔
716

717
      return isValid();
1✔
718
    }
719

720
    public void reset() {
721
      activeElement = 0;
1✔
722
    }
1✔
723

724
    public SocketAddress getCurrentAddress() {
725
      if (!isValid()) {
1✔
726
        throw new IllegalStateException("Index is past the end of the address group list");
1✔
727
      }
728
      return orderedAddresses.get(activeElement).address;
1✔
729
    }
730

731
    public Attributes getCurrentEagAttributes() {
732
      if (!isValid()) {
1✔
733
        throw new IllegalStateException("Index is off the end of the address group list");
×
734
      }
735
      return orderedAddresses.get(activeElement).attributes;
1✔
736
    }
737

738
    public List<EquivalentAddressGroup> getCurrentEagAsList() {
739
      return Collections.singletonList(getCurrentEag());
1✔
740
    }
741

742
    private EquivalentAddressGroup getCurrentEag() {
743
      if (!isValid()) {
1✔
744
        throw new IllegalStateException("Index is past the end of the address group list");
×
745
      }
746
      return orderedAddresses.get(activeElement).asEag();
1✔
747
    }
748

749
    /**
750
     * Update to new groups, resetting the current index.
751
     */
752
    public void updateGroups(List<EquivalentAddressGroup> newGroups) {
753
      checkNotNull(newGroups, "newGroups");
1✔
754
      orderedAddresses = enableHappyEyeballs
1✔
755
                             ? updateGroupsHE(newGroups)
1✔
756
                             : updateGroupsNonHE(newGroups);
1✔
757
      reset();
1✔
758
    }
1✔
759

760
    /**
761
     * Returns false if the needle was not found and the current index was left unchanged.
762
     */
763
    public boolean seekTo(SocketAddress needle) {
764
      checkNotNull(needle, "needle");
1✔
765
      for (int i = 0; i < orderedAddresses.size(); i++) {
1✔
766
        if (orderedAddresses.get(i).address.equals(needle)) {
1✔
767
          this.activeElement = i;
1✔
768
          return true;
1✔
769
        }
770
      }
771
      return false;
1✔
772
    }
773

774
    public int size() {
775
      return orderedAddresses.size();
1✔
776
    }
777

778
    private List<UnwrappedEag> updateGroupsNonHE(List<EquivalentAddressGroup> newGroups) {
779
      List<UnwrappedEag> entries = new ArrayList<>();
1✔
780
      for (int g = 0; g < newGroups.size(); g++) {
1✔
781
        EquivalentAddressGroup eag = newGroups.get(g);
1✔
782
        for (int a = 0; a < eag.getAddresses().size(); a++) {
1✔
783
          SocketAddress addr = eag.getAddresses().get(a);
1✔
784
          entries.add(new UnwrappedEag(eag.getAttributes(), addr));
1✔
785
        }
786
      }
787

788
      return entries;
1✔
789
    }
790

791
    private List<UnwrappedEag> updateGroupsHE(List<EquivalentAddressGroup> newGroups) {
792
      Boolean firstIsV6 = null;
1✔
793
      List<UnwrappedEag> v4Entries = new ArrayList<>();
1✔
794
      List<UnwrappedEag> v6Entries = new ArrayList<>();
1✔
795
      for (int g = 0; g <  newGroups.size(); g++) {
1✔
796
        EquivalentAddressGroup eag = newGroups.get(g);
1✔
797
        for (int a = 0; a < eag.getAddresses().size(); a++) {
1✔
798
          SocketAddress addr = eag.getAddresses().get(a);
1✔
799
          boolean isIpV4 = addr instanceof InetSocketAddress
1✔
800
              && ((InetSocketAddress) addr).getAddress() instanceof Inet4Address;
1✔
801
          if (isIpV4) {
1✔
802
            if (firstIsV6 == null) {
1✔
803
              firstIsV6 = false;
1✔
804
            }
805
            v4Entries.add(new UnwrappedEag(eag.getAttributes(), addr));
1✔
806
          } else {
807
            if (firstIsV6 == null) {
1✔
808
              firstIsV6 = true;
1✔
809
            }
810
            v6Entries.add(new UnwrappedEag(eag.getAttributes(), addr));
1✔
811
          }
812
        }
813
      }
814

815
      return firstIsV6 != null && firstIsV6
1✔
816
          ? interleave(v6Entries, v4Entries)
1✔
817
          : interleave(v4Entries, v6Entries);
1✔
818
    }
819

820
    private List<UnwrappedEag> interleave(List<UnwrappedEag> firstFamily,
821
                                          List<UnwrappedEag> secondFamily) {
822
      if (firstFamily.isEmpty()) {
1✔
823
        return secondFamily;
1✔
824
      }
825
      if (secondFamily.isEmpty()) {
1✔
826
        return firstFamily;
1✔
827
      }
828

829
      List<UnwrappedEag> result = new ArrayList<>(firstFamily.size() + secondFamily.size());
1✔
830
      for (int i = 0; i < Math.max(firstFamily.size(), secondFamily.size()); i++) {
1✔
831
        if (i < firstFamily.size()) {
1✔
832
          result.add(firstFamily.get(i));
1✔
833
        }
834
        if (i < secondFamily.size()) {
1✔
835
          result.add(secondFamily.get(i));
1✔
836
        }
837
      }
838
      return result;
1✔
839
    }
840

841
    private static final class UnwrappedEag {
842
      private final Attributes attributes;
843
      private final SocketAddress address;
844

845
      public UnwrappedEag(Attributes attributes, SocketAddress address) {
1✔
846
        this.attributes = attributes;
1✔
847
        this.address = address;
1✔
848
      }
1✔
849

850
      private EquivalentAddressGroup asEag() {
851
        return new EquivalentAddressGroup(address, attributes);
1✔
852
      }
853
    }
854
  }
855

856
  @VisibleForTesting
857
  int getIndexLocation() {
858
    return addressIndex.activeElement;
1✔
859
  }
860

861
  @VisibleForTesting
862
  boolean isIndexValid() {
863
    return addressIndex.isValid();
1✔
864
  }
865

866
  private static final class SubchannelData {
867
    private final Subchannel subchannel;
868
    private ConnectivityState state;
869
    private boolean completedConnectivityAttempt = false;
1✔
870
    private ConnectivityStateInfo healthStateInfo = ConnectivityStateInfo.forNonError(IDLE);
1✔
871

872
    public SubchannelData(Subchannel subchannel, ConnectivityState state) {
1✔
873
      this.subchannel = subchannel;
1✔
874
      this.state = state;
1✔
875
    }
1✔
876

877
    public Subchannel getSubchannel() {
878
      return this.subchannel;
1✔
879
    }
880

881
    public ConnectivityState getState() {
882
      return this.state;
1✔
883
    }
884

885
    public boolean isCompletedConnectivityAttempt() {
886
      return completedConnectivityAttempt;
1✔
887
    }
888

889
    private void updateState(ConnectivityState newState) {
890
      this.state = newState;
1✔
891
      if (newState == READY || newState == TRANSIENT_FAILURE) {
1✔
892
        completedConnectivityAttempt = true;
1✔
893
      } else if (newState == IDLE) {
1✔
894
        completedConnectivityAttempt = false;
1✔
895
      }
896
    }
1✔
897

898
    private ConnectivityState getHealthState() {
899
      return healthStateInfo.getState();
1✔
900
    }
901
  }
902

903
  public static final class PickFirstLeafLoadBalancerConfig {
904

905
    @Nullable
906
    public final Boolean shuffleAddressList;
907

908
    // For testing purposes only, not meant to be parsed from a real config.
909
    @Nullable
910
    final Long randomSeed;
911

912
    public PickFirstLeafLoadBalancerConfig(@Nullable Boolean shuffleAddressList) {
913
      this(shuffleAddressList, null);
1✔
914
    }
1✔
915

916
    PickFirstLeafLoadBalancerConfig(@Nullable Boolean shuffleAddressList,
917
        @Nullable Long randomSeed) {
1✔
918
      this.shuffleAddressList = shuffleAddressList;
1✔
919
      this.randomSeed = randomSeed;
1✔
920
    }
1✔
921
  }
922

923
}
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