• 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

89.49
/../grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java
1
/*
2
 * Copyright 2017 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.grpclb;
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.MoreObjects;
30
import com.google.common.base.Objects;
31
import com.google.common.base.Stopwatch;
32
import com.google.protobuf.util.Durations;
33
import io.grpc.Attributes;
34
import io.grpc.ChannelLogger;
35
import io.grpc.ChannelLogger.ChannelLogLevel;
36
import io.grpc.ConnectivityState;
37
import io.grpc.ConnectivityStateInfo;
38
import io.grpc.Context;
39
import io.grpc.EquivalentAddressGroup;
40
import io.grpc.LoadBalancer;
41
import io.grpc.LoadBalancer.FixedResultPicker;
42
import io.grpc.LoadBalancer.Helper;
43
import io.grpc.LoadBalancer.PickResult;
44
import io.grpc.LoadBalancer.PickSubchannelArgs;
45
import io.grpc.LoadBalancer.ResolvedAddresses;
46
import io.grpc.LoadBalancer.Subchannel;
47
import io.grpc.LoadBalancer.SubchannelPicker;
48
import io.grpc.LoadBalancerProvider;
49
import io.grpc.LoadBalancerRegistry;
50
import io.grpc.ManagedChannel;
51
import io.grpc.Metadata;
52
import io.grpc.Status;
53
import io.grpc.SynchronizationContext;
54
import io.grpc.SynchronizationContext.ScheduledHandle;
55
import io.grpc.grpclb.SubchannelPool.PooledSubchannelStateListener;
56
import io.grpc.internal.BackoffPolicy;
57
import io.grpc.internal.TimeProvider;
58
import io.grpc.lb.v1.ClientStats;
59
import io.grpc.lb.v1.InitialLoadBalanceRequest;
60
import io.grpc.lb.v1.InitialLoadBalanceResponse;
61
import io.grpc.lb.v1.LoadBalanceRequest;
62
import io.grpc.lb.v1.LoadBalanceResponse;
63
import io.grpc.lb.v1.LoadBalanceResponse.LoadBalanceResponseTypeCase;
64
import io.grpc.lb.v1.LoadBalancerGrpc;
65
import io.grpc.lb.v1.Server;
66
import io.grpc.lb.v1.ServerList;
67
import io.grpc.stub.StreamObserver;
68
import io.grpc.util.ForwardingLoadBalancerHelper;
69
import java.net.InetAddress;
70
import java.net.InetSocketAddress;
71
import java.net.UnknownHostException;
72
import java.util.ArrayList;
73
import java.util.Arrays;
74
import java.util.Collections;
75
import java.util.HashMap;
76
import java.util.List;
77
import java.util.Map;
78
import java.util.concurrent.ScheduledExecutorService;
79
import java.util.concurrent.TimeUnit;
80
import java.util.concurrent.atomic.AtomicBoolean;
81
import java.util.concurrent.atomic.AtomicReference;
82
import javax.annotation.Nullable;
83
import javax.annotation.concurrent.NotThreadSafe;
84

85
/**
86
 * The states of a GRPCLB working session of {@link GrpclbLoadBalancer}.  Created when
87
 * GrpclbLoadBalancer switches to GRPCLB mode.  Closed and discarded when GrpclbLoadBalancer
88
 * switches away from GRPCLB mode.
89
 */
90
@NotThreadSafe
91
final class GrpclbState {
92
  static final long FALLBACK_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
1✔
93
  private static final Attributes LB_PROVIDED_BACKEND_ATTRS =
94
      Attributes.newBuilder().set(GrpclbConstants.ATTR_LB_PROVIDED_BACKEND, true).build();
1✔
95

96
  // Temporary workaround to reduce log spam for a grpclb server that incessantly sends updates
97
  // Tracked by b/198440401
98
  static final boolean SHOULD_LOG_SERVER_LISTS =
1✔
99
      Boolean.parseBoolean(System.getProperty("io.grpc.grpclb.LogServerLists", "true"));
1✔
100

101
  @VisibleForTesting
102
  static final PickResult DROP_PICK_RESULT =
1✔
103
      PickResult.withDrop(Status.UNAVAILABLE.withDescription("Dropped as requested by balancer"));
1✔
104
  @VisibleForTesting
105
  static final Status NO_AVAILABLE_BACKENDS_STATUS =
1✔
106
      Status.UNAVAILABLE.withDescription("LoadBalancer responded without any backends");
1✔
107
  @VisibleForTesting
108
  static final Status BALANCER_TIMEOUT_STATUS =
1✔
109
      Status.UNAVAILABLE.withDescription("Timeout waiting for remote balancer");
1✔
110
  @VisibleForTesting
111
  static final Status BALANCER_REQUESTED_FALLBACK_STATUS =
1✔
112
      Status.UNAVAILABLE.withDescription("Fallback requested by balancer");
1✔
113
  @VisibleForTesting
114
  static final Status NO_FALLBACK_BACKENDS_STATUS =
1✔
115
      Status.UNAVAILABLE.withDescription("Unable to fallback, no fallback addresses found");
1✔
116
  // This error status should never be propagated to RPC failures, as "no backend or balancer
117
  // addresses found" should be directly handled as a name resolution error. So in cases of no
118
  // balancer address, fallback should never fail.
119
  private static final Status NO_LB_ADDRESS_PROVIDED_STATUS =
1✔
120
      Status.UNAVAILABLE.withDescription("No balancer address found");
1✔
121

122

123
  @VisibleForTesting
124
  static final RoundRobinEntry BUFFER_ENTRY = new RoundRobinEntry() {
1✔
125
      @Override
126
      public PickResult picked(PickSubchannelArgs args) {
127
        return PickResult.withNoResult(
×
128
            "connecting", "grpclb: waiting for backend server list");
129
      }
130

131
      @Override
132
      public String toString() {
133
        return "BUFFER_ENTRY";
×
134
      }
135
    };
136
  @VisibleForTesting
137
  static final String NO_USE_AUTHORITY_SUFFIX = "-notIntendedToBeUsed";
138

139
  enum Mode {
1✔
140
    ROUND_ROBIN,
1✔
141
    PICK_FIRST,
1✔
142
  }
143

144
  private final String serviceName;
145
  private final long fallbackTimeoutMs;
146
  private final Helper helper;
147
  private final Context context;
148
  private final SynchronizationContext syncContext;
149
  @Nullable
150
  private final SubchannelPool subchannelPool;
151
  private final TimeProvider time;
152
  private final Stopwatch stopwatch;
153
  private final ScheduledExecutorService timerService;
154

155
  private static final Attributes.Key<AtomicReference<ConnectivityStateInfo>> STATE_INFO =
1✔
156
      Attributes.Key.create("io.grpc.grpclb.GrpclbLoadBalancer.stateInfo");
1✔
157
  private final BackoffPolicy.Provider backoffPolicyProvider;
158
  private final ChannelLogger logger;
159

160
  // Scheduled only once.  Never reset.
161
  @Nullable
162
  private ScheduledHandle fallbackTimer;
163
  private List<EquivalentAddressGroup> fallbackBackendList = Collections.emptyList();
1✔
164
  private boolean usingFallbackBackends;
165
  // Reason to fallback, will be used as RPC's error message if fail to fallback (e.g., no
166
  // fallback addresses found).
167
  @Nullable
168
  private Status fallbackReason;
169
  // True if the current balancer has returned a serverlist.  Will be reset to false when lost
170
  // connection to a balancer.
171
  private boolean balancerWorking;
172
  @Nullable
173
  private BackoffPolicy lbRpcRetryPolicy;
174
  @Nullable
175
  private ScheduledHandle lbRpcRetryTimer;
176

177
  @Nullable
178
  private ManagedChannel lbCommChannel;
179

180
  @Nullable
181
  private LbStream lbStream;
182
  private Map<List<EquivalentAddressGroup>, Subchannel> subchannels = Collections.emptyMap();
1✔
183
  private final GrpclbConfig config;
184

185
  // Has the same size as the round-robin list from the balancer.
186
  // A drop entry from the round-robin list becomes a DropEntry here.
187
  // A backend entry from the robin-robin list becomes a null here.
188
  private List<DropEntry> dropList = Collections.emptyList();
1✔
189
  // Contains only non-drop, i.e., backends from the round-robin list from the balancer.
190
  private List<BackendEntry> backendList = Collections.emptyList();
1✔
191
  private ConnectivityState currentState = ConnectivityState.CONNECTING;
1✔
192
  private RoundRobinPicker currentPicker =
1✔
193
      new RoundRobinPicker(Collections.<DropEntry>emptyList(), Arrays.asList(BUFFER_ENTRY));
1✔
194
  private boolean requestConnectionPending;
195

196
  // Child LoadBalancer and state for PICK_FIRST mode delegation.
197
  private final LoadBalancerProvider pickFirstLbProvider;
198
  @Nullable
199
  private LoadBalancer pickFirstLb;
200
  private ConnectivityState pickFirstLbState = CONNECTING;
1✔
201
  private SubchannelPicker pickFirstLbPicker = new FixedResultPicker(PickResult.withNoResult());
1✔
202
  @Nullable
203
  private GrpclbClientLoadRecorder currentPickFirstLoadRecorder;
204

205
  GrpclbState(
206
      GrpclbConfig config,
207
      Helper helper,
208
      Context context,
209
      SubchannelPool subchannelPool,
210
      TimeProvider time,
211
      Stopwatch stopwatch,
212
      BackoffPolicy.Provider backoffPolicyProvider) {
1✔
213
    this.config = checkNotNull(config, "config");
1✔
214
    this.helper = checkNotNull(helper, "helper");
1✔
215
    this.context = checkNotNull(context, "context");
1✔
216
    this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext");
1✔
217
    if (config.getMode() == Mode.ROUND_ROBIN) {
1✔
218
      this.subchannelPool = checkNotNull(subchannelPool, "subchannelPool");
1✔
219
      subchannelPool.registerListener(
1✔
220
          new PooledSubchannelStateListener() {
1✔
221
            @Override
222
            public void onSubchannelState(
223
                Subchannel subchannel, ConnectivityStateInfo newState) {
224
              handleSubchannelState(subchannel, newState);
1✔
225
            }
1✔
226
          });
227
    } else {
228
      this.subchannelPool = null;
1✔
229
    }
230
    this.pickFirstLbProvider = checkNotNull(
1✔
231
        LoadBalancerRegistry.getDefaultRegistry().getProvider("pick_first"),
1✔
232
        "pick_first balancer not available");
233
    this.time = checkNotNull(time, "time provider");
1✔
234
    this.stopwatch = checkNotNull(stopwatch, "stopwatch");
1✔
235
    this.timerService = checkNotNull(helper.getScheduledExecutorService(), "timerService");
1✔
236
    this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider");
1✔
237
    if (config.getServiceName() != null) {
1✔
238
      this.serviceName = config.getServiceName();
1✔
239
    } else {
240
      this.serviceName = checkNotNull(helper.getAuthority(), "helper returns null authority");
1✔
241
    }
242
    this.fallbackTimeoutMs = config.getFallbackTimeoutMs();
1✔
243
    this.logger = checkNotNull(helper.getChannelLogger(), "logger");
1✔
244
    logger.log(ChannelLogLevel.INFO, "[grpclb-<{0}>] Created", serviceName);
1✔
245
  }
1✔
246

247
  void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo newState) {
248
    if (newState.getState() == SHUTDOWN || !subchannels.containsValue(subchannel)) {
1✔
249
      return;
1✔
250
    }
251
    if (config.getMode() == Mode.ROUND_ROBIN && newState.getState() == IDLE) {
1✔
252
      subchannel.requestConnection();
1✔
253
    }
254
    if (newState.getState() == TRANSIENT_FAILURE || newState.getState() == IDLE) {
1✔
255
      helper.refreshNameResolution();
1✔
256
    }
257

258
    AtomicReference<ConnectivityStateInfo> stateInfoRef =
1✔
259
        subchannel.getAttributes().get(STATE_INFO);
1✔
260
    // If all RR servers are unhealthy, it's possible that at least one connection is CONNECTING at
261
    // every moment which causes RR to stay in CONNECTING. It's better to keep the TRANSIENT_FAILURE
262
    // state in that case so that fail-fast RPCs can fail fast.
263
    boolean keepState =
1✔
264
        config.getMode() == Mode.ROUND_ROBIN
1✔
265
        && stateInfoRef.get().getState() == TRANSIENT_FAILURE
1✔
266
        && (newState.getState() == CONNECTING || newState.getState() == IDLE);
1✔
267
    if (!keepState) {
1✔
268
      stateInfoRef.set(newState);
1✔
269
      maybeUseFallbackBackends();
1✔
270
      maybeUpdatePicker();
1✔
271
    }
272
  }
1✔
273

274
  /**
275
   * Handle new addresses of the balancer and backends from the resolver, and create connection if
276
   * not yet connected.
277
   */
278
  void handleAddresses(
279
      List<EquivalentAddressGroup> newLbAddressGroups,
280
      List<EquivalentAddressGroup> newBackendServers) {
281
    logger.log(
1✔
282
        ChannelLogLevel.DEBUG,
283
        "[grpclb-<{0}>] Resolved addresses: lb addresses {1}, backends: {2}",
284
        serviceName,
285
        newLbAddressGroups,
286
        newBackendServers);
287
    fallbackBackendList = newBackendServers;
1✔
288
    if (newLbAddressGroups.isEmpty()) {
1✔
289
      // No balancer address: close existing balancer connection and prepare to enter fallback
290
      // mode. If there is no successful backend connection, it enters fallback mode immediately.
291
      // Otherwise, fallback does not happen until backend connections are lost. This behavior
292
      // might be different from other languages (e.g., existing balancer connection is not
293
      // closed in C-core), but we aren't changing it at this time.
294
      shutdownLbComm();
1✔
295
      if (!usingFallbackBackends) {
1✔
296
        fallbackReason = NO_LB_ADDRESS_PROVIDED_STATUS;
1✔
297
        cancelFallbackTimer();
1✔
298
        maybeUseFallbackBackends();
1✔
299
      }
300
    } else {
301
      startLbComm(newLbAddressGroups);
1✔
302
      // Avoid creating a new RPC just because the addresses were updated, as it can cause a
303
      // stampeding herd. The current RPC may be on a connection to an address not present in
304
      // newLbAddressGroups, but we're considering that "okay". If we detected the RPC is to an
305
      // outdated backend, we could choose to re-create the RPC.
306
      if (lbStream == null) {
1✔
307
        cancelLbRpcRetryTimer();
1✔
308
        startLbRpc();
1✔
309
      }
310
      // Start the fallback timer if it's never started and we are not already using fallback
311
      // backends.
312
      if (fallbackTimer == null && !usingFallbackBackends) {
1✔
313
        fallbackTimer =
1✔
314
            syncContext.schedule(
1✔
315
                new FallbackModeTask(BALANCER_TIMEOUT_STATUS),
316
                fallbackTimeoutMs,
317
                TimeUnit.MILLISECONDS,
318
                timerService);
319
      }
320
    }
321
    if (usingFallbackBackends) {
1✔
322
      // Populate the new fallback backends to round-robin list.
323
      useFallbackBackends();
1✔
324
    }
325
    maybeUpdatePicker();
1✔
326
  }
1✔
327

328
  void requestConnection() {
329
    requestConnectionPending = true;
1✔
330
    // For PICK_FIRST mode with delegation, forward to the child LB.
331
    if (config.getMode() == Mode.PICK_FIRST && pickFirstLb != null) {
1✔
332
      pickFirstLb.requestConnection();
1✔
333
      requestConnectionPending = false;
1✔
334
      return;
1✔
335
    }
336
    for (RoundRobinEntry entry : currentPicker.pickList) {
×
337
      if (entry instanceof IdleSubchannelEntry) {
×
338
        ((IdleSubchannelEntry) entry).subchannel.requestConnection();
×
339
        requestConnectionPending = false;
×
340
      }
341
    }
×
342
  }
×
343

344
  private void maybeUseFallbackBackends() {
345
    if (balancerWorking || usingFallbackBackends) {
1✔
346
      return;
1✔
347
    }
348
    // Balancer RPC should have either been broken or timed out.
349
    checkState(fallbackReason != null, "no reason to fallback");
1✔
350
    // For PICK_FIRST mode with delegation, check the child LB's state.
351
    if (config.getMode() == Mode.PICK_FIRST) {
1✔
352
      if (pickFirstLb != null && pickFirstLbState == READY) {
1✔
353
        return;
×
354
      }
355
      // For PICK_FIRST, we don't have individual subchannel states to use as fallback reason.
356
    } else {
357
      for (Subchannel subchannel : subchannels.values()) {
1✔
358
        ConnectivityStateInfo stateInfo = subchannel.getAttributes().get(STATE_INFO).get();
1✔
359
        if (stateInfo.getState() == READY) {
1✔
360
          return;
1✔
361
        }
362
        // If we do have balancer-provided backends, use one of its error in the error message if
363
        // fail to fallback.
364
        if (stateInfo.getState() == TRANSIENT_FAILURE) {
1✔
365
          fallbackReason = stateInfo.getStatus();
1✔
366
        }
367
      }
1✔
368
    }
369
    // Fallback conditions met
370
    useFallbackBackends();
1✔
371
  }
1✔
372

373
  /**
374
   * Populate backend servers to be used from the fallback backends.
375
   */
376
  private void useFallbackBackends() {
377
    usingFallbackBackends = true;
1✔
378
    logger.log(ChannelLogLevel.INFO, "[grpclb-<{0}>] Using fallback backends", serviceName);
1✔
379

380
    List<DropEntry> newDropList = new ArrayList<>();
1✔
381
    List<BackendAddressGroup> newBackendAddrList = new ArrayList<>();
1✔
382
    for (EquivalentAddressGroup eag : fallbackBackendList) {
1✔
383
      newDropList.add(null);
1✔
384
      newBackendAddrList.add(new BackendAddressGroup(eag, null));
1✔
385
    }
1✔
386
    updateServerList(newDropList, newBackendAddrList, null);
1✔
387
  }
1✔
388

389
  private void shutdownLbComm() {
390
    shutdownLbRpc();
1✔
391
    if (lbCommChannel != null) {
1✔
392
      // The channel should have no RPCs at this point
393
      lbCommChannel.shutdownNow();
1✔
394
      lbCommChannel = null;
1✔
395
    }
396
  }
1✔
397

398
  private void shutdownLbRpc() {
399
    if (lbStream != null) {
1✔
400
      lbStream.close(Status.CANCELLED.withDescription("balancer shutdown").asException());
1✔
401
      // lbStream will be set to null in LbStream.cleanup()
402
    }
403
  }
1✔
404

405
  private void startLbComm(List<EquivalentAddressGroup> overrideAuthorityEags) {
406
    checkNotNull(overrideAuthorityEags, "overrideAuthorityEags");
1✔
407
    assert !overrideAuthorityEags.isEmpty();
1✔
408
    String doNotUseAuthority = overrideAuthorityEags.get(0).getAttributes()
1✔
409
        .get(EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE) + NO_USE_AUTHORITY_SUFFIX;
1✔
410
    if (lbCommChannel == null) {
1✔
411
      lbCommChannel = helper.createOobChannel(overrideAuthorityEags, doNotUseAuthority);
1✔
412
      logger.log(
1✔
413
          ChannelLogLevel.DEBUG,
414
          "[grpclb-<{0}>] Created grpclb channel: EAG={1}",
415
          serviceName,
416
          overrideAuthorityEags);
417
    } else {
418
      helper.updateOobChannelAddresses(lbCommChannel, overrideAuthorityEags);
1✔
419
    }
420
  }
1✔
421

422
  private void startLbRpc() {
423
    checkState(lbStream == null, "previous lbStream has not been cleared yet");
1✔
424
    LoadBalancerGrpc.LoadBalancerStub stub = LoadBalancerGrpc.newStub(lbCommChannel);
1✔
425
    lbStream = new LbStream(stub);
1✔
426
    Context prevContext = context.attach();
1✔
427
    try {
428
      lbStream.start();
1✔
429
    } finally {
430
      context.detach(prevContext);
1✔
431
    }
432
    stopwatch.reset().start();
1✔
433

434
    LoadBalanceRequest initRequest = LoadBalanceRequest.newBuilder()
1✔
435
        .setInitialRequest(InitialLoadBalanceRequest.newBuilder()
1✔
436
            .setName(serviceName).build())
1✔
437
        .build();
1✔
438
    logger.log(
1✔
439
        ChannelLogLevel.DEBUG,
440
        "[grpclb-<{0}>] Sent initial grpclb request {1}", serviceName, initRequest);
441
    try {
442
      lbStream.lbRequestWriter.onNext(initRequest);
1✔
443
    } catch (Exception e) {
×
444
      lbStream.close(e);
×
445
    }
1✔
446
  }
1✔
447

448
  private void cancelFallbackTimer() {
449
    if (fallbackTimer != null) {
1✔
450
      fallbackTimer.cancel();
1✔
451
    }
452
  }
1✔
453

454
  private void cancelLbRpcRetryTimer() {
455
    if (lbRpcRetryTimer != null) {
1✔
456
      lbRpcRetryTimer.cancel();
1✔
457
      lbRpcRetryTimer = null;
1✔
458
    }
459
  }
1✔
460

461
  void shutdown() {
462
    logger.log(ChannelLogLevel.INFO, "[grpclb-<{0}>] Shutdown", serviceName);
1✔
463
    shutdownLbComm();
1✔
464
    switch (config.getMode()) {
1✔
465
      case ROUND_ROBIN:
466
        // We close the subchannels through subchannelPool instead of helper just for convenience of
467
        // testing.
468
        for (Subchannel subchannel : subchannels.values()) {
1✔
469
          returnSubchannelToPool(subchannel);
1✔
470
        }
1✔
471
        subchannelPool.clear();
1✔
472
        break;
1✔
473
      case PICK_FIRST:
474
        // Shutdown the child pick_first LB which manages its own subchannels.
475
        if (pickFirstLb != null) {
1✔
476
          pickFirstLb.shutdown();
1✔
477
          pickFirstLb = null;
1✔
478
        }
479
        break;
480
      default:
481
        throw new AssertionError("Missing case for " + config.getMode());
×
482
    }
483
    subchannels = Collections.emptyMap();
1✔
484
    cancelFallbackTimer();
1✔
485
    cancelLbRpcRetryTimer();
1✔
486
  }
1✔
487

488
  void propagateError(Status status) {
489
    logger.log(ChannelLogLevel.DEBUG, "[grpclb-<{0}>] Error: {1}", serviceName, status);
1✔
490
    if (backendList.isEmpty()) {
1✔
491
      Status error =
1✔
492
          Status.UNAVAILABLE.withCause(status.getCause()).withDescription(status.getDescription());
1✔
493
      maybeUpdatePicker(
1✔
494
          TRANSIENT_FAILURE, new RoundRobinPicker(dropList, Arrays.asList(new ErrorEntry(error))));
1✔
495
    }
496
  }
1✔
497

498
  private void returnSubchannelToPool(Subchannel subchannel) {
499
    subchannelPool.returnSubchannel(subchannel, subchannel.getAttributes().get(STATE_INFO).get());
1✔
500
  }
1✔
501

502
  @VisibleForTesting
503
  @Nullable
504
  GrpclbClientLoadRecorder getLoadRecorder() {
505
    if (lbStream == null) {
1✔
506
      return null;
×
507
    }
508
    return lbStream.loadRecorder;
1✔
509
  }
510

511
  /**
512
   * Populate backend servers to be used based on the given list of addresses.
513
   */
514
  private void updateServerList(
515
      List<DropEntry> newDropList, List<BackendAddressGroup> newBackendAddrList,
516
      @Nullable GrpclbClientLoadRecorder loadRecorder) {
517
    HashMap<List<EquivalentAddressGroup>, Subchannel> newSubchannelMap =
1✔
518
        new HashMap<>();
519
    List<BackendEntry> newBackendList = new ArrayList<>();
1✔
520

521
    switch (config.getMode()) {
1✔
522
      case ROUND_ROBIN:
523
        for (BackendAddressGroup backendAddr : newBackendAddrList) {
1✔
524
          EquivalentAddressGroup eag = backendAddr.getAddresses();
1✔
525
          List<EquivalentAddressGroup> eagAsList = Collections.singletonList(eag);
1✔
526
          Subchannel subchannel = newSubchannelMap.get(eagAsList);
1✔
527
          if (subchannel == null) {
1✔
528
            subchannel = subchannels.get(eagAsList);
1✔
529
            if (subchannel == null) {
1✔
530
              subchannel = subchannelPool.takeOrCreateSubchannel(eag, createSubchannelAttrs());
1✔
531
              subchannel.requestConnection();
1✔
532
            }
533
            newSubchannelMap.put(eagAsList, subchannel);
1✔
534
          }
535
          BackendEntry entry;
536
          // Only picks with tokens are reported to LoadRecorder
537
          if (backendAddr.getToken() == null) {
1✔
538
            entry = new BackendEntry(subchannel);
1✔
539
          } else {
540
            entry = new BackendEntry(subchannel, loadRecorder, backendAddr.getToken());
1✔
541
          }
542
          newBackendList.add(entry);
1✔
543
        }
1✔
544
        // Close Subchannels whose addresses have been delisted
545
        for (Map.Entry<List<EquivalentAddressGroup>, Subchannel> entry : subchannels.entrySet()) {
1✔
546
          List<EquivalentAddressGroup> eagList = entry.getKey();
1✔
547
          if (!newSubchannelMap.containsKey(eagList)) {
1✔
548
            returnSubchannelToPool(entry.getValue());
1✔
549
          }
550
        }
1✔
551
        subchannels = Collections.unmodifiableMap(newSubchannelMap);
1✔
552
        break;
1✔
553
      case PICK_FIRST:
554
        // Delegate to child pick_first LB for address management.
555
        // Shutdown existing child LB if addresses become empty.
556
        if (newBackendAddrList.isEmpty()) {
1✔
557
          if (pickFirstLb != null) {
1✔
558
            pickFirstLb.shutdown();
1✔
559
            pickFirstLb = null;
1✔
560
          }
561
          break;
562
        }
563
        List<EquivalentAddressGroup> eagList = new ArrayList<>();
1✔
564
        // Attach tokens to EAG attributes for TokenAttachingTracerFactory to retrieve.
565
        for (BackendAddressGroup bag : newBackendAddrList) {
1✔
566
          EquivalentAddressGroup origEag = bag.getAddresses();
1✔
567
          Attributes eagAttrs = origEag.getAttributes();
1✔
568
          if (bag.getToken() != null) {
1✔
569
            eagAttrs = eagAttrs.toBuilder()
1✔
570
                .set(GrpclbConstants.TOKEN_ATTRIBUTE_KEY, bag.getToken()).build();
1✔
571
          }
572
          eagList.add(new EquivalentAddressGroup(origEag.getAddresses(), eagAttrs));
1✔
573
        }
1✔
574

575
        if (pickFirstLb == null) {
1✔
576
          pickFirstLb = pickFirstLbProvider.newLoadBalancer(new PickFirstLbHelper());
1✔
577
        }
578

579
        // Pass addresses to child LB.
580
        pickFirstLb.acceptResolvedAddresses(
1✔
581
            ResolvedAddresses.newBuilder()
1✔
582
                .setAddresses(eagList)
1✔
583
                .build());
1✔
584
        if (requestConnectionPending) {
1✔
585
          pickFirstLb.requestConnection();
×
586
          requestConnectionPending = false;
×
587
        }
588
        // Store the load recorder for token attachment.
589
        currentPickFirstLoadRecorder = loadRecorder;
1✔
590
        break;
1✔
591
      default:
592
        throw new AssertionError("Missing case for " + config.getMode());
×
593
    }
594

595
    dropList = Collections.unmodifiableList(newDropList);
1✔
596
    backendList = Collections.unmodifiableList(newBackendList);
1✔
597
  }
1✔
598

599
  @VisibleForTesting
600
  class FallbackModeTask implements Runnable {
601
    private final Status reason;
602

603
    private FallbackModeTask(Status reason) {
1✔
604
      this.reason = reason;
1✔
605
    }
1✔
606

607
    @Override
608
    public void run() {
609
      // Timer should have been cancelled if entered fallback early.
610
      checkState(!usingFallbackBackends, "already in fallback");
1✔
611
      fallbackReason = reason;
1✔
612
      maybeUseFallbackBackends();
1✔
613
      maybeUpdatePicker();
1✔
614
    }
1✔
615
  }
616

617
  @VisibleForTesting
618
  class LbRpcRetryTask implements Runnable {
1✔
619
    @Override
620
    public void run() {
621
      startLbRpc();
1✔
622
    }
1✔
623
  }
624

625
  @VisibleForTesting
626
  static class LoadReportingTask implements Runnable {
627
    private final LbStream stream;
628

629
    LoadReportingTask(LbStream stream) {
1✔
630
      this.stream = stream;
1✔
631
    }
1✔
632

633
    @Override
634
    public void run() {
635
      stream.loadReportTimer = null;
1✔
636
      stream.sendLoadReport();
1✔
637
    }
1✔
638
  }
639

640
  private class LbStream implements StreamObserver<LoadBalanceResponse> {
641
    final GrpclbClientLoadRecorder loadRecorder;
642
    final LoadBalancerGrpc.LoadBalancerStub stub;
643
    StreamObserver<LoadBalanceRequest> lbRequestWriter;
644

645
    // These fields are only accessed from helper.runSerialized()
646
    boolean initialResponseReceived;
647
    boolean closed;
648
    long loadReportIntervalMillis = -1;
1✔
649
    ScheduledHandle loadReportTimer;
650

651
    LbStream(LoadBalancerGrpc.LoadBalancerStub stub) {
1✔
652
      this.stub = checkNotNull(stub, "stub");
1✔
653
      // Stats data only valid for current LbStream.  We do not carry over data from previous
654
      // stream.
655
      loadRecorder = new GrpclbClientLoadRecorder(time);
1✔
656
    }
1✔
657

658
    void start() {
659
      lbRequestWriter = stub.withWaitForReady().balanceLoad(this);
1✔
660
    }
1✔
661

662
    @Override public void onNext(final LoadBalanceResponse response) {
663
      syncContext.execute(new Runnable() {
1✔
664
          @Override
665
          public void run() {
666
            handleResponse(response);
1✔
667
          }
1✔
668
        });
669
    }
1✔
670

671
    @Override public void onError(final Throwable error) {
672
      syncContext.execute(new Runnable() {
1✔
673
          @Override
674
          public void run() {
675
            handleStreamClosed(Status.fromThrowable(error)
1✔
676
                .augmentDescription("Stream to GRPCLB LoadBalancer had an error"));
1✔
677
          }
1✔
678
        });
679
    }
1✔
680

681
    @Override public void onCompleted() {
682
      syncContext.execute(new Runnable() {
1✔
683
          @Override
684
          public void run() {
685
            handleStreamClosed(
1✔
686
                Status.UNAVAILABLE.withDescription("Stream to GRPCLB LoadBalancer was closed"));
1✔
687
          }
1✔
688
        });
689
    }
1✔
690

691
    // Following methods must be run in helper.runSerialized()
692

693
    private void sendLoadReport() {
694
      if (closed) {
1✔
695
        return;
×
696
      }
697
      ClientStats stats = loadRecorder.generateLoadReport();
1✔
698
      // TODO(zhangkun83): flow control?
699
      try {
700
        lbRequestWriter.onNext(LoadBalanceRequest.newBuilder().setClientStats(stats).build());
1✔
701
        scheduleNextLoadReport();
1✔
702
      } catch (Exception e) {
×
703
        close(e);
×
704
      }
1✔
705
    }
1✔
706

707
    private void scheduleNextLoadReport() {
708
      if (loadReportIntervalMillis > 0) {
1✔
709
        loadReportTimer = syncContext.schedule(
1✔
710
            new LoadReportingTask(this), loadReportIntervalMillis, TimeUnit.MILLISECONDS,
711
            timerService);
1✔
712
      }
713
    }
1✔
714

715
    private void handleResponse(LoadBalanceResponse response) {
716
      if (closed) {
1✔
717
        return;
×
718
      }
719

720
      LoadBalanceResponseTypeCase typeCase = response.getLoadBalanceResponseTypeCase();
1✔
721
      if (!initialResponseReceived) {
1✔
722
        logger.log(
1✔
723
            ChannelLogLevel.INFO,
724
            "[grpclb-<{0}>] Got an LB initial response: {1}", serviceName, response);
1✔
725
        if (typeCase != LoadBalanceResponseTypeCase.INITIAL_RESPONSE) {
1✔
726
          logger.log(
×
727
              ChannelLogLevel.WARNING,
728
              "[grpclb-<{0}>] Received a response without initial response",
729
              serviceName);
×
730
          return;
×
731
        }
732
        initialResponseReceived = true;
1✔
733
        InitialLoadBalanceResponse initialResponse = response.getInitialResponse();
1✔
734
        loadReportIntervalMillis =
1✔
735
            Durations.toMillis(initialResponse.getClientStatsReportInterval());
1✔
736
        scheduleNextLoadReport();
1✔
737
        return;
1✔
738
      }
739
      if (SHOULD_LOG_SERVER_LISTS) {
1✔
740
        logger.log(
1✔
741
            ChannelLogLevel.DEBUG, "[grpclb-<{0}>] Got an LB response: {1}", serviceName, response);
1✔
742
      } else {
743
        logger.log(ChannelLogLevel.DEBUG, "[grpclb-<{0}>] Got an LB response", serviceName);
×
744
      }
745

746
      if (typeCase == LoadBalanceResponseTypeCase.FALLBACK_RESPONSE) {
1✔
747
        // Force entering fallback requested by balancer.
748
        cancelFallbackTimer();
1✔
749
        fallbackReason = BALANCER_REQUESTED_FALLBACK_STATUS;
1✔
750
        useFallbackBackends();
1✔
751
        maybeUpdatePicker();
1✔
752
        return;
1✔
753
      } else if (typeCase != LoadBalanceResponseTypeCase.SERVER_LIST) {
1✔
754
        logger.log(
1✔
755
            ChannelLogLevel.WARNING,
756
            "[grpclb-<{0}>] Ignoring unexpected response type: {1}",
757
            serviceName,
1✔
758
            typeCase);
759
        return;
1✔
760
      }
761

762
      balancerWorking = true;
1✔
763
      // TODO(zhangkun83): handle delegate from initialResponse
764
      ServerList serverList = response.getServerList();
1✔
765
      List<DropEntry> newDropList = new ArrayList<>();
1✔
766
      List<BackendAddressGroup> newBackendAddrList = new ArrayList<>();
1✔
767
      // Construct the new collections. Create new Subchannels when necessary.
768
      for (Server server : serverList.getServersList()) {
1✔
769
        String token = server.getLoadBalanceToken();
1✔
770
        if (server.getDrop()) {
1✔
771
          newDropList.add(new DropEntry(loadRecorder, token));
1✔
772
        } else {
773
          newDropList.add(null);
1✔
774
          InetSocketAddress address;
775
          try {
776
            address = new InetSocketAddress(
1✔
777
                InetAddress.getByAddress(server.getIpAddress().toByteArray()), server.getPort());
1✔
778
          } catch (UnknownHostException e) {
×
779
            propagateError(
×
780
                Status.UNAVAILABLE
781
                    .withDescription("Invalid backend address: " + server)
×
782
                    .withCause(e));
×
783
            continue;
×
784
          }
1✔
785
          // ALTS code can use the presence of ATTR_LB_PROVIDED_BACKEND to select ALTS instead of
786
          // TLS, with Netty.
787
          EquivalentAddressGroup eag =
1✔
788
              new EquivalentAddressGroup(address, LB_PROVIDED_BACKEND_ATTRS);
1✔
789
          newBackendAddrList.add(new BackendAddressGroup(eag, token));
1✔
790
        }
791
      }
1✔
792
      // Exit fallback as soon as a new server list is received from the balancer.
793
      usingFallbackBackends = false;
1✔
794
      fallbackReason = null;
1✔
795
      cancelFallbackTimer();
1✔
796
      updateServerList(newDropList, newBackendAddrList, loadRecorder);
1✔
797
      maybeUpdatePicker();
1✔
798
    }
1✔
799

800
    private void handleStreamClosed(Status error) {
801
      checkArgument(!error.isOk(), "unexpected OK status");
1✔
802
      if (closed) {
1✔
803
        return;
1✔
804
      }
805
      closed = true;
1✔
806
      cleanUp();
1✔
807
      propagateError(error);
1✔
808
      balancerWorking = false;
1✔
809
      fallbackReason = error;
1✔
810
      cancelFallbackTimer();
1✔
811
      maybeUseFallbackBackends();
1✔
812
      maybeUpdatePicker();
1✔
813

814
      long delayNanos = 0;
1✔
815
      if (initialResponseReceived || lbRpcRetryPolicy == null) {
1✔
816
        // Reset the backoff sequence if balancer has sent the initial response, or backoff sequence
817
        // has never been initialized.
818
        lbRpcRetryPolicy = backoffPolicyProvider.get();
1✔
819
      }
820
      // Backoff only when balancer wasn't working previously.
821
      if (!initialResponseReceived) {
1✔
822
        // The back-off policy determines the interval between consecutive RPC upstarts, thus the
823
        // actual delay may be smaller than the value from the back-off policy, or even negative,
824
        // depending how much time was spent in the previous RPC.
825
        delayNanos =
1✔
826
            lbRpcRetryPolicy.nextBackoffNanos() - stopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
827
      }
828
      if (delayNanos <= 0) {
1✔
829
        startLbRpc();
1✔
830
      } else {
831
        lbRpcRetryTimer =
1✔
832
            syncContext.schedule(new LbRpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS,
1✔
833
                timerService);
1✔
834
      }
835

836
      helper.refreshNameResolution();
1✔
837
    }
1✔
838

839
    void close(Exception error) {
840
      if (closed) {
1✔
841
        return;
×
842
      }
843
      closed = true;
1✔
844
      cleanUp();
1✔
845
      lbRequestWriter.onError(error);
1✔
846
    }
1✔
847

848
    private void cleanUp() {
849
      if (loadReportTimer != null) {
1✔
850
        loadReportTimer.cancel();
1✔
851
        loadReportTimer = null;
1✔
852
      }
853
      if (lbStream == this) {
1✔
854
        lbStream = null;
1✔
855
      }
856
    }
1✔
857
  }
858

859
  /**
860
   * Make and use a picker out of the current lists and the states of subchannels if they have
861
   * changed since the last picker created.
862
   */
863
  private void maybeUpdatePicker() {
864
    List<RoundRobinEntry> pickList;
865
    ConnectivityState state;
866
    // For PICK_FIRST mode with delegation, check if child LB exists instead of backendList.
867
    boolean hasBackends = config.getMode() == Mode.PICK_FIRST
1✔
868
        ? pickFirstLb != null
1✔
869
        : !backendList.isEmpty();
1✔
870
    if (!hasBackends) {
1✔
871
      // Note balancer (is working) may enforce using fallback backends, and that fallback may
872
      // fail. So we should check if currently in fallback first.
873
      if (usingFallbackBackends) {
1✔
874
        Status error =
1✔
875
            NO_FALLBACK_BACKENDS_STATUS
876
                .withCause(fallbackReason.getCause())
1✔
877
                .augmentDescription(fallbackReason.getDescription());
1✔
878
        pickList = Collections.<RoundRobinEntry>singletonList(new ErrorEntry(error));
1✔
879
        state = TRANSIENT_FAILURE;
1✔
880
      } else if (balancerWorking)  {
1✔
881
        pickList =
1✔
882
            Collections.<RoundRobinEntry>singletonList(
1✔
883
                new ErrorEntry(NO_AVAILABLE_BACKENDS_STATUS));
884
        state = TRANSIENT_FAILURE;
1✔
885
      } else {  // still waiting for balancer
886
        pickList = Collections.singletonList(BUFFER_ENTRY);
1✔
887
        state = CONNECTING;
1✔
888
      }
889
      maybeUpdatePicker(state, new RoundRobinPicker(dropList, pickList));
1✔
890
      return;
1✔
891
    }
892
    switch (config.getMode()) {
1✔
893
      case ROUND_ROBIN:
894
        pickList = new ArrayList<>(backendList.size());
1✔
895
        Status error = null;
1✔
896
        boolean hasPending = false;
1✔
897
        for (BackendEntry entry : backendList) {
1✔
898
          Subchannel subchannel = entry.subchannel;
1✔
899
          Attributes attrs = subchannel.getAttributes();
1✔
900
          ConnectivityStateInfo stateInfo = attrs.get(STATE_INFO).get();
1✔
901
          if (stateInfo.getState() == READY) {
1✔
902
            pickList.add(entry);
1✔
903
          } else if (stateInfo.getState() == TRANSIENT_FAILURE) {
1✔
904
            error = stateInfo.getStatus();
1✔
905
          } else {
906
            hasPending = true;
1✔
907
          }
908
        }
1✔
909
        if (pickList.isEmpty()) {
1✔
910
          if (hasPending) {
1✔
911
            pickList.add(BUFFER_ENTRY);
1✔
912
            state = CONNECTING;
1✔
913
          } else {
914
            pickList.add(new ErrorEntry(error));
1✔
915
            state = TRANSIENT_FAILURE;
1✔
916
          }
917
        } else {
918
          state = READY;
1✔
919
        }
920
        break;
1✔
921
      case PICK_FIRST: {
922
        // Use child LB's state and picker. Wrap the picker for token attachment.
923
        state = pickFirstLbState;
1✔
924
        TokenAttachingTracerFactory tracerFactory =
1✔
925
            new TokenAttachingTracerFactory(currentPickFirstLoadRecorder);
926
        pickList = Collections.<RoundRobinEntry>singletonList(
1✔
927
            new ChildLbPickerEntry(pickFirstLbPicker, tracerFactory));
928
        break;
1✔
929
      }
930
      default:
931
        throw new AssertionError("Missing case for " + config.getMode());
×
932
    }
933
    maybeUpdatePicker(state, new RoundRobinPicker(dropList, pickList));
1✔
934
  }
1✔
935

936
  /**
937
   * Update the given picker to the helper if it's different from the current one.
938
   */
939
  private void maybeUpdatePicker(ConnectivityState state, RoundRobinPicker picker) {
940
    // Discard the new picker if we are sure it won't make any difference, in order to save
941
    // re-processing pending streams, and avoid unnecessary resetting of the pointer in
942
    // RoundRobinPicker.
943
    if (state.equals(currentState)
1✔
944
        && picker.dropList.equals(currentPicker.dropList)
1✔
945
        && picker.pickList.equals(currentPicker.pickList)) {
1✔
946
      return;
1✔
947
    }
948
    currentState = state;
1✔
949
    currentPicker = picker;
1✔
950
    helper.updateBalancingState(state, picker);
1✔
951
  }
1✔
952

953
  private static Attributes createSubchannelAttrs() {
954
    return Attributes.newBuilder()
1✔
955
        .set(STATE_INFO,
1✔
956
            new AtomicReference<>(
957
                ConnectivityStateInfo.forNonError(IDLE)))
1✔
958
        .build();
1✔
959
  }
960

961
  @VisibleForTesting
962
  static final class DropEntry {
963
    private final GrpclbClientLoadRecorder loadRecorder;
964
    private final String token;
965

966
    DropEntry(GrpclbClientLoadRecorder loadRecorder, String token) {
1✔
967
      this.loadRecorder = checkNotNull(loadRecorder, "loadRecorder");
1✔
968
      this.token = checkNotNull(token, "token");
1✔
969
    }
1✔
970

971
    PickResult picked() {
972
      loadRecorder.recordDroppedRequest(token);
1✔
973
      return DROP_PICK_RESULT;
1✔
974
    }
975

976
    @Override
977
    public String toString() {
978
      // This is printed in logs.  Only include useful information.
979
      return "drop(" + token + ")";
×
980
    }
981

982
    @Override
983
    public int hashCode() {
984
      return Objects.hashCode(loadRecorder, token);
×
985
    }
986

987
    @Override
988
    public boolean equals(Object other) {
989
      if (!(other instanceof DropEntry)) {
1✔
990
        return false;
1✔
991
      }
992
      DropEntry that = (DropEntry) other;
1✔
993
      return Objects.equal(loadRecorder, that.loadRecorder) && Objects.equal(token, that.token);
1✔
994
    }
995
  }
996

997
  @VisibleForTesting
998
  interface RoundRobinEntry {
999
    PickResult picked(PickSubchannelArgs args);
1000
  }
1001

1002
  @VisibleForTesting
1003
  static final class BackendEntry implements RoundRobinEntry {
1004
    final Subchannel subchannel;
1005
    @VisibleForTesting
1006
    final PickResult result;
1007
    @Nullable
1008
    private final String token;
1009

1010
    /**
1011
     * For ROUND_ROBIN: creates a BackendEntry whose usage will be reported to load recorder.
1012
     */
1013
    BackendEntry(Subchannel subchannel, GrpclbClientLoadRecorder loadRecorder, String token) {
1✔
1014
      this.subchannel = checkNotNull(subchannel, "subchannel");
1✔
1015
      this.result =
1✔
1016
          PickResult.withSubchannel(subchannel, checkNotNull(loadRecorder, "loadRecorder"));
1✔
1017
      this.token = checkNotNull(token, "token");
1✔
1018
    }
1✔
1019

1020
    /**
1021
     * For ROUND_ROBIN/PICK_FIRST: creates a BackendEntry whose usage will not be reported.
1022
     */
1023
    BackendEntry(Subchannel subchannel) {
1✔
1024
      this.subchannel = checkNotNull(subchannel, "subchannel");
1✔
1025
      this.result = PickResult.withSubchannel(subchannel);
1✔
1026
      this.token = null;
1✔
1027
    }
1✔
1028

1029
    /**
1030
     * For PICK_FIRST: creates a BackendEntry that includes all addresses.
1031
     */
1032
    BackendEntry(Subchannel subchannel, TokenAttachingTracerFactory tracerFactory) {
×
1033
      this.subchannel = checkNotNull(subchannel, "subchannel");
×
1034
      this.result =
×
1035
          PickResult.withSubchannel(subchannel, checkNotNull(tracerFactory, "tracerFactory"));
×
1036
      this.token = null;
×
1037
    }
×
1038

1039
    @Override
1040
    public PickResult picked(PickSubchannelArgs args) {
1041
      Metadata headers = args.getHeaders();
1✔
1042
      headers.discardAll(GrpclbConstants.TOKEN_METADATA_KEY);
1✔
1043
      if (token != null) {
1✔
1044
        headers.put(GrpclbConstants.TOKEN_METADATA_KEY, token);
1✔
1045
      }
1046
      return result;
1✔
1047
    }
1048

1049
    @Override
1050
    public String toString() {
1051
      // This is printed in logs.  Only give out useful information.
1052
      return "[" + subchannel.getAllAddresses().toString() + "(" + token + ")]";
×
1053
    }
1054

1055
    @Override
1056
    public int hashCode() {
1057
      return Objects.hashCode(result, token);
×
1058
    }
1059

1060
    @Override
1061
    public boolean equals(Object other) {
1062
      if (!(other instanceof BackendEntry)) {
1✔
1063
        return false;
×
1064
      }
1065
      BackendEntry that = (BackendEntry) other;
1✔
1066
      return Objects.equal(result, that.result) && Objects.equal(token, that.token);
1✔
1067
    }
1068
  }
1069

1070
  @VisibleForTesting
1071
  static final class IdleSubchannelEntry implements RoundRobinEntry {
1072
    private final SynchronizationContext syncContext;
1073
    private final Subchannel subchannel;
1074
    private final AtomicBoolean connectionRequested = new AtomicBoolean(false);
1✔
1075

1076
    IdleSubchannelEntry(Subchannel subchannel, SynchronizationContext syncContext) {
1✔
1077
      this.subchannel = checkNotNull(subchannel, "subchannel");
1✔
1078
      this.syncContext = checkNotNull(syncContext, "syncContext");
1✔
1079
    }
1✔
1080

1081
    @Override
1082
    public PickResult picked(PickSubchannelArgs args) {
1083
      if (connectionRequested.compareAndSet(false, true)) {
1✔
1084
        syncContext.execute(new Runnable() {
1✔
1085
            @Override
1086
            public void run() {
1087
              subchannel.requestConnection();
1✔
1088
            }
1✔
1089
          });
1090
      }
1091
      return PickResult.withNoResult();
1✔
1092
    }
1093

1094
    @Override
1095
    public String toString() {
1096
      // This is printed in logs.  Only give out useful information.
1097
      return "(idle)[" + subchannel.getAllAddresses().toString() + "]";
×
1098
    }
1099

1100
    @Override
1101
    public int hashCode() {
1102
      return Objects.hashCode(subchannel, syncContext);
×
1103
    }
1104

1105
    @Override
1106
    public boolean equals(Object other) {
1107
      if (!(other instanceof IdleSubchannelEntry)) {
×
1108
        return false;
×
1109
      }
1110
      IdleSubchannelEntry that = (IdleSubchannelEntry) other;
×
1111
      return Objects.equal(subchannel, that.subchannel)
×
1112
          && Objects.equal(syncContext, that.syncContext);
×
1113
    }
1114
  }
1115

1116
  @VisibleForTesting
1117
  static final class ErrorEntry implements RoundRobinEntry {
1118
    final PickResult result;
1119

1120
    ErrorEntry(Status status) {
1✔
1121
      result = PickResult.withError(status);
1✔
1122
    }
1✔
1123

1124
    @Override
1125
    public PickResult picked(PickSubchannelArgs args) {
1126
      return result;
1✔
1127
    }
1128

1129
    @Override
1130
    public int hashCode() {
1131
      return Objects.hashCode(result);
×
1132
    }
1133

1134
    @Override
1135
    public boolean equals(Object other) {
1136
      if (!(other instanceof ErrorEntry)) {
1✔
1137
        return false;
×
1138
      }
1139
      return Objects.equal(result, ((ErrorEntry) other).result);
1✔
1140
    }
1141

1142
    @Override
1143
    public String toString() {
1144
      // This is printed in logs.  Only include useful information.
1145
      return result.getStatus().toString();
×
1146
    }
1147
  }
1148

1149
  /**
1150
   * Entry that wraps a child LB's picker for PICK_FIRST mode delegation.
1151
   * Attaches TokenAttachingTracerFactory to the pick result for token propagation.
1152
   */
1153
  @VisibleForTesting
1154
  static final class ChildLbPickerEntry implements RoundRobinEntry {
1155
    private final SubchannelPicker childPicker;
1156
    private final TokenAttachingTracerFactory tracerFactory;
1157

1158
    ChildLbPickerEntry(SubchannelPicker childPicker, TokenAttachingTracerFactory tracerFactory) {
1✔
1159
      this.childPicker = checkNotNull(childPicker, "childPicker");
1✔
1160
      this.tracerFactory = checkNotNull(tracerFactory, "tracerFactory");
1✔
1161
    }
1✔
1162

1163
    @Override
1164
    public PickResult picked(PickSubchannelArgs args) {
1165
      PickResult childResult = childPicker.pickSubchannel(args);
1✔
1166
      if (childResult.getSubchannel() == null) {
1✔
1167
        // No subchannel (e.g., buffer, error), return as-is.
1168
        return childResult;
1✔
1169
      }
1170
      // Wrap the pick result to attach tokens via the tracer factory.
1171
      return PickResult.withSubchannel(
1✔
1172
          childResult.getSubchannel(), tracerFactory, childResult.getAuthorityOverride());
1✔
1173
    }
1174

1175
    @Override
1176
    public int hashCode() {
1177
      return Objects.hashCode(childPicker, tracerFactory);
×
1178
    }
1179

1180
    @Override
1181
    public boolean equals(Object other) {
1182
      if (!(other instanceof ChildLbPickerEntry)) {
1✔
1183
        return false;
1✔
1184
      }
1185
      ChildLbPickerEntry that = (ChildLbPickerEntry) other;
1✔
1186
      return Objects.equal(childPicker, that.childPicker)
1✔
1187
          && Objects.equal(tracerFactory, that.tracerFactory);
1✔
1188
    }
1189

1190
    @Override
1191
    public String toString() {
1192
      return "ChildLbPickerEntry(" + childPicker + ")";
×
1193
    }
1194

1195
    @VisibleForTesting
1196
    SubchannelPicker getChildPicker() {
1197
      return childPicker;
1✔
1198
    }
1199
  }
1200

1201
  @VisibleForTesting
1202
  static final class RoundRobinPicker extends SubchannelPicker {
1203
    @VisibleForTesting
1204
    final List<DropEntry> dropList;
1205
    private int dropIndex;
1206

1207
    @VisibleForTesting
1208
    final List<? extends RoundRobinEntry> pickList;
1209
    private int pickIndex;
1210

1211
    // dropList can be empty, which means no drop.
1212
    // pickList must not be empty.
1213
    RoundRobinPicker(List<DropEntry> dropList, List<? extends RoundRobinEntry> pickList) {
1✔
1214
      this.dropList = checkNotNull(dropList, "dropList");
1✔
1215
      this.pickList = checkNotNull(pickList, "pickList");
1✔
1216
      checkArgument(!pickList.isEmpty(), "pickList is empty");
1✔
1217
    }
1✔
1218

1219
    @Override
1220
    public PickResult pickSubchannel(PickSubchannelArgs args) {
1221
      synchronized (pickList) {
1✔
1222
        // Two-level round-robin.
1223
        // First round-robin on dropList. If a drop entry is selected, request will be dropped.  If
1224
        // a non-drop entry is selected, then round-robin on pickList.  This makes sure requests are
1225
        // dropped at the same proportion as the drop entries appear on the round-robin list from
1226
        // the balancer, while only backends from pickList are selected for the non-drop cases.
1227
        if (!dropList.isEmpty()) {
1✔
1228
          DropEntry drop = dropList.get(dropIndex);
1✔
1229
          dropIndex++;
1✔
1230
          if (dropIndex == dropList.size()) {
1✔
1231
            dropIndex = 0;
1✔
1232
          }
1233
          if (drop != null) {
1✔
1234
            return drop.picked();
1✔
1235
          }
1236
        }
1237

1238
        RoundRobinEntry pick = pickList.get(pickIndex);
1✔
1239
        pickIndex++;
1✔
1240
        if (pickIndex == pickList.size()) {
1✔
1241
          pickIndex = 0;
1✔
1242
        }
1243
        return pick.picked(args);
1✔
1244
      }
1245
    }
1246

1247
    @Override
1248
    public String toString() {
1249
      if (SHOULD_LOG_SERVER_LISTS) {
×
1250
        return MoreObjects.toStringHelper(RoundRobinPicker.class)
×
1251
            .add("dropList", dropList)
×
1252
            .add("pickList", pickList)
×
1253
            .toString();
×
1254
      }
1255
      return MoreObjects.toStringHelper(RoundRobinPicker.class).toString();
×
1256
    }
1257
  }
1258

1259
  /**
1260
   * Helper for the child pick_first LB in PICK_FIRST mode. Intercepts updateBalancingState()
1261
   * to store state and trigger the grpclb picker update with drops and token attachment.
1262
   */
1263
  private final class PickFirstLbHelper extends ForwardingLoadBalancerHelper {
1✔
1264

1265
    @Override
1266
    protected Helper delegate() {
1267
      return helper;
1✔
1268
    }
1269

1270
    @Override
1271
    public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
1272
      pickFirstLbState = newState;
1✔
1273
      pickFirstLbPicker = newPicker;
1✔
1274
      // Trigger name resolution refresh on TRANSIENT_FAILURE or IDLE, similar to ROUND_ROBIN.
1275
      if (newState == TRANSIENT_FAILURE || newState == IDLE) {
1✔
1276
        helper.refreshNameResolution();
1✔
1277
      }
1278
      maybeUseFallbackBackends();
1✔
1279
      maybeUpdatePicker();
1✔
1280
    }
1✔
1281
  }
1282
}
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