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

grpc / grpc-java / #20526

13 Oct 2026 04:06PM UTC coverage: 89.426% (-0.02%) from 89.449%
#20526

push

github

web-flow
core: Enable the new Pick First Leaf Loadbalancer (#13114)

39344 of 43996 relevant lines covered (89.43%)

0.89 hits per line

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

96.6
/../util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java
1
/*
2
 * Copyright 2022 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.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static com.google.common.base.Preconditions.checkState;
22
import static java.util.concurrent.TimeUnit.NANOSECONDS;
23

24
import com.google.common.annotations.VisibleForTesting;
25
import com.google.common.base.Ticker;
26
import com.google.common.collect.ForwardingMap;
27
import com.google.common.collect.ImmutableList;
28
import com.google.common.collect.ImmutableSet;
29
import io.grpc.Attributes;
30
import io.grpc.ChannelLogger;
31
import io.grpc.ChannelLogger.ChannelLogLevel;
32
import io.grpc.ClientStreamTracer;
33
import io.grpc.ClientStreamTracer.StreamInfo;
34
import io.grpc.ConnectivityState;
35
import io.grpc.ConnectivityStateInfo;
36
import io.grpc.EquivalentAddressGroup;
37
import io.grpc.Internal;
38
import io.grpc.LoadBalancer;
39
import io.grpc.Metadata;
40
import io.grpc.Status;
41
import io.grpc.SynchronizationContext;
42
import io.grpc.SynchronizationContext.ScheduledHandle;
43
import java.net.SocketAddress;
44
import java.util.ArrayList;
45
import java.util.Collection;
46
import java.util.HashMap;
47
import java.util.HashSet;
48
import java.util.List;
49
import java.util.Map;
50
import java.util.Random;
51
import java.util.Set;
52
import java.util.concurrent.ScheduledExecutorService;
53
import java.util.concurrent.atomic.AtomicLong;
54
import javax.annotation.Nullable;
55

56
/**
57
 * Wraps a child {@code LoadBalancer} while monitoring for outlier backends and removing them from
58
 * the use of the child LB.
59
 *
60
 * <p>This implements the outlier detection gRFC:
61
 * https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md
62
 *
63
 * <p>The implementation maintains two maps. Each endpoint status is tracked using an
64
 * EndpointTracker. E.g. for two endpoints with these address list and their tracker:
65
 * Endpoint e1 : [a1, a2] is tracked with EndpointTracker t1
66
 * Endpoint e2 : [a3] is tracked with EndpointTracker t2
67
 * The two maps are:
68
 * First, addressMap maps from socket address -> endpoint tracker : [a1 -> t1, a2 -> t1, a3 -> t2]
69
 * EndpointTracker has reference to all the subchannels of the corresponding endpoint.
70
 * Second, trackerMap maps from unordered address set -> endpoint tracker.
71
 * Updated upon address updates.
72
 */
73
@Internal
74
public final class OutlierDetectionLoadBalancer extends LoadBalancer {
75

76
  @VisibleForTesting
77
  final EndpointTrackerMap endpointTrackerMap;
78

79
  @VisibleForTesting
1 ✔
80
  final Map<SocketAddress, EndpointTracker> addressMap = new HashMap<>();
81

82
  private final SynchronizationContext syncContext;
83
  private final Helper childHelper;
84
  private final GracefulSwitchLoadBalancer switchLb;
85
  private Ticker ticker;
86
  private final ScheduledExecutorService timeService;
87
  private ScheduledHandle detectionTimerHandle;
88
  private Long detectionTimerStartNanos;
89

90
  private final ChannelLogger logger;
91

92
  private static final Attributes.Key<EndpointTracker> ENDPOINT_TRACKER_KEY
1 ✔
93
      = Attributes.Key.create("endpointTrackerKey");
1 ✔
94

95
  /**
96
   * Creates a new instance of {@link OutlierDetectionLoadBalancer}.
97
   */
98
  public OutlierDetectionLoadBalancer(Helper helper, Ticker ticker) {
1 ✔
99
    logger = helper.getChannelLogger();
1 ✔
100
    childHelper = new ChildHelper(checkNotNull(helper, "helper"));
1 ✔
101
    switchLb = new GracefulSwitchLoadBalancer(childHelper);
1 ✔
102
    endpointTrackerMap = new EndpointTrackerMap();
1 ✔
103
    this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext");
1 ✔
104
    this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService");
1 ✔
105
    this.ticker = ticker;
1 ✔
106
    logger.log(ChannelLogLevel.DEBUG, "OutlierDetection lb created.");
1 ✔
107
  }
1 ✔
108

109
  @Override
110
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
111
    logger.log(ChannelLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
1 ✔
112
    OutlierDetectionLoadBalancerConfig config
1 ✔
113
        = (OutlierDetectionLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
1 ✔
114

115
    // The map should only retain entries for endpoints in this latest update.
116
    Set<Set<SocketAddress>> endpoints = new HashSet<>();
1 ✔
117
    Map<SocketAddress, Set<SocketAddress>> addressEndpointMap = new HashMap<>();
1 ✔
118
    for (EquivalentAddressGroup addressGroup : resolvedAddresses.getAddresses()) {
1 ✔
119
      Set<SocketAddress> endpoint = ImmutableSet.copyOf(addressGroup.getAddresses());
1 ✔
120
      endpoints.add(endpoint);
1 ✔
121
      for (SocketAddress address : addressGroup.getAddresses()) {
1 ✔
122
        if (addressEndpointMap.containsKey(address)) {
1 ✔
123
          logger.log(ChannelLogLevel.WARNING,
×
124
              "Unexpected duplicated address {0} belongs to multiple endpoints", address);
125
        }
126
        addressEndpointMap.put(address, endpoint);
1 ✔
127
      }
1 ✔
128
    }
1 ✔
129
    endpointTrackerMap.keySet().retainAll(endpoints);
1 ✔
130

131
    endpointTrackerMap.updateTrackerConfigs(config);
1 ✔
132

133
    // Add any new ones.
134
    endpointTrackerMap.putNewTrackers(config, endpoints);
1 ✔
135

136
    // Update address -> tracker map.
137
    addressMap.clear();
1 ✔
138
    for (Map.Entry<SocketAddress, Set<SocketAddress>> e : addressEndpointMap.entrySet()) {
1 ✔
139
      addressMap.put(e.getKey(), endpointTrackerMap.get(e.getValue()));
1 ✔
140
    }
1 ✔
141

142
    // If outlier detection is actually configured, start a timer that will periodically try to
143
    // detect outliers.
144
    if (config.outlierDetectionEnabled()) {
1 ✔
145
      long initialDelayNanos;
146

147
      if (detectionTimerStartNanos == null) {
1 ✔
148
        // On the first go we use the configured interval.
149
        initialDelayNanos = config.intervalNanos;
1 ✔
150
      } else {
151
        // If a timer has started earlier we cancel it and use the difference between the start
152
        // time and now as the interval.
153
        initialDelayNanos = Math.max(0L,
1 ✔
154
            config.intervalNanos - (ticker.read() - detectionTimerStartNanos));
1 ✔
155
      }
156

157
      // If a timer has been previously created we need to cancel it and reset all the call counters
158
      // for a fresh start.
159
      if (detectionTimerHandle != null) {
1 ✔
160
        detectionTimerHandle.cancel();
1 ✔
161
        endpointTrackerMap.resetCallCounters();
1 ✔
162
      }
163

164
      detectionTimerHandle = syncContext.scheduleWithFixedDelay(new DetectionTimer(config, logger),
1 ✔
165
          initialDelayNanos, config.intervalNanos, NANOSECONDS, timeService);
166
    } else if (detectionTimerHandle != null) {
1 ✔
167
      // Outlier detection is not configured, but we have a lingering timer. Let's cancel it and
168
      // uneject any addresses we may have ejected.
169
      detectionTimerHandle.cancel();
1 ✔
170
      detectionTimerStartNanos = null;
1 ✔
171
      endpointTrackerMap.cancelTracking();
1 ✔
172
    }
173

174
    return switchLb.acceptResolvedAddresses(
1 ✔
175
        resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config.childConfig).build());
1 ✔
176
  }
177

178
  @Override
179
  public void handleNameResolutionError(Status error) {
180
    switchLb.handleNameResolutionError(error);
1 ✔
181
  }
1 ✔
182

183
  @Override
184
  public void shutdown() {
185
    switchLb.shutdown();
1 ✔
186
  }
1 ✔
187

188
  /**
189
   * This timer will be invoked periodically, according to configuration, and it will look for any
190
   * outlier subchannels.
191
   */
192
  final class DetectionTimer implements Runnable {
193

194
    OutlierDetectionLoadBalancerConfig config;
195
    ChannelLogger logger;
196

197
    DetectionTimer(OutlierDetectionLoadBalancerConfig config, ChannelLogger logger) {
1 ✔
198
      this.config = config;
1 ✔
199
      this.logger = logger;
1 ✔
200
    }
1 ✔
201

202
    @Override
203
    public void run() {
204
      detectionTimerStartNanos = ticker.read();
1 ✔
205

206
      endpointTrackerMap.swapCounters();
1 ✔
207

208
      for (OutlierEjectionAlgorithm algo : OutlierEjectionAlgorithm.forConfig(config, logger)) {
1 ✔
209
        algo.ejectOutliers(endpointTrackerMap, detectionTimerStartNanos);
1 ✔
210
      }
1 ✔
211

212
      endpointTrackerMap.maybeUnejectOutliers(detectionTimerStartNanos);
1 ✔
213
    }
1 ✔
214
  }
215

216
  /**
217
   * This child helper wraps the provided helper so that it can hand out wrapped {@link
218
   * OutlierDetectionSubchannel}s and manage the address info map.
219
   */
220
  final class ChildHelper extends ForwardingLoadBalancerHelper {
221

222
    private Helper delegate;
223

224
    ChildHelper(Helper delegate) {
1 ✔
225
      this.delegate = new HealthProducerHelper(delegate);
1 ✔
226
    }
1 ✔
227

228
    @Override
229
    protected Helper delegate() {
230
      return delegate;
×
231
    }
232

233
    @Override
234
    public Subchannel createSubchannel(CreateSubchannelArgs args) {
235
      // Subchannels are wrapped so that we can monitor call results and to trigger failures when
236
      // we decide to eject the subchannel.
237
      OutlierDetectionSubchannel subchannel = new OutlierDetectionSubchannel(args, delegate);
1 ✔
238

239
      // If the subchannel is associated with a single address that is also already in the map
240
      // the subchannel will be added to the map and be included in outlier detection.
241
      List<EquivalentAddressGroup> addressGroups = args.getAddresses();
1 ✔
242
      if (hasSingleAddress(addressGroups)
1 ✔
243
          && addressMap.containsKey(addressGroups.get(0).getAddresses().get(0))) {
1 ✔
244
        EndpointTracker tracker = addressMap.get(addressGroups.get(0).getAddresses().get(0));
1 ✔
245
        tracker.addSubchannel(subchannel);
1 ✔
246

247
        // If this address has already been ejected, we need to immediately eject this Subchannel.
248
        if (tracker.ejectionTimeNanos != null) {
1 ✔
249
          subchannel.eject();
×
250
        }
251
      }
252

253
      return subchannel;
1 ✔
254
    }
255

256
    @Override
257
    public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
258
      delegate.updateBalancingState(newState, new OutlierDetectionPicker(newPicker));
1 ✔
259
    }
1 ✔
260
  }
261

262
  final class OutlierDetectionSubchannel extends ForwardingSubchannel {
263

264
    private final Subchannel delegate;
265
    private EndpointTracker endpointTracker;
266
    private boolean ejected;
267
    private ConnectivityStateInfo lastSubchannelState;
268

269
    // In the new pick first: created at construction, delegates to health consumer listener;
270
    // In th old pick first: created at subchannel.start(), delegates to subchannel state listener.
271
    private SubchannelStateListener subchannelStateListener;
272
    private final ChannelLogger logger;
273

274
    OutlierDetectionSubchannel(CreateSubchannelArgs args, Helper helper) {
1 ✔
275
      LoadBalancer.SubchannelStateListener healthConsumerListener =
1 ✔
276
          args.getOption(HEALTH_CONSUMER_LISTENER_ARG_KEY);
1 ✔
277
      if (healthConsumerListener != null) {
1 ✔
278
        this.subchannelStateListener = healthConsumerListener;
1 ✔
279
        SubchannelStateListener upstreamListener =
1 ✔
280
            new OutlierDetectionSubchannelStateListener(healthConsumerListener);
281
        this.delegate = helper.createSubchannel(args.toBuilder()
1 ✔
282
             .addOption(HEALTH_CONSUMER_LISTENER_ARG_KEY, upstreamListener).build());
1 ✔
283
      } else {
1 ✔
284
        this.delegate = helper.createSubchannel(args);
1 ✔
285
      }
286
      this.logger = delegate.getChannelLogger();
1 ✔
287
    }
1 ✔
288

289
    @Override
290
    public void start(SubchannelStateListener listener) {
291
      if (subchannelStateListener != null) {
1 ✔
292
        super.start(listener);
1 ✔
293
      } else {
294
        subchannelStateListener = listener;
1 ✔
295
        super.start(new OutlierDetectionSubchannelStateListener(listener));
1 ✔
296
      }
297
    }
1 ✔
298

299
    @Override
300
    public void shutdown() {
301
      if (endpointTracker != null) {
1 ✔
302
        endpointTracker.removeSubchannel(this);
1 ✔
303
      }
304
      super.shutdown();
1 ✔
305
    }
1 ✔
306

307
    @Override
308
    public Attributes getAttributes() {
309
      if (endpointTracker != null) {
1 ✔
310
        return delegate.getAttributes().toBuilder().set(ENDPOINT_TRACKER_KEY, endpointTracker)
1 ✔
311
            .build();
1 ✔
312
      } else {
313
        return delegate.getAttributes();
×
314
      }
315
    }
316

317
    @Override
318
    public void updateAddresses(List<EquivalentAddressGroup> addressGroups) {
319
      // Outlier detection only supports subchannels with a single address, but the list of
320
      // addressGroups associated with a subchannel can change at any time, so we need to react to
321
      // changes in the address list plurality.
322

323
      // No change in address plurality, we replace the single one with a new one.
324
      if (hasSingleAddress(getAllAddresses()) && hasSingleAddress(addressGroups)) {
1 ✔
325
        // Remove the current subchannel from the old address it is associated with in the map.
326
        if (endpointTrackerMap.containsValue(endpointTracker)) {
1 ✔
327
          endpointTracker.removeSubchannel(this);
1 ✔
328
        }
329

330
        // If the map has an entry for the new address, we associate this subchannel with it.
331
        SocketAddress address = addressGroups.get(0).getAddresses().get(0);
1 ✔
332
        if (addressMap.containsKey(address)) {
1 ✔
333
          addressMap.get(address).addSubchannel(this);
1 ✔
334
        }
335
      } else if (hasSingleAddress(getAllAddresses()) && !hasSingleAddress(addressGroups)) {
1 ✔
336
        // We go from a single address to having multiple, making this subchannel uneligible for
337
        // outlier detection. Remove it from all trackers and reset the call counters of all the
338
        // associated trackers.
339
        // Remove the current subchannel from the old address it is associated with in the map.
340
        if (addressMap.containsKey(getAddresses().getAddresses().get(0))) {
1 ✔
341
          EndpointTracker tracker = addressMap.get(getAddresses().getAddresses().get(0));
1 ✔
342
          tracker.removeSubchannel(this);
1 ✔
343
          tracker.resetCallCounters();
1 ✔
344
        }
1 ✔
345
      } else if (!hasSingleAddress(getAllAddresses()) && hasSingleAddress(addressGroups)) {
1 ✔
346
        // We go from, previously uneligble, multiple address mode to a single address. If the map
347
        // has an entry for the new address, we associate this subchannel with it.
348
        SocketAddress address = addressGroups.get(0).getAddresses().get(0);
1 ✔
349
        if (addressMap.containsKey(address)) {
1 ✔
350
          EndpointTracker tracker = addressMap.get(address);
1 ✔
351
          tracker.addSubchannel(this);
1 ✔
352
        }
353
      }
354

355
      // We could also have multiple addressGroups and get an update for multiple new ones. This is
356
      // a no-op as we will just continue to ignore multiple address subchannels.
357

358
      delegate.updateAddresses(addressGroups);
1 ✔
359
    }
1 ✔
360

361
    /**
362
     * If the {@link Subchannel} is considered for outlier detection the associated {@link
363
     * EndpointTracker} should be set.
364
     */
365
    void setEndpointTracker(EndpointTracker endpointTracker) {
366
      this.endpointTracker = endpointTracker;
1 ✔
367
    }
1 ✔
368

369
    void clearEndpointTracker() {
370
      this.endpointTracker = null;
1 ✔
371
    }
1 ✔
372

373
    void eject() {
374
      ejected = true;
1 ✔
375
      subchannelStateListener.onSubchannelState(ConnectivityStateInfo.forTransientFailure(
1 ✔
376
          Status.UNAVAILABLE.withDescription(
1 ✔
377
              "The subchannel has been ejected by outlier detection")));
378
      logger.log(ChannelLogLevel.INFO, "Subchannel ejected: {0}", this);
1 ✔
379
    }
1 ✔
380

381
    void uneject() {
382
      ejected = false;
1 ✔
383
      if (lastSubchannelState != null) {
1 ✔
384
        subchannelStateListener.onSubchannelState(lastSubchannelState);
1 ✔
385
        logger.log(ChannelLogLevel.INFO, "Subchannel unejected: {0}", this);
1 ✔
386
      }
387
    }
1 ✔
388

389
    boolean isEjected() {
390
      return ejected;
1 ✔
391
    }
392

393
    @Override
394
    protected Subchannel delegate() {
395
      return delegate;
1 ✔
396
    }
397

398
    /**
399
     * Wraps the actual listener so that state changes from the actual one can be intercepted.
400
     */
401
    final class OutlierDetectionSubchannelStateListener implements SubchannelStateListener {
402

403
      private final SubchannelStateListener delegate;
404

405
      OutlierDetectionSubchannelStateListener(SubchannelStateListener delegate) {
1 ✔
406
        this.delegate = delegate;
1 ✔
407
      }
1 ✔
408

409
      @Override
410
      public void onSubchannelState(ConnectivityStateInfo newState) {
411
        lastSubchannelState = newState;
1 ✔
412
        if (!ejected) {
1 ✔
413
          delegate.onSubchannelState(newState);
1 ✔
414
        }
415
      }
1 ✔
416
    }
417

418
    @Override
419
    public String toString() {
420
      return "OutlierDetectionSubchannel{"
×
421
              + "addresses=" + delegate.getAllAddresses()
×
422
              + '}';
423
    }
424
  }
425

426

427
  /**
428
   * This picker delegates the actual picking logic to a wrapped delegate, but associates a {@link
429
   * ClientStreamTracer} with each pick to track the results of each subchannel stream.
430
   */
431
  final class OutlierDetectionPicker extends SubchannelPicker {
432

433
    private final SubchannelPicker delegate;
434

435
    OutlierDetectionPicker(SubchannelPicker delegate) {
1 ✔
436
      this.delegate = delegate;
1 ✔
437
    }
1 ✔
438

439
    @Override
440
    public PickResult pickSubchannel(PickSubchannelArgs args) {
441
      PickResult pickResult = delegate.pickSubchannel(args);
1 ✔
442

443
      Subchannel subchannel = pickResult.getSubchannel();
1 ✔
444
      if (subchannel != null) {
1 ✔
445
        EndpointTracker tracker = subchannel.getAttributes().get(ENDPOINT_TRACKER_KEY);
1 ✔
446
        if (subchannel instanceof OutlierDetectionSubchannel) {
1 ✔
447
          subchannel = ((OutlierDetectionSubchannel) subchannel).delegate();
1 ✔
448
        }
449
        return pickResult.copyWithSubchannel(subchannel)
1 ✔
450
            .copyWithStreamTracerFactory(new ResultCountingClientStreamTracerFactory(
1 ✔
451
                tracker,
452
                pickResult.getStreamTracerFactory()));
1 ✔
453
      }
454

455
      return pickResult;
×
456
    }
457

458
    /**
459
     * Builds instances of a {@link ClientStreamTracer} that increments the call count in the
460
     * tracker for each closed stream.
461
     */
462
    final class ResultCountingClientStreamTracerFactory extends ClientStreamTracer.Factory {
463

464
      private final EndpointTracker tracker;
465

466
      @Nullable
467
      private final ClientStreamTracer.Factory delegateFactory;
468

469
      ResultCountingClientStreamTracerFactory(EndpointTracker tracker,
470
          @Nullable ClientStreamTracer.Factory delegateFactory) {
1 ✔
471
        this.tracker = tracker;
1 ✔
472
        this.delegateFactory = delegateFactory;
1 ✔
473
      }
1 ✔
474

475
      @Override
476
      public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) {
477
        if (delegateFactory != null) {
1 ✔
478
          ClientStreamTracer delegateTracer = delegateFactory.newClientStreamTracer(info, headers);
1 ✔
479
          return new ForwardingClientStreamTracer() {
1 ✔
480
            @Override
481
            protected ClientStreamTracer delegate() {
482
              return delegateTracer;
1 ✔
483
            }
484

485
            @Override
486
            public void streamClosed(Status status) {
487
              tracker.incrementCallCount(status.isOk());
1 ✔
488
              delegate().streamClosed(status);
1 ✔
489
            }
1 ✔
490
          };
491
        } else {
492
          return new ClientStreamTracer() {
1 ✔
493
            @Override
494
            public void streamClosed(Status status) {
495
              tracker.incrementCallCount(status.isOk());
1 ✔
496
            }
1 ✔
497
          };
498
        }
499
      }
500
    }
501
  }
502

503
  /**
504
   * Tracks additional information about the endpoint needed for outlier detection.
505
   */
506
  static final class EndpointTracker {
507

508
    private OutlierDetectionLoadBalancerConfig config;
509
    // Marked as volatile to assure that when the inactive counter is swapped in as the new active
510
    // one, all threads see the change and don't hold on to a reference to the now inactive counter.
511
    private volatile CallCounter activeCallCounter = new CallCounter();
1 ✔
512
    private CallCounter inactiveCallCounter = new CallCounter();
1 ✔
513
    private Long ejectionTimeNanos;
514
    private int ejectionTimeMultiplier;
515
    private final Set<OutlierDetectionSubchannel> subchannels = new HashSet<>();
1 ✔
516

517
    EndpointTracker(OutlierDetectionLoadBalancerConfig config) {
1 ✔
518
      this.config = config;
1 ✔
519
    }
1 ✔
520

521
    void setConfig(OutlierDetectionLoadBalancerConfig config) {
522
      this.config = config;
1 ✔
523
    }
1 ✔
524

525
    /**
526
     * Adds a subchannel to the tracker, while assuring that the subchannel ejection status is
527
     * updated to match the tracker's if needed.
528
     */
529
    boolean addSubchannel(OutlierDetectionSubchannel subchannel) {
530
      // Make sure that the subchannel is in the same ejection state as the new tracker it is
531
      // associated with.
532
      if (subchannelsEjected() && !subchannel.isEjected()) {
1 ✔
533
        subchannel.eject();
×
534
      } else if (!subchannelsEjected() && subchannel.isEjected()) {
1 ✔
535
        subchannel.uneject();
1 ✔
536
      }
537
      subchannel.setEndpointTracker(this);
1 ✔
538
      return subchannels.add(subchannel);
1 ✔
539
    }
540

541
    boolean removeSubchannel(OutlierDetectionSubchannel subchannel) {
542
      subchannel.clearEndpointTracker();
1 ✔
543
      return subchannels.remove(subchannel);
1 ✔
544
    }
545

546
    boolean containsSubchannel(OutlierDetectionSubchannel subchannel) {
547
      return subchannels.contains(subchannel);
×
548
    }
549

550
    @VisibleForTesting
551
    Set<OutlierDetectionSubchannel> getSubchannels() {
552
      return ImmutableSet.copyOf(subchannels);
1 ✔
553
    }
554

555
    void incrementCallCount(boolean success) {
556
      // If neither algorithm is configured, no point in incrementing counters.
557
      if (config.successRateEjection == null && config.failurePercentageEjection == null) {
1 ✔
558
        return;
×
559
      }
560

561
      if (success) {
1 ✔
562
        activeCallCounter.successCount.getAndIncrement();
1 ✔
563
      } else {
564
        activeCallCounter.failureCount.getAndIncrement();
1 ✔
565
      }
566
    }
1 ✔
567

568
    @VisibleForTesting
569
    long activeVolume() {
570
      return activeCallCounter.successCount.get() + activeCallCounter.failureCount.get();
1 ✔
571
    }
572

573
    long inactiveVolume() {
574
      return inactiveCallCounter.successCount.get() + inactiveCallCounter.failureCount.get();
1 ✔
575
    }
576

577
    double successRate() {
578
      return ((double) inactiveCallCounter.successCount.get()) / inactiveVolume();
1 ✔
579
    }
580

581
    double failureRate() {
582
      return ((double)inactiveCallCounter.failureCount.get()) / inactiveVolume();
1 ✔
583
    }
584

585
    void resetCallCounters() {
586
      activeCallCounter.reset();
1 ✔
587
      inactiveCallCounter.reset();
1 ✔
588
    }
1 ✔
589

590
    void decrementEjectionTimeMultiplier() {
591
      // The multiplier should not go negative.
592
      ejectionTimeMultiplier = ejectionTimeMultiplier == 0 ? 0 : ejectionTimeMultiplier - 1;
1 ✔
593
    }
1 ✔
594

595
    void resetEjectionTimeMultiplier() {
596
      ejectionTimeMultiplier = 0;
1 ✔
597
    }
1 ✔
598

599
    /**
600
     * Swaps the active and inactive counters.
601
     *
602
     * <p>Note that this method is not thread safe as the swap is not done atomically. This is
603
     * expected to only be called from the timer that is scheduled at a fixed delay, assuring that
604
     * only one timer is active at a time.
605
     */
606
    void swapCounters() {
607
      inactiveCallCounter.reset();
1 ✔
608
      CallCounter tempCounter = activeCallCounter;
1 ✔
609
      activeCallCounter = inactiveCallCounter;
1 ✔
610
      inactiveCallCounter = tempCounter;
1 ✔
611
    }
1 ✔
612

613
    void ejectSubchannels(long ejectionTimeNanos) {
614
      this.ejectionTimeNanos = ejectionTimeNanos;
1 ✔
615
      ejectionTimeMultiplier++;
1 ✔
616
      for (OutlierDetectionSubchannel subchannel : subchannels) {
1 ✔
617
        subchannel.eject();
1 ✔
618
      }
1 ✔
619
    }
1 ✔
620

621
    /**
622
     * Uneject a currently ejected address.
623
     */
624
    void unejectSubchannels() {
625
      checkState(ejectionTimeNanos != null, "not currently ejected");
1 ✔
626
      ejectionTimeNanos = null;
1 ✔
627
      for (OutlierDetectionSubchannel subchannel : subchannels) {
1 ✔
628
        subchannel.uneject();
1 ✔
629
      }
1 ✔
630
    }
1 ✔
631

632
    boolean subchannelsEjected() {
633
      return ejectionTimeNanos != null;
1 ✔
634
    }
635

636
    public boolean maxEjectionTimeElapsed(long currentTimeNanos) {
637
      // The instant in time beyond which the address should no longer be ejected. Also making sure
638
      // we honor any maximum ejection time setting.
639
      long maxEjectionDurationSecs
1 ✔
640
          = Math.max(config.baseEjectionTimeNanos, config.maxEjectionTimeNanos);
1 ✔
641
      long maxEjectionTimeNanos =
1 ✔
642
          ejectionTimeNanos + Math.min(
1 ✔
643
              config.baseEjectionTimeNanos * ejectionTimeMultiplier,
644
              maxEjectionDurationSecs);
645

646
      return currentTimeNanos - maxEjectionTimeNanos > 0;
1 ✔
647
    }
648

649
    /** Tracks both successful and failed call counts. */
650
    private static final class CallCounter {
1 ✔
651
      AtomicLong successCount = new AtomicLong();
1 ✔
652
      AtomicLong failureCount = new AtomicLong();
1 ✔
653

654
      void reset() {
655
        successCount.set(0);
1 ✔
656
        failureCount.set(0);
1 ✔
657
      }
1 ✔
658
    }
659

660
    @Override
661
    public String toString() {
662
      return "EndpointTracker{"
×
663
              + "subchannels=" + subchannels
664
              + '}';
665
    }
666
  }
667

668
  /**
669
   * Maintains a mapping from endpoint (a set of addresses) to their trackers.
670
   */
671
  static final class EndpointTrackerMap extends ForwardingMap<Set<SocketAddress>, EndpointTracker> {
672
    private final Map<Set<SocketAddress>, EndpointTracker> trackerMap;
673

674
    EndpointTrackerMap() {
1 ✔
675
      trackerMap = new HashMap<>();
1 ✔
676
    }
1 ✔
677

678
    @Override
679
    protected Map<Set<SocketAddress>, EndpointTracker> delegate() {
680
      return trackerMap;
1 ✔
681
    }
682

683
    void updateTrackerConfigs(OutlierDetectionLoadBalancerConfig config) {
684
      for (EndpointTracker tracker: trackerMap.values()) {
1 ✔
685
        tracker.setConfig(config);
1 ✔
686
      }
1 ✔
687
    }
1 ✔
688

689
    /** Adds a new tracker for every given address. */
690
    void putNewTrackers(OutlierDetectionLoadBalancerConfig config,
691
        Set<Set<SocketAddress>> endpoints) {
692
      for (Set<SocketAddress> endpoint : endpoints) {
1 ✔
693
        if (!trackerMap.containsKey(endpoint)) {
1 ✔
694
          trackerMap.put(endpoint, new EndpointTracker(config));
1 ✔
695
        }
696
      }
1 ✔
697
    }
1 ✔
698

699
    /** Resets the call counters for all the trackers in the map. */
700
    void resetCallCounters() {
701
      for (EndpointTracker tracker : trackerMap.values()) {
1 ✔
702
        tracker.resetCallCounters();
1 ✔
703
      }
1 ✔
704
    }
1 ✔
705

706
    /**
707
     * When OD gets disabled we need to uneject any subchannels that may have been ejected and
708
     * to reset the ejection time multiplier.
709
     */
710
    void cancelTracking() {
711
      for (EndpointTracker tracker : trackerMap.values()) {
1 ✔
712
        if (tracker.subchannelsEjected()) {
1 ✔
713
          tracker.unejectSubchannels();
×
714
        }
715
        tracker.resetEjectionTimeMultiplier();
1 ✔
716
      }
1 ✔
717
    }
1 ✔
718

719
    /** Swaps the active and inactive counters for each tracker. */
720
    void swapCounters() {
721
      for (EndpointTracker tracker : trackerMap.values()) {
1 ✔
722
        tracker.swapCounters();
1 ✔
723
      }
1 ✔
724
    }
1 ✔
725

726
    /**
727
     * At the end of a timer run we need to decrement the ejection time multiplier for trackers
728
     * that don't have ejected subchannels and uneject ones that have spent the maximum ejection
729
     * time allowed.
730
     */
731
    void maybeUnejectOutliers(long detectionTimerStartNanos) {
732
      for (EndpointTracker tracker : trackerMap.values()) {
1 ✔
733
        if (!tracker.subchannelsEjected()) {
1 ✔
734
          tracker.decrementEjectionTimeMultiplier();
1 ✔
735
        }
736

737
        if (tracker.subchannelsEjected() && tracker.maxEjectionTimeElapsed(
1 ✔
738
            detectionTimerStartNanos)) {
739
          tracker.unejectSubchannels();
1 ✔
740
        }
741
      }
1 ✔
742
    }
1 ✔
743

744
    /**
745
     * How many percent of the addresses have been ejected.
746
     */
747
    double ejectionPercentage() {
748
      if (trackerMap.isEmpty()) {
1 ✔
749
        return 0;
×
750
      }
751
      int totalEndpoints = 0;
1 ✔
752
      int ejectedEndpoints = 0;
1 ✔
753
      for (EndpointTracker tracker : trackerMap.values()) {
1 ✔
754
        totalEndpoints++;
1 ✔
755
        if (tracker.subchannelsEjected()) {
1 ✔
756
          ejectedEndpoints++;
1 ✔
757
        }
758
      }
1 ✔
759
      return ((double)ejectedEndpoints / totalEndpoints) * 100;
1 ✔
760
    }
761
  }
762

763

764
  /**
765
   * Implementations provide different ways of ejecting outlier addresses..
766
   */
767
  interface OutlierEjectionAlgorithm {
768

769
    /** Eject any outlier addresses. */
770
    void ejectOutliers(EndpointTrackerMap trackerMap, long ejectionTimeNanos);
771

772
    /** Builds a list of algorithms that are enabled in the given config. */
773
    @Nullable
774
    static List<OutlierEjectionAlgorithm> forConfig(OutlierDetectionLoadBalancerConfig config,
775
                                                    ChannelLogger logger) {
776
      ImmutableList.Builder<OutlierEjectionAlgorithm> algoListBuilder = ImmutableList.builder();
1 ✔
777
      if (config.successRateEjection != null) {
1 ✔
778
        algoListBuilder.add(new SuccessRateOutlierEjectionAlgorithm(config, logger));
1 ✔
779
      }
780
      if (config.failurePercentageEjection != null) {
1 ✔
781
        algoListBuilder.add(new FailurePercentageOutlierEjectionAlgorithm(config, logger));
1 ✔
782
      }
783
      return algoListBuilder.build();
1 ✔
784
    }
785
  }
786

787
  /**
788
   * This algorithm ejects addresses that don't maintain a required rate of successful calls. The
789
   * required rate is not fixed, but is based on the mean and standard deviation of the success
790
   * rates of all of the addresses.
791
   */
792
  static final class SuccessRateOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm {
793

794
    private final OutlierDetectionLoadBalancerConfig config;
795

796
    private final ChannelLogger logger;
797

798
    SuccessRateOutlierEjectionAlgorithm(OutlierDetectionLoadBalancerConfig config,
799
                                        ChannelLogger logger) {
1 ✔
800
      checkArgument(config.successRateEjection != null, "success rate ejection config is null");
1 ✔
801
      this.config = config;
1 ✔
802
      this.logger = logger;
1 ✔
803
    }
1 ✔
804

805
    @Override
806
    public void ejectOutliers(EndpointTrackerMap trackerMap, long ejectionTimeNanos) {
807

808
      // Only consider addresses that have the minimum request volume specified in the config.
809
      List<EndpointTracker> trackersWithVolume = trackersWithVolume(trackerMap,
1 ✔
810
          config.successRateEjection.requestVolume);
811
      // If we don't have enough endpoints with significant volume then there's nothing to do.
812
      if (trackersWithVolume.size() < config.successRateEjection.minimumHosts
1 ✔
813
          || trackersWithVolume.size() == 0) {
1 ✔
814
        return;
1 ✔
815
      }
816

817
      // Calculate mean and standard deviation of the fractions of successful calls.
818
      List<Double> successRates = new ArrayList<>();
1 ✔
819
      for (EndpointTracker tracker : trackersWithVolume) {
1 ✔
820
        successRates.add(tracker.successRate());
1 ✔
821
      }
1 ✔
822
      double mean = mean(successRates);
1 ✔
823
      double stdev = standardDeviation(successRates, mean);
1 ✔
824

825
      double requiredSuccessRate =
1 ✔
826
          mean - stdev * (config.successRateEjection.stdevFactor / 1000f);
827

828
      for (EndpointTracker tracker : trackersWithVolume) {
1 ✔
829
        // If we are above or equal to the max ejection percentage, don't eject any more. This will
830
        // allow the total ejections to go one above the max, but at the same time it assures at
831
        // least one ejection, which the spec calls for. This behavior matches what Envoy proxy
832
        // does.
833
        if (trackerMap.ejectionPercentage() >= config.maxEjectionPercent) {
1 ✔
834
          return;
1 ✔
835
        }
836

837
        // If success rate is below the threshold, eject the address.
838
        if (tracker.successRate() < requiredSuccessRate) {
1 ✔
839
          logger.log(ChannelLogLevel.DEBUG,
1 ✔
840
                  "SuccessRate algorithm detected outlier: {0}. "
841
                          + "Parameters: successRate={1}, mean={2}, stdev={3}, "
842
                          + "requiredSuccessRate={4}",
843
                  tracker, tracker.successRate(),  mean, stdev, requiredSuccessRate);
1 ✔
844
          // Only eject some endpoints based on the enforcement percentage.
845
          if (new Random().nextInt(100) < config.successRateEjection.enforcementPercentage) {
1 ✔
846
            tracker.ejectSubchannels(ejectionTimeNanos);
1 ✔
847
          }
848
        }
849
      }
1 ✔
850
    }
1 ✔
851

852
    /** Calculates the mean of the given values. */
853
    @VisibleForTesting
854
    static double mean(Collection<Double> values) {
855
      double totalValue = 0;
1 ✔
856
      for (double value : values) {
1 ✔
857
        totalValue += value;
1 ✔
858
      }
1 ✔
859

860
      return totalValue / values.size();
1 ✔
861
    }
862

863
    /** Calculates the standard deviation for the given values and their mean. */
864
    @VisibleForTesting
865
    static double standardDeviation(Collection<Double> values, double mean) {
866
      double squaredDifferenceSum = 0;
1 ✔
867
      for (double value : values) {
1 ✔
868
        double difference = value - mean;
1 ✔
869
        squaredDifferenceSum += difference * difference;
1 ✔
870
      }
1 ✔
871
      double variance = squaredDifferenceSum / values.size();
1 ✔
872

873
      return Math.sqrt(variance);
1 ✔
874
    }
875
  }
876

877
  static final class FailurePercentageOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm {
878

879
    private final OutlierDetectionLoadBalancerConfig config;
880

881
    private final ChannelLogger logger;
882

883
    FailurePercentageOutlierEjectionAlgorithm(OutlierDetectionLoadBalancerConfig config,
884
                                              ChannelLogger logger) {
1 ✔
885
      this.config = config;
1 ✔
886
      this.logger = logger;
1 ✔
887
    }
1 ✔
888

889
    @Override
890
    public void ejectOutliers(EndpointTrackerMap trackerMap, long ejectionTimeNanos) {
891

892
      // Only consider endpoints that have the minimum request volume specified in the config.
893
      List<EndpointTracker> trackersWithVolume = trackersWithVolume(trackerMap,
1 ✔
894
          config.failurePercentageEjection.requestVolume);
895
      // If we don't have enough endpoints with significant volume then there's nothing to do.
896
      if (trackersWithVolume.size() < config.failurePercentageEjection.minimumHosts
1 ✔
897
          || trackersWithVolume.size() == 0) {
1 ✔
898
        return;
1 ✔
899
      }
900

901
      // If this endpoint does not have enough volume to be considered, skip to the next one.
902
      for (EndpointTracker tracker : trackersWithVolume) {
1 ✔
903
        // If we are above or equal to the max ejection percentage, don't eject any more. This will
904
        // allow the total ejections to go one above the max, but at the same time it assures at
905
        // least one ejection, which the spec calls for. This behavior matches what Envoy proxy
906
        // does.
907
        if (trackerMap.ejectionPercentage() >= config.maxEjectionPercent) {
1 ✔
908
          return;
×
909
        }
910

911
        if (tracker.inactiveVolume() < config.failurePercentageEjection.requestVolume) {
1 ✔
912
          continue;
×
913
        }
914

915
        // If the failure rate is above the threshold, we should eject...
916
        double maxFailureRate = ((double)config.failurePercentageEjection.threshold) / 100;
1 ✔
917
        if (tracker.failureRate() > maxFailureRate) {
1 ✔
918
          logger.log(ChannelLogLevel.DEBUG,
1 ✔
919
                  "FailurePercentage algorithm detected outlier: {0}, failureRate={1}",
920
                  tracker, tracker.failureRate());
1 ✔
921
          // ...but only enforce this based on the enforcement percentage.
922
          if (new Random().nextInt(100) < config.failurePercentageEjection.enforcementPercentage) {
1 ✔
923
            tracker.ejectSubchannels(ejectionTimeNanos);
1 ✔
924
          }
925
        }
926
      }
1 ✔
927
    }
1 ✔
928
  }
929

930
  /** Returns only the trackers that have the minimum configured volume to be considered. */
931
  private static List<EndpointTracker> trackersWithVolume(EndpointTrackerMap trackerMap,
932
                                                          int volume) {
933
    List<EndpointTracker> trackersWithVolume = new ArrayList<>();
1 ✔
934
    for (EndpointTracker tracker : trackerMap.values()) {
1 ✔
935
      if (tracker.inactiveVolume() >= volume) {
1 ✔
936
        trackersWithVolume.add(tracker);
1 ✔
937
      }
938
    }
1 ✔
939
    return trackersWithVolume;
1 ✔
940
  }
941

942
  /** Counts how many addresses are in a given address group. */
943
  private static boolean hasSingleAddress(List<EquivalentAddressGroup> addressGroups) {
944
    int addressCount = 0;
1 ✔
945
    for (EquivalentAddressGroup addressGroup : addressGroups) {
1 ✔
946
      addressCount += addressGroup.getAddresses().size();
1 ✔
947
      if (addressCount > 1) {
1 ✔
948
        return false;
1 ✔
949
      }
950
    }
1 ✔
951
    return true;
1 ✔
952
  }
953

954
  /**
955
   * The configuration for {@link OutlierDetectionLoadBalancer}.
956
   */
957
  public static final class OutlierDetectionLoadBalancerConfig {
958

959
    public final long intervalNanos;
960
    public final long baseEjectionTimeNanos;
961
    public final long maxEjectionTimeNanos;
962
    public final int maxEjectionPercent;
963
    public final SuccessRateEjection successRateEjection;
964
    public final FailurePercentageEjection failurePercentageEjection;
965
    public final Object childConfig;
966

967
    private OutlierDetectionLoadBalancerConfig(Builder builder) {
1 ✔
968
      this.intervalNanos = builder.intervalNanos;
1 ✔
969
      this.baseEjectionTimeNanos = builder.baseEjectionTimeNanos;
1 ✔
970
      this.maxEjectionTimeNanos = builder.maxEjectionTimeNanos;
1 ✔
971
      this.maxEjectionPercent = builder.maxEjectionPercent;
1 ✔
972
      this.successRateEjection = builder.successRateEjection;
1 ✔
973
      this.failurePercentageEjection = builder.failurePercentageEjection;
1 ✔
974
      this.childConfig = builder.childConfig;
1 ✔
975
    }
1 ✔
976

977
    /** Builds a new {@link OutlierDetectionLoadBalancerConfig}. */
978
    public static final class Builder {
1 ✔
979
      long intervalNanos = 10_000_000_000L; // 10s
1 ✔
980
      long baseEjectionTimeNanos = 30_000_000_000L; // 30s
1 ✔
981
      long maxEjectionTimeNanos = 300_000_000_000L; // 300s
1 ✔
982
      int maxEjectionPercent = 10;
1 ✔
983
      SuccessRateEjection successRateEjection;
984
      FailurePercentageEjection failurePercentageEjection;
985
      Object childConfig;
986

987
      /** The interval between outlier detection sweeps. */
988
      public Builder setIntervalNanos(long intervalNanos) {
989
        this.intervalNanos = intervalNanos;
1 ✔
990
        return this;
1 ✔
991
      }
992

993
      /** The base time an address is ejected for. */
994
      public Builder setBaseEjectionTimeNanos(long baseEjectionTimeNanos) {
995
        this.baseEjectionTimeNanos = baseEjectionTimeNanos;
1 ✔
996
        return this;
1 ✔
997
      }
998

999
      /** The longest time an address can be ejected. */
1000
      public Builder setMaxEjectionTimeNanos(long maxEjectionTimeNanos) {
1001
        this.maxEjectionTimeNanos = maxEjectionTimeNanos;
1 ✔
1002
        return this;
1 ✔
1003
      }
1004

1005
      /** The algorithm agnostic maximum percentage of addresses that can be ejected. */
1006
      public Builder setMaxEjectionPercent(int maxEjectionPercent) {
1007
        this.maxEjectionPercent = maxEjectionPercent;
1 ✔
1008
        return this;
1 ✔
1009
      }
1010

1011
      /** Set to enable success rate ejection. */
1012
      public Builder setSuccessRateEjection(
1013
          SuccessRateEjection successRateEjection) {
1014
        this.successRateEjection = successRateEjection;
1 ✔
1015
        return this;
1 ✔
1016
      }
1017

1018
      /** Set to enable failure percentage ejection. */
1019
      public Builder setFailurePercentageEjection(
1020
          FailurePercentageEjection failurePercentageEjection) {
1021
        this.failurePercentageEjection = failurePercentageEjection;
1 ✔
1022
        return this;
1 ✔
1023
      }
1024

1025
      /**
1026
       * Sets the graceful child switch config the {@link OutlierDetectionLoadBalancer} delegates
1027
       * to.
1028
       */
1029
      public Builder setChildConfig(Object childConfig) {
1030
        checkState(childConfig != null);
1 ✔
1031
        this.childConfig = childConfig;
1 ✔
1032
        return this;
1 ✔
1033
      }
1034

1035
      /** Builds a new instance of {@link OutlierDetectionLoadBalancerConfig}. */
1036
      public OutlierDetectionLoadBalancerConfig build() {
1037
        checkState(childConfig != null);
1 ✔
1038
        return new OutlierDetectionLoadBalancerConfig(this);
1 ✔
1039
      }
1040
    }
1041

1042
    /** The configuration for success rate ejection. */
1043
    public static final class SuccessRateEjection {
1044

1045
      public final int stdevFactor;
1046
      public final int enforcementPercentage;
1047
      public final int minimumHosts;
1048
      public final int requestVolume;
1049

1050
      SuccessRateEjection(Builder builder) {
1 ✔
1051
        this.stdevFactor = builder.stdevFactor;
1 ✔
1052
        this.enforcementPercentage = builder.enforcementPercentage;
1 ✔
1053
        this.minimumHosts = builder.minimumHosts;
1 ✔
1054
        this.requestVolume = builder.requestVolume;
1 ✔
1055
      }
1 ✔
1056

1057
      /** Builds new instances of {@link SuccessRateEjection}. */
1058
      public static final class Builder {
1 ✔
1059

1060
        int stdevFactor = 1900;
1 ✔
1061
        int enforcementPercentage = 100;
1 ✔
1062
        int minimumHosts = 5;
1 ✔
1063
        int requestVolume = 100;
1 ✔
1064

1065
        /** The product of this and the standard deviation of success rates determine the ejection
1066
         * threshold.
1067
         */
1068
        public Builder setStdevFactor(int stdevFactor) {
1069
          this.stdevFactor = stdevFactor;
1 ✔
1070
          return this;
1 ✔
1071
        }
1072

1073
        /** Only eject this percentage of outliers. */
1074
        public Builder setEnforcementPercentage(int enforcementPercentage) {
1075
          checkArgument(enforcementPercentage >= 0 && enforcementPercentage <= 100);
1 ✔
1076
          this.enforcementPercentage = enforcementPercentage;
1 ✔
1077
          return this;
1 ✔
1078
        }
1079

1080
        /** The minimum amount of hosts needed for success rate ejection. */
1081
        public Builder setMinimumHosts(int minimumHosts) {
1082
          checkArgument(minimumHosts >= 0);
1 ✔
1083
          this.minimumHosts = minimumHosts;
1 ✔
1084
          return this;
1 ✔
1085
        }
1086

1087
        /** The minimum address request volume to be considered for success rate ejection. */
1088
        public Builder setRequestVolume(int requestVolume) {
1089
          checkArgument(requestVolume >= 0);
1 ✔
1090
          this.requestVolume = requestVolume;
1 ✔
1091
          return this;
1 ✔
1092
        }
1093

1094
        /** Builds a new instance of {@link SuccessRateEjection}. */
1095
        public SuccessRateEjection build() {
1096
          return new SuccessRateEjection(this);
1 ✔
1097
        }
1098
      }
1099
    }
1100

1101
    /** The configuration for failure percentage ejection. */
1102
    public static final class FailurePercentageEjection {
1103
      public final int threshold;
1104
      public final int enforcementPercentage;
1105
      public final int minimumHosts;
1106
      public final int requestVolume;
1107

1108
      FailurePercentageEjection(Builder builder) {
1 ✔
1109
        this.threshold = builder.threshold;
1 ✔
1110
        this.enforcementPercentage = builder.enforcementPercentage;
1 ✔
1111
        this.minimumHosts = builder.minimumHosts;
1 ✔
1112
        this.requestVolume = builder.requestVolume;
1 ✔
1113
      }
1 ✔
1114

1115
      /** For building new {@link FailurePercentageEjection} instances. */
1116
      public static class Builder {
1 ✔
1117
        int threshold = 85;
1 ✔
1118
        int enforcementPercentage = 100;
1 ✔
1119
        int minimumHosts = 5;
1 ✔
1120
        int requestVolume = 50;
1 ✔
1121

1122
        /** The failure percentage that will result in an address being considered an outlier. */
1123
        public Builder setThreshold(int threshold) {
1124
          checkArgument(threshold >= 0 && threshold <= 100);
1 ✔
1125
          this.threshold = threshold;
1 ✔
1126
          return this;
1 ✔
1127
        }
1128

1129
        /** Only eject this percentage of outliers. */
1130
        public Builder setEnforcementPercentage(int enforcementPercentage) {
1131
          checkArgument(enforcementPercentage >= 0 && enforcementPercentage <= 100);
1 ✔
1132
          this.enforcementPercentage = enforcementPercentage;
1 ✔
1133
          return this;
1 ✔
1134
        }
1135

1136
        /** The minimum amount of host for failure percentage ejection to be enabled. */
1137
        public Builder setMinimumHosts(int minimumHosts) {
1138
          checkArgument(minimumHosts >= 0);
1 ✔
1139
          this.minimumHosts = minimumHosts;
1 ✔
1140
          return this;
1 ✔
1141
        }
1142

1143
        /**
1144
         * The request volume required for an address to be considered for failure percentage
1145
         * ejection.
1146
         */
1147
        public Builder setRequestVolume(int requestVolume) {
1148
          checkArgument(requestVolume >= 0);
1 ✔
1149
          this.requestVolume = requestVolume;
1 ✔
1150
          return this;
1 ✔
1151
        }
1152

1153
        /** Builds a new instance of {@link FailurePercentageEjection}. */
1154
        public FailurePercentageEjection build() {
1155
          return new FailurePercentageEjection(this);
1 ✔
1156
        }
1157
      }
1158
    }
1159

1160
    /** Determine if any outlier detection algorithms are enabled in the config. */
1161
    boolean outlierDetectionEnabled() {
1162
      return successRateEjection != null || failurePercentageEjection != null;
1 ✔
1163
    }
1164
  }
1165
}
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