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

grpc / grpc-java / #19102

14 Mar 2024 03:53AM UTC coverage: 88.259% (-0.05%) from 88.311%
#19102

push

github

web-flow
core: Eliminate NPE seen in PickFirstLeafLoadBalancer (#11013)

ref b/329420531

31150 of 35294 relevant lines covered (88.26%)

0.88 hits per line

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

96.71
/../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.collect.ForwardingMap;
26
import com.google.common.collect.ImmutableList;
27
import com.google.common.collect.ImmutableSet;
28
import io.grpc.Attributes;
29
import io.grpc.ChannelLogger;
30
import io.grpc.ChannelLogger.ChannelLogLevel;
31
import io.grpc.ClientStreamTracer;
32
import io.grpc.ClientStreamTracer.StreamInfo;
33
import io.grpc.ConnectivityState;
34
import io.grpc.ConnectivityStateInfo;
35
import io.grpc.EquivalentAddressGroup;
36
import io.grpc.Internal;
37
import io.grpc.LoadBalancer;
38
import io.grpc.Metadata;
39
import io.grpc.Status;
40
import io.grpc.SynchronizationContext;
41
import io.grpc.SynchronizationContext.ScheduledHandle;
42
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
43
import io.grpc.internal.TimeProvider;
44
import java.net.SocketAddress;
45
import java.util.ArrayList;
46
import java.util.Collection;
47
import java.util.HashMap;
48
import java.util.HashSet;
49
import java.util.List;
50
import java.util.Map;
51
import java.util.Random;
52
import java.util.Set;
53
import java.util.concurrent.ScheduledExecutorService;
54
import java.util.concurrent.atomic.AtomicLong;
55
import javax.annotation.Nullable;
56

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

77
  @VisibleForTesting
78
  final EndpointTrackerMap endpointTrackerMap;
79

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

83
  private final SynchronizationContext syncContext;
84
  private final Helper childHelper;
85
  private final GracefulSwitchLoadBalancer switchLb;
86
  private TimeProvider timeProvider;
87
  private final ScheduledExecutorService timeService;
88
  private ScheduledHandle detectionTimerHandle;
89
  private Long detectionTimerStartNanos;
90

91
  private final ChannelLogger logger;
92

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

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

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

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

132
    endpointTrackerMap.updateTrackerConfigs(config);
1✔
133

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

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

143
    switchLb.switchTo(config.childPolicy.getProvider());
1✔
144

145
    // If outlier detection is actually configured, start a timer that will periodically try to
146
    // detect outliers.
147
    if (config.outlierDetectionEnabled()) {
1✔
148
      Long initialDelayNanos;
149

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

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

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

177
    switchLb.handleResolvedAddresses(
1✔
178
        resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config.childPolicy.getConfig())
1✔
179
            .build());
1✔
180
    return Status.OK;
1✔
181
  }
182

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

188
  @Override
189
  public void shutdown() {
190
    switchLb.shutdown();
1✔
191
  }
1✔
192

193
  /**
194
   * This timer will be invoked periodically, according to configuration, and it will look for any
195
   * outlier subchannels.
196
   */
197
  class DetectionTimer implements Runnable {
198

199
    OutlierDetectionLoadBalancerConfig config;
200
    ChannelLogger logger;
201

202
    DetectionTimer(OutlierDetectionLoadBalancerConfig config, ChannelLogger logger) {
1✔
203
      this.config = config;
1✔
204
      this.logger = logger;
1✔
205
    }
1✔
206

207
    @Override
208
    public void run() {
209
      detectionTimerStartNanos = timeProvider.currentTimeNanos();
1✔
210

211
      endpointTrackerMap.swapCounters();
1✔
212

213
      for (OutlierEjectionAlgorithm algo : OutlierEjectionAlgorithm.forConfig(config, logger)) {
1✔
214
        algo.ejectOutliers(endpointTrackerMap, detectionTimerStartNanos);
1✔
215
      }
1✔
216

217
      endpointTrackerMap.maybeUnejectOutliers(detectionTimerStartNanos);
1✔
218
    }
1✔
219
  }
220

221
  /**
222
   * This child helper wraps the provided helper so that it can hand out wrapped {@link
223
   * OutlierDetectionSubchannel}s and manage the address info map.
224
   */
225
  class ChildHelper extends ForwardingLoadBalancerHelper {
226

227
    private Helper delegate;
228

229
    ChildHelper(Helper delegate) {
1✔
230
      this.delegate = new HealthProducerHelper(delegate);
1✔
231
    }
1✔
232

233
    @Override
234
    protected Helper delegate() {
235
      return delegate;
×
236
    }
237

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

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

252
        // If this address has already been ejected, we need to immediately eject this Subchannel.
253
        if (tracker.ejectionTimeNanos != null) {
1✔
254
          subchannel.eject();
×
255
        }
256
      }
257

258
      return subchannel;
1✔
259
    }
260

261
    @Override
262
    public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
263
      delegate.updateBalancingState(newState, new OutlierDetectionPicker(newPicker));
1✔
264
    }
1✔
265
  }
266

267
  class OutlierDetectionSubchannel extends ForwardingSubchannel {
268

269
    private final Subchannel delegate;
270
    private EndpointTracker endpointTracker;
271
    private boolean ejected;
272
    private ConnectivityStateInfo lastSubchannelState;
273

274
    // In the new pick first: created at construction, delegates to health consumer listener;
275
    // In th old pick first: created at subchannel.start(), delegates to subchannel state listener.
276
    private SubchannelStateListener subchannelStateListener;
277
    private final ChannelLogger logger;
278

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

294
    @Override
295
    public void start(SubchannelStateListener listener) {
296
      if (subchannelStateListener != null) {
1✔
297
        super.start(listener);
1✔
298
      } else {
299
        subchannelStateListener = listener;
1✔
300
        super.start(new OutlierDetectionSubchannelStateListener(listener));
1✔
301
      }
302
    }
1✔
303

304
    @Override
305
    public void shutdown() {
306
      if (endpointTracker != null) {
1✔
307
        endpointTracker.removeSubchannel(this);
1✔
308
      }
309
      super.shutdown();
1✔
310
    }
1✔
311

312
    @Override
313
    public Attributes getAttributes() {
314
      if (endpointTracker != null) {
1✔
315
        return delegate.getAttributes().toBuilder().set(ENDPOINT_TRACKER_KEY, endpointTracker)
1✔
316
            .build();
1✔
317
      } else {
318
        return delegate.getAttributes();
×
319
      }
320
    }
321

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

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

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

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

363
      delegate.updateAddresses(addressGroups);
1✔
364
    }
1✔
365

366
    /**
367
     * If the {@link Subchannel} is considered for outlier detection the associated {@link
368
     * EndpointTracker} should be set.
369
     */
370
    void setEndpointTracker(EndpointTracker endpointTracker) {
371
      this.endpointTracker = endpointTracker;
1✔
372
    }
1✔
373

374
    void clearEndpointTracker() {
375
      this.endpointTracker = null;
1✔
376
    }
1✔
377

378
    void eject() {
379
      ejected = true;
1✔
380
      subchannelStateListener.onSubchannelState(
1✔
381
          ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE));
1✔
382
      logger.log(ChannelLogLevel.INFO, "Subchannel ejected: {0}", this);
1✔
383
    }
1✔
384

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

393
    boolean isEjected() {
394
      return ejected;
1✔
395
    }
396

397
    @Override
398
    protected Subchannel delegate() {
399
      return delegate;
1✔
400
    }
401

402
    /**
403
     * Wraps the actual listener so that state changes from the actual one can be intercepted.
404
     */
405
    class OutlierDetectionSubchannelStateListener implements SubchannelStateListener {
406

407
      private final SubchannelStateListener delegate;
408

409
      OutlierDetectionSubchannelStateListener(SubchannelStateListener delegate) {
1✔
410
        this.delegate = delegate;
1✔
411
      }
1✔
412

413
      @Override
414
      public void onSubchannelState(ConnectivityStateInfo newState) {
415
        lastSubchannelState = newState;
1✔
416
        if (!ejected) {
1✔
417
          delegate.onSubchannelState(newState);
1✔
418
        }
419
      }
1✔
420
    }
421

422
    @Override
423
    public String toString() {
424
      return "OutlierDetectionSubchannel{"
×
425
              + "addresses=" + delegate.getAllAddresses()
×
426
              + '}';
427
    }
428
  }
429

430

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

437
    private final SubchannelPicker delegate;
438

439
    OutlierDetectionPicker(SubchannelPicker delegate) {
1✔
440
      this.delegate = delegate;
1✔
441
    }
1✔
442

443
    @Override
444
    public PickResult pickSubchannel(PickSubchannelArgs args) {
445
      PickResult pickResult = delegate.pickSubchannel(args);
1✔
446

447
      Subchannel subchannel = pickResult.getSubchannel();
1✔
448
      if (subchannel != null) {
1✔
449
        return PickResult.withSubchannel(subchannel, new ResultCountingClientStreamTracerFactory(
1✔
450
            subchannel.getAttributes().get(ENDPOINT_TRACKER_KEY),
1✔
451
            pickResult.getStreamTracerFactory()));
1✔
452
      }
453

454
      return pickResult;
×
455
    }
456

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

463
      private final EndpointTracker tracker;
464

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

645
      return currentTimeNanos > maxEjectionTimeNanos;
1✔
646
    }
647

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

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

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

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

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

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

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

688
    /** Adds a new tracker for every given address. */
689
    void putNewTrackers(OutlierDetectionLoadBalancerConfig config,
690
        Set<Set<SocketAddress>> endpoints) {
691
      endpoints.forEach(e -> trackerMap.putIfAbsent(e, new EndpointTracker(config)));
1✔
692
    }
1✔
693

694
    /** Resets the call counters for all the trackers in the map. */
695
    void resetCallCounters() {
696
      for (EndpointTracker tracker : trackerMap.values()) {
1✔
697
        tracker.resetCallCounters();
1✔
698
      }
1✔
699
    }
1✔
700

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

714
    /** Swaps the active and inactive counters for each tracker. */
715
    void swapCounters() {
716
      for (EndpointTracker tracker : trackerMap.values()) {
1✔
717
        tracker.swapCounters();
1✔
718
      }
1✔
719
    }
1✔
720

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

732
        if (tracker.subchannelsEjected() && tracker.maxEjectionTimeElapsed(
1✔
733
            detectionTimerStartNanos)) {
1✔
734
          tracker.unejectSubchannels();
1✔
735
        }
736
      }
1✔
737
    }
1✔
738

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

758

759
  /**
760
   * Implementations provide different ways of ejecting outlier addresses..
761
   */
762
  interface OutlierEjectionAlgorithm {
763

764
    /** Eject any outlier addresses. */
765
    void ejectOutliers(EndpointTrackerMap trackerMap, long ejectionTimeNanos);
766

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

782
  /**
783
   * This algorithm ejects addresses that don't maintain a required rate of successful calls. The
784
   * required rate is not fixed, but is based on the mean and standard deviation of the success
785
   * rates of all of the addresses.
786
   */
787
  static class SuccessRateOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm {
788

789
    private final OutlierDetectionLoadBalancerConfig config;
790

791
    private final ChannelLogger logger;
792

793
    SuccessRateOutlierEjectionAlgorithm(OutlierDetectionLoadBalancerConfig config,
794
                                        ChannelLogger logger) {
1✔
795
      checkArgument(config.successRateEjection != null, "success rate ejection config is null");
1✔
796
      this.config = config;
1✔
797
      this.logger = logger;
1✔
798
    }
1✔
799

800
    @Override
801
    public void ejectOutliers(EndpointTrackerMap trackerMap, long ejectionTimeNanos) {
802

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

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

820
      double requiredSuccessRate =
1✔
821
          mean - stdev * (config.successRateEjection.stdevFactor / 1000f);
1✔
822

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

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

847
    /** Calculates the mean of the given values. */
848
    @VisibleForTesting
849
    static double mean(Collection<Double> values) {
850
      double totalValue = 0;
1✔
851
      for (double value : values) {
1✔
852
        totalValue += value;
1✔
853
      }
1✔
854

855
      return totalValue / values.size();
1✔
856
    }
857

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

868
      return Math.sqrt(variance);
1✔
869
    }
870
  }
871

872
  static class FailurePercentageOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm {
873

874
    private final OutlierDetectionLoadBalancerConfig config;
875

876
    private final ChannelLogger logger;
877

878
    FailurePercentageOutlierEjectionAlgorithm(OutlierDetectionLoadBalancerConfig config,
879
                                              ChannelLogger logger) {
1✔
880
      this.config = config;
1✔
881
      this.logger = logger;
1✔
882
    }
1✔
883

884
    @Override
885
    public void ejectOutliers(EndpointTrackerMap trackerMap, long ejectionTimeNanos) {
886

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

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

906
        if (tracker.inactiveVolume() < config.failurePercentageEjection.requestVolume) {
1✔
907
          continue;
×
908
        }
909

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

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

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

949
  /**
950
   * The configuration for {@link OutlierDetectionLoadBalancer}.
951
   */
952
  public static final class OutlierDetectionLoadBalancerConfig {
953

954
    public final Long intervalNanos;
955
    public final Long baseEjectionTimeNanos;
956
    public final Long maxEjectionTimeNanos;
957
    public final Integer maxEjectionPercent;
958
    public final SuccessRateEjection successRateEjection;
959
    public final FailurePercentageEjection failurePercentageEjection;
960
    public final PolicySelection childPolicy;
961

962
    private OutlierDetectionLoadBalancerConfig(Long intervalNanos,
963
        Long baseEjectionTimeNanos,
964
        Long maxEjectionTimeNanos,
965
        Integer maxEjectionPercent,
966
        SuccessRateEjection successRateEjection,
967
        FailurePercentageEjection failurePercentageEjection,
968
        PolicySelection childPolicy) {
1✔
969
      this.intervalNanos = intervalNanos;
1✔
970
      this.baseEjectionTimeNanos = baseEjectionTimeNanos;
1✔
971
      this.maxEjectionTimeNanos = maxEjectionTimeNanos;
1✔
972
      this.maxEjectionPercent = maxEjectionPercent;
1✔
973
      this.successRateEjection = successRateEjection;
1✔
974
      this.failurePercentageEjection = failurePercentageEjection;
1✔
975
      this.childPolicy = childPolicy;
1✔
976
    }
1✔
977

978
    /** Builds a new {@link OutlierDetectionLoadBalancerConfig}. */
979
    public static class Builder {
1✔
980
      Long intervalNanos = 10_000_000_000L; // 10s
1✔
981
      Long baseEjectionTimeNanos = 30_000_000_000L; // 30s
1✔
982
      Long maxEjectionTimeNanos = 300_000_000_000L; // 300s
1✔
983
      Integer maxEjectionPercent = 10;
1✔
984
      SuccessRateEjection successRateEjection;
985
      FailurePercentageEjection failurePercentageEjection;
986
      PolicySelection childPolicy;
987

988
      /** The interval between outlier detection sweeps. */
989
      public Builder setIntervalNanos(Long intervalNanos) {
990
        checkArgument(intervalNanos != null);
1✔
991
        this.intervalNanos = intervalNanos;
1✔
992
        return this;
1✔
993
      }
994

995
      /** The base time an address is ejected for. */
996
      public Builder setBaseEjectionTimeNanos(Long baseEjectionTimeNanos) {
997
        checkArgument(baseEjectionTimeNanos != null);
1✔
998
        this.baseEjectionTimeNanos = baseEjectionTimeNanos;
1✔
999
        return this;
1✔
1000
      }
1001

1002
      /** The longest time an address can be ejected. */
1003
      public Builder setMaxEjectionTimeNanos(Long maxEjectionTimeNanos) {
1004
        checkArgument(maxEjectionTimeNanos != null);
1✔
1005
        this.maxEjectionTimeNanos = maxEjectionTimeNanos;
1✔
1006
        return this;
1✔
1007
      }
1008

1009
      /** The algorithm agnostic maximum percentage of addresses that can be ejected. */
1010
      public Builder setMaxEjectionPercent(Integer maxEjectionPercent) {
1011
        checkArgument(maxEjectionPercent != null);
1✔
1012
        this.maxEjectionPercent = maxEjectionPercent;
1✔
1013
        return this;
1✔
1014
      }
1015

1016
      /** Set to enable success rate ejection. */
1017
      public Builder setSuccessRateEjection(
1018
          SuccessRateEjection successRateEjection) {
1019
        this.successRateEjection = successRateEjection;
1✔
1020
        return this;
1✔
1021
      }
1022

1023
      /** Set to enable failure percentage ejection. */
1024
      public Builder setFailurePercentageEjection(
1025
          FailurePercentageEjection failurePercentageEjection) {
1026
        this.failurePercentageEjection = failurePercentageEjection;
1✔
1027
        return this;
1✔
1028
      }
1029

1030
      /** Sets the child policy the {@link OutlierDetectionLoadBalancer} delegates to. */
1031
      public Builder setChildPolicy(PolicySelection childPolicy) {
1032
        checkState(childPolicy != null);
1✔
1033
        this.childPolicy = childPolicy;
1✔
1034
        return this;
1✔
1035
      }
1036

1037
      /** Builds a new instance of {@link OutlierDetectionLoadBalancerConfig}. */
1038
      public OutlierDetectionLoadBalancerConfig build() {
1039
        checkState(childPolicy != null);
1✔
1040
        return new OutlierDetectionLoadBalancerConfig(intervalNanos, baseEjectionTimeNanos,
1✔
1041
            maxEjectionTimeNanos, maxEjectionPercent, successRateEjection,
1042
            failurePercentageEjection, childPolicy);
1043
      }
1044
    }
1045

1046
    /** The configuration for success rate ejection. */
1047
    public static class SuccessRateEjection {
1048

1049
      public final Integer stdevFactor;
1050
      public final Integer enforcementPercentage;
1051
      public final Integer minimumHosts;
1052
      public final Integer requestVolume;
1053

1054
      SuccessRateEjection(Integer stdevFactor, Integer enforcementPercentage, Integer minimumHosts,
1055
          Integer requestVolume) {
1✔
1056
        this.stdevFactor = stdevFactor;
1✔
1057
        this.enforcementPercentage = enforcementPercentage;
1✔
1058
        this.minimumHosts = minimumHosts;
1✔
1059
        this.requestVolume = requestVolume;
1✔
1060
      }
1✔
1061

1062
      /** Builds new instances of {@link SuccessRateEjection}. */
1063
      public static final class Builder {
1✔
1064

1065
        Integer stdevFactor = 1900;
1✔
1066
        Integer enforcementPercentage = 100;
1✔
1067
        Integer minimumHosts = 5;
1✔
1068
        Integer requestVolume = 100;
1✔
1069

1070
        /** The product of this and the standard deviation of success rates determine the ejection
1071
         * threshold.
1072
         */
1073
        public Builder setStdevFactor(Integer stdevFactor) {
1074
          checkArgument(stdevFactor != null);
1✔
1075
          this.stdevFactor = stdevFactor;
1✔
1076
          return this;
1✔
1077
        }
1078

1079
        /** Only eject this percentage of outliers. */
1080
        public Builder setEnforcementPercentage(Integer enforcementPercentage) {
1081
          checkArgument(enforcementPercentage != null);
1✔
1082
          checkArgument(enforcementPercentage >= 0 && enforcementPercentage <= 100);
1✔
1083
          this.enforcementPercentage = enforcementPercentage;
1✔
1084
          return this;
1✔
1085
        }
1086

1087
        /** The minimum amount of hosts needed for success rate ejection. */
1088
        public Builder setMinimumHosts(Integer minimumHosts) {
1089
          checkArgument(minimumHosts != null);
1✔
1090
          checkArgument(minimumHosts >= 0);
1✔
1091
          this.minimumHosts = minimumHosts;
1✔
1092
          return this;
1✔
1093
        }
1094

1095
        /** The minimum address request volume to be considered for success rate ejection. */
1096
        public Builder setRequestVolume(Integer requestVolume) {
1097
          checkArgument(requestVolume != null);
1✔
1098
          checkArgument(requestVolume >= 0);
1✔
1099
          this.requestVolume = requestVolume;
1✔
1100
          return this;
1✔
1101
        }
1102

1103
        /** Builds a new instance of {@link SuccessRateEjection}. */
1104
        public SuccessRateEjection build() {
1105
          return new SuccessRateEjection(stdevFactor, enforcementPercentage, minimumHosts,
1✔
1106
              requestVolume);
1107
        }
1108
      }
1109
    }
1110

1111
    /** The configuration for failure percentage ejection. */
1112
    public static class FailurePercentageEjection {
1113
      public final Integer threshold;
1114
      public final Integer enforcementPercentage;
1115
      public final Integer minimumHosts;
1116
      public final Integer requestVolume;
1117

1118
      FailurePercentageEjection(Integer threshold, Integer enforcementPercentage,
1119
          Integer minimumHosts, Integer requestVolume) {
1✔
1120
        this.threshold = threshold;
1✔
1121
        this.enforcementPercentage = enforcementPercentage;
1✔
1122
        this.minimumHosts = minimumHosts;
1✔
1123
        this.requestVolume = requestVolume;
1✔
1124
      }
1✔
1125

1126
      /** For building new {@link FailurePercentageEjection} instances. */
1127
      public static class Builder {
1✔
1128
        Integer threshold = 85;
1✔
1129
        Integer enforcementPercentage = 100;
1✔
1130
        Integer minimumHosts = 5;
1✔
1131
        Integer requestVolume = 50;
1✔
1132

1133
        /** The failure percentage that will result in an address being considered an outlier. */
1134
        public Builder setThreshold(Integer threshold) {
1135
          checkArgument(threshold != null);
1✔
1136
          checkArgument(threshold >= 0 && threshold <= 100);
1✔
1137
          this.threshold = threshold;
1✔
1138
          return this;
1✔
1139
        }
1140

1141
        /** Only eject this percentage of outliers. */
1142
        public Builder setEnforcementPercentage(Integer enforcementPercentage) {
1143
          checkArgument(enforcementPercentage != null);
1✔
1144
          checkArgument(enforcementPercentage >= 0 && enforcementPercentage <= 100);
1✔
1145
          this.enforcementPercentage = enforcementPercentage;
1✔
1146
          return this;
1✔
1147
        }
1148

1149
        /** The minimum amount of host for failure percentage ejection to be enabled. */
1150
        public Builder setMinimumHosts(Integer minimumHosts) {
1151
          checkArgument(minimumHosts != null);
1✔
1152
          checkArgument(minimumHosts >= 0);
1✔
1153
          this.minimumHosts = minimumHosts;
1✔
1154
          return this;
1✔
1155
        }
1156

1157
        /**
1158
         * The request volume required for an address to be considered for failure percentage
1159
         * ejection.
1160
         */
1161
        public Builder setRequestVolume(Integer requestVolume) {
1162
          checkArgument(requestVolume != null);
1✔
1163
          checkArgument(requestVolume >= 0);
1✔
1164
          this.requestVolume = requestVolume;
1✔
1165
          return this;
1✔
1166
        }
1167

1168
        /** Builds a new instance of {@link FailurePercentageEjection}. */
1169
        public FailurePercentageEjection build() {
1170
          return new FailurePercentageEjection(threshold, enforcementPercentage, minimumHosts,
1✔
1171
              requestVolume);
1172
        }
1173
      }
1174
    }
1175

1176
    /** Determine if any outlier detection algorithms are enabled in the config. */
1177
    boolean outlierDetectionEnabled() {
1178
      return successRateEjection != null || failurePercentageEjection != null;
1✔
1179
    }
1180
  }
1181
}
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