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

grpc / grpc-java / #20278

14 May 2026 06:34AM UTC coverage: 88.827% (+0.008%) from 88.819%
#20278

push

github

web-flow
xds: Disable Priority LB child policy retention cache (#12806)

Implements gRFC A115:
https://github.com/grpc/proposal/blob/master/A115-remove-priority-lb-child-policy-cache.md

36262 of 40823 relevant lines covered (88.83%)

0.89 hits per line

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

98.85
/../xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java
1
/*
2
 * Copyright 2020 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.xds;
18

19
import static com.google.common.base.Preconditions.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.TRANSIENT_FAILURE;
24

25
import io.grpc.ConnectivityState;
26
import io.grpc.InternalLogId;
27
import io.grpc.LoadBalancer;
28
import io.grpc.Status;
29
import io.grpc.SynchronizationContext;
30
import io.grpc.SynchronizationContext.ScheduledHandle;
31
import io.grpc.internal.GrpcUtil;
32
import io.grpc.util.ForwardingLoadBalancerHelper;
33
import io.grpc.util.GracefulSwitchLoadBalancer;
34
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
35
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig;
36
import io.grpc.xds.client.XdsLogger;
37
import io.grpc.xds.client.XdsLogger.XdsLogLevel;
38
import java.util.ArrayList;
39
import java.util.Collection;
40
import java.util.HashMap;
41
import java.util.HashSet;
42
import java.util.List;
43
import java.util.Map;
44
import java.util.Objects;
45
import java.util.Set;
46
import java.util.concurrent.ScheduledExecutorService;
47
import java.util.concurrent.TimeUnit;
48
import javax.annotation.Nullable;
49

50
/**
51
 * Load balancer for priority policy. A <em>priority</em> represents a logical entity within a
52
 * cluster for load balancing purposes.
53
 */
54
final class PriorityLoadBalancer extends LoadBalancer {
55
  private final Helper helper;
56
  private final SynchronizationContext syncContext;
57
  private final ScheduledExecutorService executor;
58
  private final XdsLogger logger;
59

60
  // Includes all active and deactivated children. Mutable. New entries are only added from priority
61
  // 0 up to the selected priority. An entry is only deleted 15 minutes after its deactivation.
62
  // Note that calling into a child can cause the child to call back into the LB policy and modify
63
  // the map.  Therefore copy values before looping over them.
64
  private final Map<String, ChildLbState> children = new HashMap<>();
1✔
65

66
  // Following fields are only null initially.
67
  private ResolvedAddresses resolvedAddresses;
68
  // List of priority names in order.
69
  private List<String> priorityNames;
70
  // Config for each priority.
71
  private Map<String, PriorityChildConfig> priorityConfigs;
72
  @Nullable private String currentPriority;
73
  private ConnectivityState currentConnectivityState;
74
  private SubchannelPicker currentPicker;
75
  // Set to true if currently in the process of handling resolved addresses.
76
  private boolean handlingResolvedAddresses;
77
  static boolean enablePriorityLbChildPolicyCache =
1✔
78
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE", false);
1✔
79

80
  PriorityLoadBalancer(Helper helper) {
1✔
81
    this.helper = checkNotNull(helper, "helper");
1✔
82
    syncContext = helper.getSynchronizationContext();
1✔
83
    executor = helper.getScheduledExecutorService();
1✔
84
    InternalLogId logId = InternalLogId.allocate("priority-lb", helper.getAuthority());
1✔
85
    logger = XdsLogger.withLogId(logId);
1✔
86
    logger.log(XdsLogLevel.INFO, "Created");
1✔
87
  }
1✔
88

89
  @Override
90
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
91
    logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
1✔
92
    this.resolvedAddresses = resolvedAddresses;
1✔
93
    PriorityLbConfig config = (PriorityLbConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
94
    checkNotNull(config, "missing priority lb config");
1✔
95
    priorityNames = config.priorities;
1✔
96
    priorityConfigs = config.childConfigs;
1✔
97
    Status status = Status.OK;
1✔
98
    Set<String> prioritySet = new HashSet<>(config.priorities);
1✔
99
    ArrayList<String> childKeys = new ArrayList<>(children.keySet());
1✔
100
    for (String priority : childKeys) {
1✔
101
      if (!prioritySet.contains(priority)) {
1✔
102
        ChildLbState childLbState = children.get(priority);
1✔
103
        if (childLbState != null) {
1✔
104
          if (enablePriorityLbChildPolicyCache) {
1✔
105
            childLbState.deactivate();
1✔
106
          } else {
107
            childLbState.tearDown();
1✔
108
            children.remove(priority);
1✔
109
          }
110
        }
111
      }
112
    }
1✔
113
    handlingResolvedAddresses = true;
1✔
114
    for (String priority : priorityNames) {
1✔
115
      ChildLbState childLbState = children.get(priority);
1✔
116
      if (childLbState != null) {
1✔
117
        Status newStatus = childLbState.updateResolvedAddresses();
1✔
118
        if (!newStatus.isOk()) {
1✔
119
          status = newStatus;
1✔
120
        }
121
      }
122
    }
1✔
123
    handlingResolvedAddresses = false;
1✔
124
    Status newStatus = tryNextPriority();
1✔
125
    if (!newStatus.isOk()) {
1✔
126
      status = newStatus;
1✔
127
    }
128
    return status;
1✔
129
  }
130

131
  @Override
132
  public void handleNameResolutionError(Status error) {
133
    logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error);
1✔
134
    boolean gotoTransientFailure = true;
1✔
135
    Collection<ChildLbState> childValues = new ArrayList<>(children.values());
1✔
136
    for (ChildLbState child : childValues) {
1✔
137
      if (priorityNames.contains(child.priority)) {
1✔
138
        child.lb.handleNameResolutionError(error);
1✔
139
        gotoTransientFailure = false;
1✔
140
      }
141
    }
1✔
142
    if (gotoTransientFailure) {
1✔
143
      updateOverallState(
×
144
          null, TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
×
145
    }
146
  }
1✔
147

148
  @Override
149
  public void shutdown() {
150
    logger.log(XdsLogLevel.INFO, "Shutdown");
1✔
151
    Collection<ChildLbState> childValues = new ArrayList<>(children.values());
1✔
152
    for (ChildLbState child : childValues) {
1✔
153
      child.tearDown();
1✔
154
    }
1✔
155
    children.clear();
1✔
156
  }
1✔
157

158
  private Status tryNextPriority() {
159
    for (int i = 0; i < priorityNames.size(); i++) {
1✔
160
      String priority = priorityNames.get(i);
1✔
161
      if (!children.containsKey(priority)) {
1✔
162
        ChildLbState child =
1✔
163
            new ChildLbState(priority, priorityConfigs.get(priority).ignoreReresolution);
1✔
164
        children.put(priority, child);
1✔
165
        // Child is created in CONNECTING with pending failOverTimer
166
        updateOverallState(priority, child.connectivityState, child.picker);
1✔
167
        // Calling the child's updateResolvedAddresses() can result in tryNextPriority() being
168
        // called recursively. We need to be sure to be done with processing here before it is
169
        // called.
170
        return child.updateResolvedAddresses(); // Give priority i time to connect.
1✔
171
      }
172
      ChildLbState child = children.get(priority);
1✔
173
      child.reactivate();
1✔
174
      if (child.connectivityState.equals(READY) || child.connectivityState.equals(IDLE)) {
1✔
175
        logger.log(XdsLogLevel.DEBUG, "Shifted to priority {0}", priority);
1✔
176
        updateOverallState(priority, child.connectivityState, child.picker);
1✔
177
        for (int j = i + 1; j < priorityNames.size(); j++) {
1✔
178
          String p = priorityNames.get(j);
1✔
179
          if (children.containsKey(p)) {
1✔
180
            children.get(p).deactivate();
1✔
181
          }
182
        }
183
        return Status.OK;
1✔
184
      }
185
      if (child.failOverTimer.isPending()) {
1✔
186
        updateOverallState(priority, child.connectivityState, child.picker);
1✔
187
        return Status.OK; // Give priority i time to connect.
1✔
188
      }
189
    }
190
    for (int i = 0; i < priorityNames.size(); i++) {
1✔
191
      String priority = priorityNames.get(i);
1✔
192
      ChildLbState child = children.get(priority);
1✔
193
      if (child.connectivityState.equals(CONNECTING)) {
1✔
194
        updateOverallState(priority, child.connectivityState, child.picker);
1✔
195
        return Status.OK;
1✔
196
      }
197
    }
198
    logger.log(XdsLogLevel.DEBUG, "All priority failed");
1✔
199
    String lastPriority = priorityNames.get(priorityNames.size() - 1);
1✔
200
    ChildLbState child = children.get(lastPriority);
1✔
201
    updateOverallState(lastPriority, child.connectivityState, child.picker);
1✔
202
    return Status.OK;
1✔
203
  }
204

205
  private void updateOverallState(
206
      @Nullable String priority, ConnectivityState state, SubchannelPicker picker) {
207
    if (!Objects.equals(priority, currentPriority) || !state.equals(currentConnectivityState)
1✔
208
        || !picker.equals(currentPicker)) {
1✔
209
      currentPriority = priority;
1✔
210
      currentConnectivityState = state;
1✔
211
      currentPicker = picker;
1✔
212
      helper.updateBalancingState(state, picker);
1✔
213
    }
214
  }
1✔
215

216
  private final class ChildLbState {
217
    final String priority;
218
    final ChildHelper childHelper;
219
    final GracefulSwitchLoadBalancer lb;
220
    // Timer to fail over to the next priority if not connected in 10 sec. Scheduled only once at
221
    // child initialization.
222
    ScheduledHandle failOverTimer;
223
    boolean seenReadyOrIdleSinceTransientFailure = false;
1✔
224
    // Timer to delay shutdown and deletion of the priority. Scheduled whenever the child is
225
    // deactivated.
226
    @Nullable ScheduledHandle deletionTimer;
227
    ConnectivityState connectivityState = CONNECTING;
1✔
228
    SubchannelPicker picker = new FixedResultPicker(PickResult.withNoResult());
1✔
229

230
    ChildLbState(final String priority, boolean ignoreReresolution) {
1✔
231
      this.priority = priority;
1✔
232
      childHelper = new ChildHelper(ignoreReresolution);
1✔
233
      lb = new GracefulSwitchLoadBalancer(childHelper);
1✔
234
      failOverTimer = syncContext.schedule(new FailOverTask(), 10, TimeUnit.SECONDS, executor);
1✔
235
      logger.log(XdsLogLevel.DEBUG, "Priority created: {0}", priority);
1✔
236
    }
1✔
237

238
    final class FailOverTask implements Runnable {
1✔
239
      @Override
240
      public void run() {
241
        if (deletionTimer != null && deletionTimer.isPending()) {
1✔
242
          // The child is deactivated.
243
          return;
1✔
244
        }
245
        logger.log(XdsLogLevel.DEBUG, "Priority {0} failed over to next", priority);
1✔
246
        Status status = tryNextPriority();
1✔
247
        if (!status.isOk()) {
1✔
248
          // A child had a problem with the addresses/config. Request it to be refreshed
249
          helper.refreshNameResolution();
1✔
250
        }
251
      }
1✔
252
    }
253

254
    /**
255
     * Called when the child becomes a priority that is or appears before the first READY one in the
256
     * {@code priorities} list, due to either config update or balancing state update.
257
     */
258
    void reactivate() {
259
      if (deletionTimer != null && deletionTimer.isPending()) {
1✔
260
        deletionTimer.cancel();
1✔
261
        logger.log(XdsLogLevel.DEBUG, "Priority reactivated: {0}", priority);
1✔
262
      }
263
    }
1✔
264

265
    /**
266
     * Called when either the child is removed by config update, or a higher priority becomes READY.
267
     */
268
    void deactivate() {
269
      if (deletionTimer != null && deletionTimer.isPending()) {
1✔
270
        return;
1✔
271
      }
272

273
      class DeletionTask implements Runnable {
1✔
274
        @Override
275
        public void run() {
276
          tearDown();
1✔
277
          children.remove(priority);
1✔
278
        }
1✔
279
      }
280

281
      deletionTimer = syncContext.schedule(new DeletionTask(), 15, TimeUnit.MINUTES, executor);
1✔
282
      logger.log(XdsLogLevel.DEBUG, "Priority deactivated: {0}", priority);
1✔
283
    }
1✔
284

285
    void tearDown() {
286
      if (failOverTimer.isPending()) {
1✔
287
        failOverTimer.cancel();
1✔
288
      }
289
      if (deletionTimer != null && deletionTimer.isPending()) {
1✔
290
        deletionTimer.cancel();
1✔
291
      }
292
      lb.shutdown();
1✔
293
      logger.log(XdsLogLevel.DEBUG, "Priority deleted: {0}", priority);
1✔
294
    }
1✔
295

296
    /**
297
     * Called either when the child is just created and in this case updated with the cached {@code
298
     * resolvedAddresses}, or when priority lb receives a new resolved addresses while the child
299
     * already exists.
300
     */
301
    Status updateResolvedAddresses() {
302
      PriorityLbConfig config =
1✔
303
          (PriorityLbConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1✔
304
      return lb.acceptResolvedAddresses(
1✔
305
          resolvedAddresses.toBuilder()
1✔
306
              .setAddresses(AddressFilter.filter(resolvedAddresses.getAddresses(), priority))
1✔
307
              .setLoadBalancingPolicyConfig(config.childConfigs.get(priority).childConfig)
1✔
308
              .build());
1✔
309
    }
310

311
    final class ChildHelper extends ForwardingLoadBalancerHelper {
312
      private final boolean ignoreReresolution;
313

314
      ChildHelper(boolean ignoreReresolution) {
1✔
315
        this.ignoreReresolution = ignoreReresolution;
1✔
316
      }
1✔
317

318
      @Override
319
      public void refreshNameResolution() {
320
        if (!ignoreReresolution) {
1✔
321
          delegate().refreshNameResolution();
1✔
322
        }
323
      }
1✔
324

325
      @Override
326
      public void updateBalancingState(final ConnectivityState newState,
327
          final SubchannelPicker newPicker) {
328
        if (!children.containsKey(priority)) {
1✔
329
          return;
1✔
330
        }
331
        ConnectivityState oldState = connectivityState;
1✔
332
        connectivityState = newState;
1✔
333
        picker = newPicker;
1✔
334

335
        if (deletionTimer != null && deletionTimer.isPending()) {
1✔
336
          return;
1✔
337
        }
338
        if (newState.equals(CONNECTING) && !oldState.equals(newState)) {
1✔
339
          if (!failOverTimer.isPending() && seenReadyOrIdleSinceTransientFailure) {
1✔
340
            failOverTimer = syncContext.schedule(new FailOverTask(), 10, TimeUnit.SECONDS,
1✔
341
                executor);
1✔
342
          }
343
        } else if (newState.equals(READY) || newState.equals(IDLE)) {
1✔
344
          seenReadyOrIdleSinceTransientFailure = true;
1✔
345
          failOverTimer.cancel();
1✔
346
        } else if (newState.equals(TRANSIENT_FAILURE)) {
1✔
347
          seenReadyOrIdleSinceTransientFailure = false;
1✔
348
          failOverTimer.cancel();
1✔
349
        }
350

351
        // If we are currently handling newly resolved addresses, let's not try to reconfigure as
352
        // the address handling process will take care of that to provide an atomic config update.
353
        if (!handlingResolvedAddresses) {
1✔
354
          Status status = tryNextPriority();
1✔
355
          if (!status.isOk()) {
1✔
356
            // A child had a problem with the addresses/config. Request it to be refreshed
357
            helper.refreshNameResolution();
1✔
358
          }
359
        }
360
      }
1✔
361

362
      @Override
363
      protected Helper delegate() {
364
        return helper;
1✔
365
      }
366
    }
367
  }
368
}
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