• 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.78
/../core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java
1
/*
2
 * Copyright 2015 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.SHUTDOWN;
23
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
24

25
import io.grpc.ConnectivityState;
26
import io.grpc.ConnectivityStateInfo;
27
import io.grpc.EquivalentAddressGroup;
28
import io.grpc.LoadBalancer;
29
import io.grpc.Status;
30
import java.util.List;
31
import java.util.Random;
32
import java.util.concurrent.atomic.AtomicBoolean;
33
import javax.annotation.Nullable;
34

35
/**
36
 * A {@link LoadBalancer} that provides no load-balancing over the addresses from the {@link
37
 * io.grpc.NameResolver}.  The channel's default behavior is used, which is walking down the address
38
 * list and sticking to the first that works.
39
 */
40
final class PickFirstLoadBalancer extends LoadBalancer {
41
  private static final PickResult CONNECTING_RESULT =
1✔
42
      PickResult.withNoResult("connecting", "pick_first: attempting to connect");
1✔
43
  private final Helper helper;
44
  private Subchannel subchannel;
45
  private ConnectivityState currentState = IDLE;
1✔
46

47
  PickFirstLoadBalancer(Helper helper) {
1✔
48
    this.helper = checkNotNull(helper, "helper");
1✔
49
  }
1✔
50

51
  @Override
52
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
53
    List<EquivalentAddressGroup> servers = resolvedAddresses.getAddresses();
1✔
54
    if (servers.isEmpty()) {
1✔
55
      Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
56
              "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses()
1✔
57
                      + ", attrs=" + resolvedAddresses.getAttributes());
1✔
58
      handleNameResolutionError(unavailableStatus);
1✔
59
      return unavailableStatus;
1✔
60
    }
61

62
    // We can optionally be configured to shuffle the address list. This can help better distribute
63
    // the load.
64
    if (resolvedAddresses.getLoadBalancingPolicyConfig() instanceof PickFirstLoadBalancerConfig) {
1✔
65
      PickFirstLoadBalancerConfig config
1✔
66
          = (PickFirstLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
67
      if (config.shuffleAddressList != null && config.shuffleAddressList) {
1✔
68
        servers = PickFirstLeafLoadBalancer.shuffle(
1✔
69
            servers, config.randomSeed != null ? new Random(config.randomSeed) : new Random());
1✔
70
      }
71
    }
72

73
    if (subchannel == null) {
1✔
74
      final Subchannel subchannel = helper.createSubchannel(
1✔
75
          CreateSubchannelArgs.newBuilder()
1✔
76
              .setAddresses(servers)
1✔
77
              .build());
1✔
78
      subchannel.start(new SubchannelStateListener() {
1✔
79
          @Override
80
          public void onSubchannelState(ConnectivityStateInfo stateInfo) {
81
            processSubchannelState(subchannel, stateInfo);
1✔
82
          }
1✔
83
        });
84
      this.subchannel = subchannel;
1✔
85

86
      // The channel state does not get updated when doing name resolving today, so for the moment
87
      // let LB report CONNECTION and call subchannel.requestConnection() immediately.
88
      updateBalancingState(CONNECTING, new FixedResultPicker(connectingResult()));
1✔
89
      subchannel.requestConnection();
1✔
90
    } else {
1✔
91
      subchannel.updateAddresses(servers);
1✔
92
    }
93

94
    return Status.OK;
1✔
95
  }
96

97
  @Override
98
  public void handleNameResolutionError(Status error) {
99
    if (subchannel != null) {
1✔
100
      subchannel.shutdown();
1✔
101
      subchannel = null;
1✔
102
    }
103

104
    // NB(lukaszx0) Whether we should propagate the error unconditionally is arguable. It's fine
105
    // for time being.
106
    updateBalancingState(TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
1✔
107
  }
1✔
108

109
  private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) {
110
    ConnectivityState newState = stateInfo.getState();
1✔
111
    if (newState == SHUTDOWN) {
1✔
112
      return;
1✔
113
    }
114
    if (newState == TRANSIENT_FAILURE || newState == IDLE) {
1✔
115
      helper.refreshNameResolution();
1✔
116
    }
117

118
    // If we are transitioning from a TRANSIENT_FAILURE to CONNECTING or IDLE we ignore this state
119
    // transition and still keep the LB in TRANSIENT_FAILURE state. This is referred to as "sticky
120
    // transient failure". Only a subchannel state change to READY will get the LB out of
121
    // TRANSIENT_FAILURE. If the state is IDLE we additionally request a new connection so that we
122
    // keep retrying for a connection.
123
    if (currentState == TRANSIENT_FAILURE) {
1✔
124
      if (newState == CONNECTING) {
1✔
125
        return;
1✔
126
      } else if (newState == IDLE) {
1✔
127
        requestConnection();
1✔
128
        return;
1✔
129
      }
130
    }
131

132
    SubchannelPicker picker;
133
    switch (newState) {
1✔
134
      case IDLE:
135
        picker = new RequestConnectionPicker();
1✔
136
        break;
1✔
137
      case CONNECTING:
138
        // It's safe to use RequestConnectionPicker here, so when coming from IDLE we could leave
139
        // the current picker in-place. But ignoring the potential optimization is simpler.
140
        picker = new FixedResultPicker(connectingResult());
1✔
141
        break;
1✔
142
      case READY:
143
        picker = new FixedResultPicker(PickResult.withSubchannel(subchannel));
1✔
144
        break;
1✔
145
      case TRANSIENT_FAILURE:
146
        picker = new FixedResultPicker(PickResult.withError(stateInfo.getStatus()));
1✔
147
        break;
1✔
148
      default:
149
        throw new IllegalArgumentException("Unsupported state:" + newState);
×
150
    }
151

152
    updateBalancingState(newState, picker);
1✔
153
  }
1✔
154

155
  private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) {
156
    currentState = state;
1✔
157
    helper.updateBalancingState(state, picker);
1✔
158
  }
1✔
159

160
  @Override
161
  public void shutdown() {
162
    if (subchannel != null) {
1✔
163
      subchannel.shutdown();
1✔
164
    }
165
  }
1✔
166

167
  @Override
168
  public void requestConnection() {
169
    if (subchannel != null) {
1✔
170
      subchannel.requestConnection();
1✔
171
    }
172
  }
1✔
173

174
  private PickResult connectingResult() {
175
    return CONNECTING_RESULT;
1✔
176
  }
177

178
  /** Picker that requests connection during the first pick, and returns noResult. */
179
  private final class RequestConnectionPicker extends SubchannelPicker {
1✔
180
    private final AtomicBoolean connectionRequested = new AtomicBoolean(false);
1✔
181

182
    @Override
183
    public PickResult pickSubchannel(PickSubchannelArgs args) {
184
      if (connectionRequested.compareAndSet(false, true)) {
1✔
185
        helper.getSynchronizationContext().execute(PickFirstLoadBalancer.this::requestConnection);
1✔
186
      }
187
      return PickResult.withNoResult(
1✔
188
          "connecting", "pick_first: requesting connection");
189
    }
190
  }
191

192
  public static final class PickFirstLoadBalancerConfig {
193

194
    @Nullable
195
    public final Boolean shuffleAddressList;
196

197
    // For testing purposes only, not meant to be parsed from a real config.
198
    @Nullable final Long randomSeed;
199

200
    public PickFirstLoadBalancerConfig(@Nullable Boolean shuffleAddressList) {
201
      this(shuffleAddressList, null);
1✔
202
    }
1✔
203

204
    PickFirstLoadBalancerConfig(@Nullable Boolean shuffleAddressList, @Nullable Long randomSeed) {
1✔
205
      this.shuffleAddressList = shuffleAddressList;
1✔
206
      this.randomSeed = randomSeed;
1✔
207
    }
1✔
208
  }
209
}
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