• 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

85.48
/../xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.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 io.grpc.ConnectivityState.CONNECTING;
22
import static io.grpc.ConnectivityState.IDLE;
23
import static io.grpc.ConnectivityState.READY;
24
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
25
import static io.grpc.xds.LeastRequestLoadBalancerProvider.DEFAULT_CHOICE_COUNT;
26
import static io.grpc.xds.LeastRequestLoadBalancerProvider.MAX_CHOICE_COUNT;
27
import static io.grpc.xds.LeastRequestLoadBalancerProvider.MIN_CHOICE_COUNT;
28

29
import com.google.common.annotations.VisibleForTesting;
30
import com.google.common.base.MoreObjects;
31
import io.grpc.Attributes;
32
import io.grpc.ClientStreamTracer;
33
import io.grpc.ClientStreamTracer.StreamInfo;
34
import io.grpc.ConnectivityState;
35
import io.grpc.LoadBalancer;
36
import io.grpc.LoadBalancerProvider;
37
import io.grpc.Metadata;
38
import io.grpc.Status;
39
import io.grpc.util.MultiChildLoadBalancer;
40
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
41
import java.util.ArrayList;
42
import java.util.HashSet;
43
import java.util.List;
44
import java.util.concurrent.atomic.AtomicInteger;
45

46
/**
47
 * A {@link LoadBalancer} that provides least request load balancing based on
48
 * outstanding request counters.
49
 * It works by sampling a number of subchannels and picking the one with the
50
 * fewest amount of outstanding requests.
51
 * The default sampling amount of two is also known as
52
 * the "power of two choices" (P2C).
53
 */
54
final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
55
  private final ThreadSafeRandom random;
56

57
  private SubchannelPicker currentPicker = new FixedResultPicker(
1✔
58
      PickResult.withNoResult("connecting", "least_request: initializing"));
1✔
59
  private int choiceCount = DEFAULT_CHOICE_COUNT;
1✔
60

61
  LeastRequestLoadBalancer(Helper helper) {
62
    this(helper, ThreadSafeRandomImpl.instance);
1✔
63
  }
1✔
64

65
  @VisibleForTesting
66
  LeastRequestLoadBalancer(Helper helper, ThreadSafeRandom random) {
67
    super(helper);
1✔
68
    this.random = checkNotNull(random, "random");
1✔
69
  }
1✔
70

71
  @Override
72
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
73
    // Need to update choiceCount before calling super so that the updateBalancingState call has the
74
    // new value.  However, if the update fails we need to revert it.
75
    int oldChoiceCount = choiceCount;
1✔
76
    LeastRequestConfig config =
1✔
77
        (LeastRequestConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
78
    if (config != null) {
1✔
79
      choiceCount = config.choiceCount;
1✔
80
    }
81

82
    Status addressAcceptanceStatus = super.acceptResolvedAddresses(resolvedAddresses);
1✔
83

84
    if (!addressAcceptanceStatus.isOk()) {
1✔
85
      choiceCount = oldChoiceCount;
1✔
86
    }
87

88
    return addressAcceptanceStatus;
1✔
89
  }
90

91
  /**
92
   * Updates picker with the list of active subchannels (state == READY).
93
   *
94
   *  <p>
95
   * If no active subchannels exist, but some are in TRANSIENT_FAILURE then returns a picker
96
   * with all of the children in TF so that the application code will get an error from a varying
97
   * random one when it tries to get a subchannel.
98
   * </p>
99
   */
100
  @SuppressWarnings("ReferenceEquality")
101
  @Override
102
  protected void updateOverallBalancingState() {
103
    List<ChildLbState> activeList = getReadyChildren();
1✔
104
    if (activeList.isEmpty()) {
1✔
105
      // No READY subchannels, determine aggregate state and error status
106
      boolean isConnecting = false;
1✔
107
      List<ChildLbState> childrenInTf = new ArrayList<>();
1✔
108
      for (ChildLbState childLbState : getChildLbStates()) {
1✔
109
        ConnectivityState state = childLbState.getCurrentState();
1✔
110
        if (state == CONNECTING || state == IDLE) {
1✔
111
          isConnecting = true;
1✔
112
        } else if (state == TRANSIENT_FAILURE) {
1✔
113
          childrenInTf.add(childLbState);
1✔
114
        }
115
      }
1✔
116
      if (isConnecting) {
1✔
117
        updateBalancingState(
1✔
118
            CONNECTING,
119
            new FixedResultPicker(
120
                PickResult.withNoResult("connecting", "least_request: connecting")));
1✔
121
      } else {
122
        // Give it all the failing children and let it randomly pick among them
123
        updateBalancingState(TRANSIENT_FAILURE,
1✔
124
            new ReadyPicker(childrenInTf, choiceCount, random));
125
      }
126
    } else {
1✔
127
      updateBalancingState(READY, new ReadyPicker(activeList, choiceCount, random));
1✔
128
    }
129
  }
1✔
130

131
  @Override
132
  protected ChildLbState createChildLbState(Object key) {
133
    return new LeastRequestLbState(key, pickFirstLbProvider);
1✔
134
  }
135

136
  private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) {
137
    if (state != currentConnectivityState || !picker.equals(currentPicker)) {
1✔
138
      getHelper().updateBalancingState(state, picker);
1✔
139
      currentConnectivityState = state;
1✔
140
      currentPicker = picker;
1✔
141
    }
142
  }
1✔
143

144
  /**
145
   * This should ONLY be used by tests.
146
   */
147
  @VisibleForTesting
148
  void setResolvingAddresses(boolean newValue) {
149
    super.resolvingAddresses = newValue;
1✔
150
  }
1✔
151

152
  // Expose for tests in this package.
153
  private static AtomicInteger getInFlights(ChildLbState childLbState) {
154
    return ((LeastRequestLbState)childLbState).activeRequests;
1✔
155
  }
156

157
  @VisibleForTesting
158
  static final class ReadyPicker extends SubchannelPicker {
159
    private final List<SubchannelPicker> childPickers; // non-empty
160
    private final List<AtomicInteger> childInFlights; // 1:1 with childPickers
161
    private final int choiceCount;
162
    private final ThreadSafeRandom random;
163
    private final int hashCode;
164

165
    ReadyPicker(List<ChildLbState> childLbStates, int choiceCount, ThreadSafeRandom random) {
1✔
166
      checkArgument(!childLbStates.isEmpty(), "empty list");
1✔
167
      this.childPickers = new ArrayList<>(childLbStates.size());
1✔
168
      this.childInFlights = new ArrayList<>(childLbStates.size());
1✔
169
      for (ChildLbState state : childLbStates) {
1✔
170
        childPickers.add(state.getCurrentPicker());
1✔
171
        childInFlights.add(getInFlights(state));
1✔
172
      }
1✔
173
      this.choiceCount = choiceCount;
1✔
174
      this.random = checkNotNull(random, "random");
1✔
175

176
      int sum = 0;
1✔
177
      for (SubchannelPicker child : childPickers) {
1✔
178
        sum += child.hashCode();
1✔
179
      }
1✔
180
      this.hashCode = sum ^ choiceCount;
1✔
181
    }
1✔
182

183
    @Override
184
    public PickResult pickSubchannel(PickSubchannelArgs args) {
185
      int child = nextChildToUse();
1✔
186
      PickResult childResult = childPickers.get(child).pickSubchannel(args);
1✔
187

188
      if (!childResult.getStatus().isOk() || childResult.getSubchannel() == null) {
1✔
189
        return childResult;
×
190
      }
191

192
      if (childResult.getStreamTracerFactory() != null) {
1✔
193
        // Already wrapped, so just use the current picker for selected child
194
        return childResult;
×
195
      } else {
196
        // Wrap the subchannel
197
        OutstandingRequestsTracingFactory factory =
1✔
198
            new OutstandingRequestsTracingFactory(childInFlights.get(child));
1✔
199
        return PickResult.withSubchannel(childResult.getSubchannel(), factory);
1✔
200
      }
201
    }
202

203
    @Override
204
    public String toString() {
205
      return MoreObjects.toStringHelper(ReadyPicker.class)
×
206
                        .add("list", childPickers)
×
207
                        .add("choiceCount", choiceCount)
×
208
                        .toString();
×
209
    }
210

211
    private int nextChildToUse() {
212
      int candidate = random.nextInt(childPickers.size());
1✔
213
      for (int i = 0; i < choiceCount - 1; ++i) {
1✔
214
        int sampled = random.nextInt(childPickers.size());
1✔
215
        if (childInFlights.get(sampled).get() < childInFlights.get(candidate).get()) {
1✔
216
          candidate = sampled;
1✔
217
        }
218
      }
219
      return candidate;
1✔
220
    }
221

222
    @VisibleForTesting
223
    List<SubchannelPicker> getChildPickers() {
224
      return childPickers;
1✔
225
    }
226

227
    @Override
228
    public int hashCode() {
229
      return hashCode;
×
230
    }
231

232
    @Override
233
    public boolean equals(Object o) {
234
      if (!(o instanceof ReadyPicker)) {
1✔
235
        return false;
1✔
236
      }
237
      ReadyPicker other = (ReadyPicker) o;
1✔
238
      if (other == this) {
1✔
239
        return true;
×
240
      }
241
      // the lists cannot contain duplicate children
242
      return hashCode == other.hashCode
1✔
243
          && choiceCount == other.choiceCount
244
          && childPickers.size() == other.childPickers.size()
1✔
245
          && new HashSet<>(childPickers).containsAll(other.childPickers);
1✔
246
    }
247
  }
248

249
  @VisibleForTesting
250
  static final class EmptyPicker extends SubchannelPicker {
×
251
    @Override
252
    public PickResult pickSubchannel(PickSubchannelArgs args) {
253
      return PickResult.withNoResult(
×
254
          "connecting", "least_request: waiting for subchannel");
255
    }
256

257
    @Override
258
    public int hashCode() {
259
      return getClass().hashCode();
×
260
    }
261

262
    @Override
263
    public boolean equals(Object o) {
264
      return o instanceof EmptyPicker;
×
265
    }
266

267
    @Override
268
    public String toString() {
269
      return MoreObjects.toStringHelper(EmptyPicker.class).toString();
×
270
    }
271
  }
272

273
  private static final class OutstandingRequestsTracingFactory extends
274
      ClientStreamTracer.Factory {
275
    private final AtomicInteger inFlights;
276

277
    private OutstandingRequestsTracingFactory(AtomicInteger inFlights) {
1✔
278
      this.inFlights = checkNotNull(inFlights, "inFlights");
1✔
279
    }
1✔
280

281
    @Override
282
    public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) {
283
      return new ClientStreamTracer() {
1✔
284
        @Override
285
        public void streamCreated(Attributes transportAttrs, Metadata headers) {
286
          inFlights.incrementAndGet();
1✔
287
        }
1✔
288

289
        @Override
290
        public void streamClosed(Status status) {
291
          inFlights.decrementAndGet();
1✔
292
        }
1✔
293
      };
294
    }
295
  }
296

297
  static final class LeastRequestConfig {
298
    final int choiceCount;
299

300
    LeastRequestConfig(int choiceCount) {
1✔
301
      checkArgument(choiceCount >= MIN_CHOICE_COUNT, "choiceCount <= 1");
1✔
302
      // Even though a choiceCount value larger than 2 is currently considered valid in xDS
303
      // we restrict it to 10 here as specified in "A48: xDS Least Request LB Policy".
304
      this.choiceCount = Math.min(choiceCount, MAX_CHOICE_COUNT);
1✔
305
    }
1✔
306

307
    @Override
308
    public boolean equals(Object o) {
309
      if (!(o instanceof LeastRequestConfig)) {
1✔
310
        return false;
×
311
      }
312
      LeastRequestConfig that = (LeastRequestConfig) o;
1✔
313
      return this.choiceCount == that.choiceCount;
1✔
314
    }
315

316
    @Override
317
    public int hashCode() {
318
      return choiceCount;
×
319
    }
320

321
    @Override
322
    public String toString() {
323
      return MoreObjects.toStringHelper(this)
×
324
          .add("choiceCount", choiceCount)
×
325
          .toString();
×
326
    }
327
  }
328

329
  protected class LeastRequestLbState extends ChildLbState {
330
    private final AtomicInteger activeRequests = new AtomicInteger(0);
1✔
331

332
    public LeastRequestLbState(Object key, LoadBalancerProvider policyProvider) {
1✔
333
      super(key, policyProvider);
1✔
334
    }
1✔
335

336
    int getActiveRequests() {
337
      return activeRequests.get();
1✔
338
    }
339

340
    @Override
341
    protected ChildLbStateHelper createChildHelper() {
342
      return new ChildLbStateHelper() {
1✔
343
        @Override
344
        public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
345
          super.updateBalancingState(newState, newPicker);
1✔
346
          if (!resolvingAddresses && newState == IDLE) {
1✔
347
            getLb().requestConnection();
1✔
348
          }
349
        }
1✔
350
      };
351
    }
352
  }
353
}
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