• 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

99.49
/../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(
1✔
144
          null, TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error)));
1✔
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(
1✔
229
        PickResult.withNoResult("connecting", "priority child state uninitialized"));
1✔
230

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

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

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

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

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

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

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

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

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

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

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

326
      @Override
327
      public void updateBalancingState(final ConnectivityState newState,
328
          final SubchannelPicker newPicker) {
329
        if (!children.containsKey(priority)) {
1✔
330
          return;
1✔
331
        }
332
        ConnectivityState oldState = connectivityState;
1✔
333
        connectivityState = newState;
1✔
334
        if (newState == CONNECTING || newState == IDLE) {
1✔
335
          picker = new PriorityPicker(newPicker, priority);
1✔
336
        } else {
337
          picker = newPicker;
1✔
338
        }
339

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

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

367
      @Override
368
      protected Helper delegate() {
369
        return helper;
1✔
370
      }
371
    }
372
  }
373

374
  private static final class PriorityPicker extends SubchannelPicker {
375
    private final SubchannelPicker delegate;
376
    private final String priority;
377

378
    PriorityPicker(SubchannelPicker delegate, String priority) {
1✔
379
      this.delegate = checkNotNull(delegate, "delegate");
1✔
380
      this.priority = checkNotNull(priority, "priority");
1✔
381
    }
1✔
382

383
    @Override
384
    public PickResult pickSubchannel(PickSubchannelArgs args) {
385
      PickResult childResult = delegate.pickSubchannel(args);
1✔
386
      if (!childResult.hasResult() && childResult.getDelayType() != null) {
1✔
387
        String childReason = childResult.getDelayReason();
1✔
388
        String composedType = priority + ":" + childResult.getDelayType();
1✔
389
        String reason = "waiting on priority group " + priority + " ("
1✔
390
            + (childReason != null ? childReason : "connecting") + ")";
1✔
391
        return PickResult.withNoResult(composedType, reason);
1✔
392
      }
393
      return childResult;
1✔
394
    }
395

396
    @Override
397
    public boolean equals(Object o) {
398
      if (this == o) {
1✔
399
        return true;
1✔
400
      }
401
      if (o == null || getClass() != o.getClass()) {
1✔
402
        return false;
1✔
403
      }
404
      PriorityPicker that = (PriorityPicker) o;
1✔
405
      return delegate.equals(that.delegate) && priority.equals(that.priority);
1✔
406
    }
407

408
    @Override
409
    public int hashCode() {
410
      return Objects.hash(delegate, priority);
×
411
    }
412
  }
413
}
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