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

grpc / grpc-java / #19753

28 Mar 2025 03:18PM UTC coverage: 88.601% (+0.005%) from 88.596%
#19753

push

github

ejona86
util: Graceful switch to new LB when leaving CONNECTING

Previously it would wait for the new LB to enter READY. However, that
prevents there being an upper-bound on how long the old policy will
continue to be used. The point of graceful switch is to avoid RPCs
seeing increased latency when we swap config. We don't want it to
prevent the system from becoming eventually consistent.

34613 of 39066 relevant lines covered (88.6%)

0.89 hits per line

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

97.22
/../util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java
1
/*
2
 * Copyright 2019 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.util;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static com.google.common.base.Preconditions.checkState;
21

22
import com.google.common.annotations.VisibleForTesting;
23
import com.google.common.base.MoreObjects;
24
import com.google.common.base.Objects;
25
import io.grpc.ConnectivityState;
26
import io.grpc.ConnectivityStateInfo;
27
import io.grpc.ExperimentalApi;
28
import io.grpc.LoadBalancer;
29
import io.grpc.LoadBalancerRegistry;
30
import io.grpc.NameResolver.ConfigOrError;
31
import io.grpc.Status;
32
import io.grpc.internal.ServiceConfigUtil;
33
import java.util.List;
34
import java.util.Map;
35
import javax.annotation.Nullable;
36
import javax.annotation.concurrent.NotThreadSafe;
37

38
/**
39
 * A load balancer that gracefully swaps to a new lb policy. If the channel is currently in a state
40
 * other than READY, the new policy will be swapped into place immediately.  Otherwise, the channel
41
 * will keep using the old policy until the new policy leaves CONNECTING or the old policy exits
42
 * READY.
43
 *
44
 * <p>The child balancer and configuration is specified using service config. Config objects are
45
 * generally created by calling {@link #parseLoadBalancingPolicyConfig(List)} from a
46
 * {@link io.grpc.LoadBalancerProvider#parseLoadBalancingPolicyConfig
47
 * provider's parseLoadBalancingPolicyConfig()} implementation.
48
 */
49
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/5999")
50
@NotThreadSafe // Must be accessed in SynchronizationContext
51
public final class GracefulSwitchLoadBalancer extends ForwardingLoadBalancer {
52
  private final LoadBalancer defaultBalancer = new LoadBalancer() {
1✔
53
    @Override
54
    public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
55
      throw new AssertionError("real LB is called instead");
×
56
    }
57

58
    @Override
59
    public void handleNameResolutionError(final Status error) {
60
      helper.updateBalancingState(
1✔
61
          ConnectivityState.TRANSIENT_FAILURE,
62
          new FixedResultPicker(PickResult.withError(error)));
1✔
63
    }
1✔
64

65
    @Override
66
    public void shutdown() {}
1✔
67
  };
68

69
  @VisibleForTesting
70
  static final SubchannelPicker BUFFER_PICKER = new SubchannelPicker() {
1✔
71
    @Override
72
    public PickResult pickSubchannel(PickSubchannelArgs args) {
73
      return PickResult.withNoResult();
1✔
74
    }
75

76
    @Override
77
    public String toString() {
78
      return "BUFFER_PICKER";
×
79
    }
80
  };
81

82
  private final Helper helper;
83

84
  // While the new policy is not fully switched on, the pendingLb is handling new updates from name
85
  // resolver, and the currentLb is updating channel state and picker for the given helper.
86
  // The current fields are guaranteed to be set after the initial swapTo().
87
  // The pending fields are cleared when it becomes current.
88
  @Nullable private LoadBalancer.Factory currentBalancerFactory;
89
  private LoadBalancer currentLb = defaultBalancer;
1✔
90
  @Nullable private LoadBalancer.Factory pendingBalancerFactory;
91
  private LoadBalancer pendingLb = defaultBalancer;
1✔
92
  private ConnectivityState pendingState;
93
  private SubchannelPicker pendingPicker;
94

95
  private boolean currentLbIsReady;
96

97
  public GracefulSwitchLoadBalancer(Helper helper) {
1✔
98
    this.helper = checkNotNull(helper, "helper");
1✔
99
  }
1✔
100

101
  @Override
102
  public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
103
    Config config = (Config) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
104
    switchToInternal(config.childFactory);
1✔
105
    delegate().handleResolvedAddresses(
1✔
106
        resolvedAddresses.toBuilder()
1✔
107
          .setLoadBalancingPolicyConfig(config.childConfig)
1✔
108
          .build());
1✔
109
  }
1✔
110

111
  @Override
112
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
113
    Config config = (Config) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
114
    switchToInternal(config.childFactory);
1✔
115
    return delegate().acceptResolvedAddresses(
1✔
116
        resolvedAddresses.toBuilder()
1✔
117
          .setLoadBalancingPolicyConfig(config.childConfig)
1✔
118
          .build());
1✔
119
  }
120

121
  private void switchToInternal(LoadBalancer.Factory newBalancerFactory) {
122
    checkNotNull(newBalancerFactory, "newBalancerFactory");
1✔
123

124
    if (newBalancerFactory.equals(pendingBalancerFactory)) {
1✔
125
      return;
1✔
126
    }
127
    pendingLb.shutdown();
1✔
128
    pendingLb = defaultBalancer;
1✔
129
    pendingBalancerFactory = null;
1✔
130
    pendingState = ConnectivityState.CONNECTING;
1✔
131
    pendingPicker = BUFFER_PICKER;
1✔
132

133
    if (newBalancerFactory.equals(currentBalancerFactory)) {
1✔
134
      return;
1✔
135
    }
136

137
    class PendingHelper extends ForwardingLoadBalancerHelper {
1✔
138
      LoadBalancer lb;
139

140
      @Override
141
      protected Helper delegate() {
142
        return helper;
1✔
143
      }
144

145
      @Override
146
      public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
147
        if (lb == pendingLb) {
1✔
148
          checkState(currentLbIsReady, "there's pending lb while current lb has been out of READY");
1✔
149
          pendingState = newState;
1✔
150
          pendingPicker = newPicker;
1✔
151
          if (newState != ConnectivityState.CONNECTING) {
1✔
152
            swap();
1✔
153
          }
154
        } else if (lb == currentLb) {
1✔
155
          currentLbIsReady = newState == ConnectivityState.READY;
1✔
156
          if (!currentLbIsReady && pendingLb != defaultBalancer) {
1✔
157
            swap(); // current policy exits READY, so swap
1✔
158
          } else {
159
            helper.updateBalancingState(newState, newPicker);
1✔
160
          }
161
        }
162
      }
1✔
163
    }
164

165
    PendingHelper pendingHelper = new PendingHelper();
1✔
166
    pendingHelper.lb = newBalancerFactory.newLoadBalancer(pendingHelper);
1✔
167
    pendingLb = pendingHelper.lb;
1✔
168
    pendingBalancerFactory = newBalancerFactory;
1✔
169
    if (!currentLbIsReady) {
1✔
170
      swap(); // the old policy is not READY at the moment, so swap to the new one right now
1✔
171
    }
172
  }
1✔
173

174
  private void swap() {
175
    helper.updateBalancingState(pendingState, pendingPicker);
1✔
176
    currentLb.shutdown();
1✔
177
    currentLb = pendingLb;
1✔
178
    currentBalancerFactory = pendingBalancerFactory;
1✔
179
    pendingLb = defaultBalancer;
1✔
180
    pendingBalancerFactory = null;
1✔
181
  }
1✔
182

183
  @Override
184
  protected LoadBalancer delegate() {
185
    return pendingLb == defaultBalancer ? currentLb : pendingLb;
1✔
186
  }
187

188
  @Override
189
  @Deprecated
190
  public void handleSubchannelState(
191
      Subchannel subchannel, ConnectivityStateInfo stateInfo) {
192
    throw new UnsupportedOperationException(
1✔
193
        "handleSubchannelState() is not supported by " + this.getClass().getName());
1✔
194
  }
195

196
  @Override
197
  public void shutdown() {
198
    pendingLb.shutdown();
1✔
199
    currentLb.shutdown();
1✔
200
  }
1✔
201

202
  public String delegateType() {
203
    return delegate().getClass().getSimpleName();
×
204
  }
205

206
  /**
207
   * Provided a JSON list of LoadBalancingConfigs, parse it into a config to pass to GracefulSwitch.
208
   */
209
  public static ConfigOrError parseLoadBalancingPolicyConfig(
210
      List<Map<String, ?>> loadBalancingConfigs) {
211
    return parseLoadBalancingPolicyConfig(
1✔
212
        loadBalancingConfigs, LoadBalancerRegistry.getDefaultRegistry());
1✔
213
  }
214

215
  /**
216
   * Provided a JSON list of LoadBalancingConfigs, parse it into a config to pass to GracefulSwitch.
217
   */
218
  public static ConfigOrError parseLoadBalancingPolicyConfig(
219
      List<Map<String, ?>> loadBalancingConfigs, LoadBalancerRegistry lbRegistry) {
220
    List<ServiceConfigUtil.LbConfig> childConfigCandidates =
1✔
221
        ServiceConfigUtil.unwrapLoadBalancingConfigList(loadBalancingConfigs);
1✔
222
    if (childConfigCandidates == null || childConfigCandidates.isEmpty()) {
1✔
223
      return ConfigOrError.fromError(
1✔
224
          Status.INTERNAL.withDescription("No child LB config specified"));
1✔
225
    }
226
    ConfigOrError selectedConfig =
1✔
227
        ServiceConfigUtil.selectLbPolicyFromList(childConfigCandidates, lbRegistry);
1✔
228
    if (selectedConfig.getError() != null) {
1✔
229
      Status error = selectedConfig.getError();
1✔
230
      return ConfigOrError.fromError(
1✔
231
          Status.INTERNAL
232
              .withCause(error.getCause())
1✔
233
              .withDescription(error.getDescription())
1✔
234
              .augmentDescription("Failed to select child config"));
1✔
235
    }
236
    ServiceConfigUtil.PolicySelection selection =
1✔
237
        (ServiceConfigUtil.PolicySelection) selectedConfig.getConfig();
1✔
238
    return ConfigOrError.fromConfig(
1✔
239
        createLoadBalancingPolicyConfig(selection.getProvider(), selection.getConfig()));
1✔
240
  }
241

242
  /**
243
   * Directly create a config to pass to GracefulSwitch. The object returned is the same as would be
244
   * found in {@code ConfigOrError.getConfig()}.
245
   */
246
  public static Object createLoadBalancingPolicyConfig(
247
      LoadBalancer.Factory childFactory, @Nullable Object childConfig) {
248
    return new Config(childFactory, childConfig);
1✔
249
  }
250

251
  static final class Config {
252
    final LoadBalancer.Factory childFactory;
253
    @Nullable
254
    final Object childConfig;
255

256
    public Config(LoadBalancer.Factory childFactory, @Nullable Object childConfig) {
1✔
257
      this.childFactory = checkNotNull(childFactory, "childFactory");
1✔
258
      this.childConfig = childConfig;
1✔
259
    }
1✔
260

261
    @Override
262
    public boolean equals(Object o) {
263
      if (this == o) {
1✔
264
        return true;
1✔
265
      }
266
      if (!(o instanceof Config)) {
1✔
267
        return false;
1✔
268
      }
269
      Config that = (Config) o;
1✔
270
      return Objects.equal(childFactory, that.childFactory)
1✔
271
          && Objects.equal(childConfig, that.childConfig);
1✔
272
    }
273

274
    @Override
275
    public int hashCode() {
276
      return Objects.hashCode(childFactory, childConfig);
1✔
277
    }
278

279
    @Override
280
    public String toString() {
281
      return MoreObjects.toStringHelper("GracefulSwitchLoadBalancer.Config")
1✔
282
          .add("childFactory", childFactory)
1✔
283
          .add("childConfig", childConfig)
1✔
284
          .toString();
1✔
285
    }
286
  }
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc