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

grpc / grpc-java / #19548

11 Nov 2024 09:16PM UTC coverage: 84.61% (+0.005%) from 84.605%
#19548

push

github

ejona86
util: Remove EAG conveniences from MultiChildLb

This is a step toward removing ResolvedAddresses from ChildLbState,
which isn't actually used by MultiChildLb. Most usages of the EAG usages
can be served more directly without peering into MultiChildLb's
internals or even accessing ChildLbStates, which make the tests less
sensitive to implementation changes. Some changes do leverage the new
behavior of MultiChildLb where it preserves the order of the entries.

This does fix an important bug in shutdown tests. The tests looped over
the ChildLbStates after shutdown, but shutdown deleted all the children
so it looped over an entry collection. Fixing that exposed that
deliverSubchannelState() didn't function after shutdown, as the listener
was removed from the map when the subchannel was shut down. Moving the
listener onto the TestSubchannel allowed having access to the listener
even after shutdown.

A few places in LeastRequestLb lines were just deleted, but that's
because an existing assertion already provided the same check but
without digging into MultiChildLb.

34091 of 40292 relevant lines covered (84.61%)

0.85 hits per line

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

97.18
/../util/src/main/java/io/grpc/util/MultiChildLoadBalancer.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.util;
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.Maps;
28
import io.grpc.Attributes;
29
import io.grpc.ConnectivityState;
30
import io.grpc.EquivalentAddressGroup;
31
import io.grpc.Internal;
32
import io.grpc.LoadBalancer;
33
import io.grpc.LoadBalancerProvider;
34
import io.grpc.Status;
35
import io.grpc.internal.PickFirstLoadBalancerProvider;
36
import java.net.SocketAddress;
37
import java.util.ArrayList;
38
import java.util.Collection;
39
import java.util.Collections;
40
import java.util.HashSet;
41
import java.util.List;
42
import java.util.Map;
43
import java.util.logging.Level;
44
import java.util.logging.Logger;
45
import javax.annotation.Nullable;
46

47
/**
48
 * A base load balancing policy for those policies which has multiple children such as
49
 * ClusterManager or the petiole policies.  For internal use only.
50
 */
51
@Internal
52
public abstract class MultiChildLoadBalancer extends LoadBalancer {
53

54
  private static final Logger logger = Logger.getLogger(MultiChildLoadBalancer.class.getName());
1✔
55
  // Modify by replacing the list to release memory when no longer used.
56
  private List<ChildLbState> childLbStates = new ArrayList<>(0);
1✔
57
  private final Helper helper;
58
  // Set to true if currently in the process of handling resolved addresses.
59
  protected boolean resolvingAddresses;
60

61
  protected final LoadBalancerProvider pickFirstLbProvider = new PickFirstLoadBalancerProvider();
1✔
62

63
  protected ConnectivityState currentConnectivityState;
64

65

66
  protected MultiChildLoadBalancer(Helper helper) {
1✔
67
    this.helper = checkNotNull(helper, "helper");
1✔
68
    logger.log(Level.FINE, "Created");
1✔
69
  }
1✔
70

71
  /**
72
   * Using the state of all children will calculate the current connectivity state,
73
   * update fields, generate a picker and then call
74
   * {@link Helper#updateBalancingState(ConnectivityState, SubchannelPicker)}.
75
   */
76
  protected abstract void updateOverallBalancingState();
77

78
  /**
79
   * Override to utilize parsing of the policy configuration or alternative helper/lb generation.
80
   * Override this if keys are not Endpoints or if child policies have configuration. Null map
81
   * values preserve the child without delivering the child an update.
82
   */
83
  protected Map<Object, ResolvedAddresses> createChildAddressesMap(
84
      ResolvedAddresses resolvedAddresses) {
85
    Map<Object, ResolvedAddresses> childAddresses =
1✔
86
        Maps.newLinkedHashMapWithExpectedSize(resolvedAddresses.getAddresses().size());
1✔
87
    for (EquivalentAddressGroup eag : resolvedAddresses.getAddresses()) {
1✔
88
      ResolvedAddresses addresses = resolvedAddresses.toBuilder()
1✔
89
          .setAddresses(Collections.singletonList(eag))
1✔
90
          .setAttributes(Attributes.newBuilder().set(IS_PETIOLE_POLICY, true).build())
1✔
91
          .setLoadBalancingPolicyConfig(null)
1✔
92
          .build();
1✔
93
      childAddresses.put(new Endpoint(eag), addresses);
1✔
94
    }
1✔
95
    return childAddresses;
1✔
96
  }
97

98
  /**
99
   * Override to create an instance of a subclass.
100
   */
101
  protected ChildLbState createChildLbState(Object key) {
102
    return new ChildLbState(key, pickFirstLbProvider);
1✔
103
  }
104

105
  /**
106
   *   Override to completely replace the default logic or to do additional activities.
107
   */
108
  @Override
109
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
110
    try {
111
      resolvingAddresses = true;
1✔
112

113
      // process resolvedAddresses to update children
114
      AcceptResolvedAddrRetVal acceptRetVal = acceptResolvedAddressesInternal(resolvedAddresses);
1✔
115
      if (!acceptRetVal.status.isOk()) {
1✔
116
        return acceptRetVal.status;
1✔
117
      }
118

119
      // Update the picker and our connectivity state
120
      updateOverallBalancingState();
1✔
121

122
      // shutdown removed children
123
      shutdownRemoved(acceptRetVal.removedChildren);
1✔
124
      return acceptRetVal.status;
1✔
125
    } finally {
126
      resolvingAddresses = false;
1✔
127
    }
128
  }
129

130
  /**
131
   * Handle the name resolution error.
132
   *
133
   * <p/>Override if you need special handling.
134
   */
135
  @Override
136
  public void handleNameResolutionError(Status error) {
137
    if (currentConnectivityState != READY)  {
1✔
138
      helper.updateBalancingState(
1✔
139
          TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
1✔
140
    }
141
  }
1✔
142

143
  @Override
144
  public void shutdown() {
145
    logger.log(Level.FINE, "Shutdown");
1✔
146
    for (ChildLbState state : childLbStates) {
1✔
147
      state.shutdown();
1✔
148
    }
1✔
149
    childLbStates.clear();
1✔
150
  }
1✔
151

152
  /**
153
   *   This does the work to update the child map and calculate which children have been removed.
154
   *   You must call {@link #updateOverallBalancingState} to update the picker
155
   *   and call {@link #shutdownRemoved(List)} to shutdown the endpoints that have been removed.
156
    */
157
  protected final AcceptResolvedAddrRetVal acceptResolvedAddressesInternal(
158
      ResolvedAddresses resolvedAddresses) {
159
    logger.log(Level.FINE, "Received resolution result: {0}", resolvedAddresses);
1✔
160

161
    Map<Object, ResolvedAddresses> newChildAddresses = createChildAddressesMap(resolvedAddresses);
1✔
162

163
    // Handle error case
164
    if (newChildAddresses.isEmpty()) {
1✔
165
      Status unavailableStatus = Status.UNAVAILABLE.withDescription(
1✔
166
          "NameResolver returned no usable address. " + resolvedAddresses);
167
      handleNameResolutionError(unavailableStatus);
1✔
168
      return new AcceptResolvedAddrRetVal(unavailableStatus, null);
1✔
169
    }
170

171
    List<ChildLbState> removed = updateChildrenWithResolvedAddresses(newChildAddresses);
1✔
172
    return new AcceptResolvedAddrRetVal(Status.OK, removed);
1✔
173
  }
174

175
  /** Returns removed children. */
176
  private List<ChildLbState> updateChildrenWithResolvedAddresses(
177
      Map<Object, ResolvedAddresses> newChildAddresses) {
178
    // Create a map with the old values
179
    Map<Object, ChildLbState> oldStatesMap =
1✔
180
        Maps.newLinkedHashMapWithExpectedSize(childLbStates.size());
1✔
181
    for (ChildLbState state : childLbStates) {
1✔
182
      oldStatesMap.put(state.getKey(), state);
1✔
183
    }
1✔
184

185
    // Move ChildLbStates from the map to a new list (preserving the new map's order)
186
    List<ChildLbState> newChildLbStates = new ArrayList<>(newChildAddresses.size());
1✔
187
    for (Map.Entry<Object, ResolvedAddresses> entry : newChildAddresses.entrySet()) {
1✔
188
      ChildLbState childLbState = oldStatesMap.remove(entry.getKey());
1✔
189
      if (childLbState == null) {
1✔
190
        childLbState = createChildLbState(entry.getKey());
1✔
191
      }
192
      newChildLbStates.add(childLbState);
1✔
193
      if (entry.getValue() != null) {
1✔
194
        childLbState.setResolvedAddresses(entry.getValue()); // update child
1✔
195
        childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB
1✔
196
      }
197
    }
1✔
198

199
    childLbStates = newChildLbStates;
1✔
200
    // Remaining entries in map are orphaned
201
    return new ArrayList<>(oldStatesMap.values());
1✔
202
  }
203

204
  protected final void shutdownRemoved(List<ChildLbState> removedChildren) {
205
    // Do shutdowns after updating picker to reduce the chance of failing an RPC by picking a
206
    // subchannel that has been shutdown.
207
    for (ChildLbState childLbState : removedChildren) {
1✔
208
      childLbState.shutdown();
1✔
209
    }
1✔
210
  }
1✔
211

212
  @Nullable
213
  protected static ConnectivityState aggregateState(
214
      @Nullable ConnectivityState overallState, ConnectivityState childState) {
215
    if (overallState == null) {
1✔
216
      return childState;
1✔
217
    }
218
    if (overallState == READY || childState == READY) {
1✔
219
      return READY;
1✔
220
    }
221
    if (overallState == CONNECTING || childState == CONNECTING) {
1✔
222
      return CONNECTING;
1✔
223
    }
224
    if (overallState == IDLE || childState == IDLE) {
1✔
225
      return IDLE;
×
226
    }
227
    return overallState;
1✔
228
  }
229

230
  protected final Helper getHelper() {
231
    return helper;
1✔
232
  }
233

234
  @VisibleForTesting
235
  public final Collection<ChildLbState> getChildLbStates() {
236
    return childLbStates;
1✔
237
  }
238

239
  /**
240
   * Filters out non-ready child load balancers (subchannels).
241
   */
242
  protected final List<ChildLbState> getReadyChildren() {
243
    List<ChildLbState> activeChildren = new ArrayList<>();
1✔
244
    for (ChildLbState child : getChildLbStates()) {
1✔
245
      if (child.getCurrentState() == READY) {
1✔
246
        activeChildren.add(child);
1✔
247
      }
248
    }
1✔
249
    return activeChildren;
1✔
250
  }
251

252
  /**
253
   * This represents the state of load balancer children.  Each endpoint (represented by an
254
   * EquivalentAddressGroup or EDS string) will have a separate ChildLbState which in turn will
255
   * have a single child LoadBalancer created from the provided factory.
256
   *
257
   * <p>A ChildLbStateHelper is the glue between ChildLbState and the helpers associated with the
258
   * petiole policy above and the PickFirstLoadBalancer's helper below.
259
   *
260
   * <p>If you wish to store additional state information related to each subchannel, then extend
261
   * this class.
262
   */
263
  public class ChildLbState {
264
    private final Object key;
265
    private ResolvedAddresses resolvedAddresses;
266

267
    private final LoadBalancer lb;
268
    private ConnectivityState currentState;
269
    private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult());
1✔
270

271
    public ChildLbState(Object key, LoadBalancer.Factory policyFactory) {
1✔
272
      this.key = key;
1✔
273
      this.lb = policyFactory.newLoadBalancer(createChildHelper());
1✔
274
      this.currentState = CONNECTING;
1✔
275
    }
1✔
276

277
    protected ChildLbStateHelper createChildHelper() {
278
      return new ChildLbStateHelper();
1✔
279
    }
280

281
    /**
282
     * Override for unique behavior such as delayed shutdowns of subchannels.
283
     */
284
    protected void shutdown() {
285
      lb.shutdown();
1✔
286
      this.currentState = SHUTDOWN;
1✔
287
      logger.log(Level.FINE, "Child balancer {0} deleted", key);
1✔
288
    }
1✔
289

290
    @Override
291
    public String toString() {
292
      return "Address = " + key
×
293
          + ", state = " + currentState
294
          + ", picker type: " + currentPicker.getClass()
×
295
          + ", lb: " + lb;
296
    }
297

298
    public final Object getKey() {
299
      return key;
1✔
300
    }
301

302
    @VisibleForTesting
303
    public final LoadBalancer getLb() {
304
      return lb;
1✔
305
    }
306

307
    @VisibleForTesting
308
    public final SubchannelPicker getCurrentPicker() {
309
      return currentPicker;
1✔
310
    }
311

312
    public final ConnectivityState getCurrentState() {
313
      return currentState;
1✔
314
    }
315

316
    protected final void setCurrentState(ConnectivityState newState) {
317
      currentState = newState;
1✔
318
    }
1✔
319

320
    protected final void setCurrentPicker(SubchannelPicker newPicker) {
321
      currentPicker = newPicker;
1✔
322
    }
1✔
323

324
    protected final void setResolvedAddresses(ResolvedAddresses newAddresses) {
325
      checkNotNull(newAddresses, "Missing address list for child");
1✔
326
      resolvedAddresses = newAddresses;
1✔
327
    }
1✔
328

329
    @VisibleForTesting
330
    public final ResolvedAddresses getResolvedAddresses() {
331
      return resolvedAddresses;
1✔
332
    }
333

334
    /**
335
     * ChildLbStateHelper is the glue between ChildLbState and the helpers associated with the
336
     * petiole policy above and the PickFirstLoadBalancer's helper below.
337
     *
338
     * <p>The ChildLbState updates happen during updateBalancingState.  Otherwise, it is doing
339
     * simple forwarding.
340
     */
341
    protected class ChildLbStateHelper extends ForwardingLoadBalancerHelper {
1✔
342

343
      /**
344
       * Update current state and picker for this child and then use
345
       * {@link #updateOverallBalancingState()} for the parent LB.
346
       */
347
      @Override
348
      public void updateBalancingState(final ConnectivityState newState,
349
          final SubchannelPicker newPicker) {
350
        if (currentState == SHUTDOWN) {
1✔
351
          return;
×
352
        }
353

354
        currentState = newState;
1✔
355
        currentPicker = newPicker;
1✔
356
        // If we are already in the process of resolving addresses, the overall balancing state
357
        // will be updated at the end of it, and we don't need to trigger that update here.
358
        if (!resolvingAddresses) {
1✔
359
          updateOverallBalancingState();
1✔
360
        }
361
      }
1✔
362

363
      @Override
364
      protected Helper delegate() {
365
        return helper;
1✔
366
      }
367
    }
368
  }
369

370
  /**
371
   * Endpoint is an optimization to quickly lookup and compare EquivalentAddressGroup address sets.
372
   * It ignores the attributes. Is used as a key for ChildLbState for most load balancers
373
   * (ClusterManagerLB uses a String).
374
   */
375
  protected static class Endpoint {
376
    final Collection<SocketAddress> addrs;
377
    final int hashCode;
378

379
    public Endpoint(EquivalentAddressGroup eag) {
1✔
380
      checkNotNull(eag, "eag");
1✔
381

382
      if (eag.getAddresses().size() < 10) {
1✔
383
        addrs = eag.getAddresses();
1✔
384
      } else {
385
        // This is expected to be very unlikely in practice
386
        addrs = new HashSet<>(eag.getAddresses());
1✔
387
      }
388
      int sum = 0;
1✔
389
      for (SocketAddress address : eag.getAddresses()) {
1✔
390
        sum += address.hashCode();
1✔
391
      }
1✔
392
      hashCode = sum;
1✔
393
    }
1✔
394

395
    @Override
396
    public int hashCode() {
397
      return hashCode;
1✔
398
    }
399

400
    @Override
401
    public boolean equals(Object other) {
402
      if (this == other) {
1✔
403
        return true;
1✔
404
      }
405

406
      if (!(other instanceof Endpoint)) {
1✔
407
        return false;
1✔
408
      }
409
      Endpoint o = (Endpoint) other;
1✔
410
      if (o.hashCode != hashCode || o.addrs.size() != addrs.size()) {
1✔
411
        return false;
1✔
412
      }
413

414
      return o.addrs.containsAll(addrs);
1✔
415
    }
416

417
    @Override
418
    public String toString() {
419
      return addrs.toString();
1✔
420
    }
421
  }
422

423
  protected static class AcceptResolvedAddrRetVal {
424
    public final Status status;
425
    public final List<ChildLbState> removedChildren;
426

427
    public AcceptResolvedAddrRetVal(Status status, List<ChildLbState> removedChildren) {
1✔
428
      this.status = status;
1✔
429
      this.removedChildren = removedChildren;
1✔
430
    }
1✔
431
  }
432
}
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

© 2026 Coveralls, Inc