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

grpc / grpc-java / #19489

02 Oct 2024 06:03PM UTC coverage: 84.589% (+0.002%) from 84.587%
#19489

push

github

web-flow
util: Store only a list of children in MultiChildLB

A map of children is still needed, but is created temporarily on update.
The order of children is currently preserved, but we could use regular
HashMaps if that is not useful.

33663 of 39796 relevant lines covered (84.59%)

0.85 hits per line

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

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

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

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

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

64
  protected ConnectivityState currentConnectivityState;
65

66

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

240
  @VisibleForTesting
241
  public final ChildLbState getChildLbState(Object key) {
242
    for (ChildLbState state : childLbStates) {
1✔
243
      if (Objects.equal(state.getKey(), key)) {
1✔
244
        return state;
1✔
245
      }
246
    }
1✔
247
    return null;
×
248
  }
249

250
  @VisibleForTesting
251
  public final ChildLbState getChildLbStateEag(EquivalentAddressGroup eag) {
252
    return getChildLbState(new Endpoint(eag));
1✔
253
  }
254

255
  /**
256
   * Filters out non-ready child load balancers (subchannels).
257
   */
258
  protected final List<ChildLbState> getReadyChildren() {
259
    List<ChildLbState> activeChildren = new ArrayList<>();
1✔
260
    for (ChildLbState child : getChildLbStates()) {
1✔
261
      if (child.getCurrentState() == READY) {
1✔
262
        activeChildren.add(child);
1✔
263
      }
264
    }
1✔
265
    return activeChildren;
1✔
266
  }
267

268
  /**
269
   * This represents the state of load balancer children.  Each endpoint (represented by an
270
   * EquivalentAddressGroup or EDS string) will have a separate ChildLbState which in turn will
271
   * have a single child LoadBalancer created from the provided factory.
272
   *
273
   * <p>A ChildLbStateHelper is the glue between ChildLbState and the helpers associated with the
274
   * petiole policy above and the PickFirstLoadBalancer's helper below.
275
   *
276
   * <p>If you wish to store additional state information related to each subchannel, then extend
277
   * this class.
278
   */
279
  public class ChildLbState {
280
    private final Object key;
281
    private ResolvedAddresses resolvedAddresses;
282

283
    private final LoadBalancer lb;
284
    private ConnectivityState currentState;
285
    private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult());
1✔
286

287
    public ChildLbState(Object key, LoadBalancer.Factory policyFactory) {
1✔
288
      this.key = key;
1✔
289
      this.lb = policyFactory.newLoadBalancer(createChildHelper());
1✔
290
      this.currentState = CONNECTING;
1✔
291
    }
1✔
292

293
    protected ChildLbStateHelper createChildHelper() {
294
      return new ChildLbStateHelper();
1✔
295
    }
296

297
    /**
298
     * Override for unique behavior such as delayed shutdowns of subchannels.
299
     */
300
    protected void shutdown() {
301
      lb.shutdown();
1✔
302
      this.currentState = SHUTDOWN;
1✔
303
      logger.log(Level.FINE, "Child balancer {0} deleted", key);
1✔
304
    }
1✔
305

306
    @Override
307
    public String toString() {
308
      return "Address = " + key
×
309
          + ", state = " + currentState
310
          + ", picker type: " + currentPicker.getClass()
×
311
          + ", lb: " + lb;
312
    }
313

314
    public final Object getKey() {
315
      return key;
1✔
316
    }
317

318
    @VisibleForTesting
319
    public final LoadBalancer getLb() {
320
      return lb;
1✔
321
    }
322

323
    @VisibleForTesting
324
    public final SubchannelPicker getCurrentPicker() {
325
      return currentPicker;
1✔
326
    }
327

328
    public final ConnectivityState getCurrentState() {
329
      return currentState;
1✔
330
    }
331

332
    protected final void setCurrentState(ConnectivityState newState) {
333
      currentState = newState;
1✔
334
    }
1✔
335

336
    protected final void setCurrentPicker(SubchannelPicker newPicker) {
337
      currentPicker = newPicker;
1✔
338
    }
1✔
339

340
    public final EquivalentAddressGroup getEag() {
341
      if (resolvedAddresses == null || resolvedAddresses.getAddresses().isEmpty()) {
1✔
342
        return null;
×
343
      }
344
      return resolvedAddresses.getAddresses().get(0);
1✔
345
    }
346

347
    protected final void setResolvedAddresses(ResolvedAddresses newAddresses) {
348
      checkNotNull(newAddresses, "Missing address list for child");
1✔
349
      resolvedAddresses = newAddresses;
1✔
350
    }
1✔
351

352
    @VisibleForTesting
353
    public final ResolvedAddresses getResolvedAddresses() {
354
      return resolvedAddresses;
1✔
355
    }
356

357
    /**
358
     * ChildLbStateHelper is the glue between ChildLbState and the helpers associated with the
359
     * petiole policy above and the PickFirstLoadBalancer's helper below.
360
     *
361
     * <p>The ChildLbState updates happen during updateBalancingState.  Otherwise, it is doing
362
     * simple forwarding.
363
     */
364
    protected class ChildLbStateHelper extends ForwardingLoadBalancerHelper {
1✔
365

366
      /**
367
       * Update current state and picker for this child and then use
368
       * {@link #updateOverallBalancingState()} for the parent LB.
369
       */
370
      @Override
371
      public void updateBalancingState(final ConnectivityState newState,
372
          final SubchannelPicker newPicker) {
373
        if (currentState == SHUTDOWN) {
1✔
374
          return;
×
375
        }
376

377
        currentState = newState;
1✔
378
        currentPicker = newPicker;
1✔
379
        // If we are already in the process of resolving addresses, the overall balancing state
380
        // will be updated at the end of it, and we don't need to trigger that update here.
381
        if (!resolvingAddresses) {
1✔
382
          updateOverallBalancingState();
1✔
383
        }
384
      }
1✔
385

386
      @Override
387
      protected Helper delegate() {
388
        return helper;
1✔
389
      }
390
    }
391
  }
392

393
  /**
394
   * Endpoint is an optimization to quickly lookup and compare EquivalentAddressGroup address sets.
395
   * It ignores the attributes. Is used as a key for ChildLbState for most load balancers
396
   * (ClusterManagerLB uses a String).
397
   */
398
  protected static class Endpoint {
399
    final Collection<SocketAddress> addrs;
400
    final int hashCode;
401

402
    public Endpoint(EquivalentAddressGroup eag) {
1✔
403
      checkNotNull(eag, "eag");
1✔
404

405
      if (eag.getAddresses().size() < 10) {
1✔
406
        addrs = eag.getAddresses();
1✔
407
      } else {
408
        // This is expected to be very unlikely in practice
409
        addrs = new HashSet<>(eag.getAddresses());
1✔
410
      }
411
      int sum = 0;
1✔
412
      for (SocketAddress address : eag.getAddresses()) {
1✔
413
        sum += address.hashCode();
1✔
414
      }
1✔
415
      hashCode = sum;
1✔
416
    }
1✔
417

418
    @Override
419
    public int hashCode() {
420
      return hashCode;
1✔
421
    }
422

423
    @Override
424
    public boolean equals(Object other) {
425
      if (this == other) {
1✔
426
        return true;
1✔
427
      }
428

429
      if (!(other instanceof Endpoint)) {
1✔
430
        return false;
1✔
431
      }
432
      Endpoint o = (Endpoint) other;
1✔
433
      if (o.hashCode != hashCode || o.addrs.size() != addrs.size()) {
1✔
434
        return false;
1✔
435
      }
436

437
      return o.addrs.containsAll(addrs);
1✔
438
    }
439

440
    @Override
441
    public String toString() {
442
      return addrs.toString();
1✔
443
    }
444
  }
445

446
  protected static class AcceptResolvedAddrRetVal {
447
    public final Status status;
448
    public final List<ChildLbState> removedChildren;
449

450
    public AcceptResolvedAddrRetVal(Status status, List<ChildLbState> removedChildren) {
1✔
451
      this.status = status;
1✔
452
      this.removedChildren = removedChildren;
1✔
453
    }
1✔
454
  }
455
}
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