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

grpc / grpc-java / #19708

21 Feb 2025 05:25AM UTC coverage: 88.622% (+0.03%) from 88.595%
#19708

push

github

ejona86
Use acceptResolvedAddresses() in easy cases

We want to move away from handleResolvedAddresses(). These are "easy" in
that they need no logic. LBs extending ForwardingLoadBalancer had the
method duplicated from handleResolvedAddresses() and swapped away from
`super` because ForwardingLoadBalancer only forwards
handleResolvedAddresses() reliably today. Duplicating small methods was
less bug-prone than dealing with ForwardingLoadBalancer.

34310 of 38715 relevant lines covered (88.62%)

0.89 hits per line

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

96.93
/../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.TimeProvider;
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 TimeProvider timeProvider;
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, TimeProvider timeProvider) {
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.timeProvider = timeProvider;
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 - (timeProvider.currentTimeNanos() - 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);
1✔
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
  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 = timeProvider.currentTimeNanos();
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
  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;
1✔
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
  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
    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
  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
        return PickResult.withSubchannel(subchannel, new ResultCountingClientStreamTracerFactory(
1✔
446
            subchannel.getAttributes().get(ENDPOINT_TRACKER_KEY),
1✔
447
            pickResult.getStreamTracerFactory()));
1✔
448
      }
449

450
      return pickResult;
×
451
    }
452

453
    /**
454
     * Builds instances of a {@link ClientStreamTracer} that increments the call count in the
455
     * tracker for each closed stream.
456
     */
457
    class ResultCountingClientStreamTracerFactory extends ClientStreamTracer.Factory {
458

459
      private final EndpointTracker tracker;
460

461
      @Nullable
462
      private final ClientStreamTracer.Factory delegateFactory;
463

464
      ResultCountingClientStreamTracerFactory(EndpointTracker tracker,
465
          @Nullable ClientStreamTracer.Factory delegateFactory) {
1✔
466
        this.tracker = tracker;
1✔
467
        this.delegateFactory = delegateFactory;
1✔
468
      }
1✔
469

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

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

498
  /**
499
   * Tracks additional information about the endpoint needed for outlier detection.
500
   */
501
  static class EndpointTracker {
502

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

512
    EndpointTracker(OutlierDetectionLoadBalancerConfig config) {
1✔
513
      this.config = config;
1✔
514
    }
1✔
515

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

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

536
    boolean removeSubchannel(OutlierDetectionSubchannel subchannel) {
537
      subchannel.clearEndpointTracker();
1✔
538
      return subchannels.remove(subchannel);
1✔
539
    }
540

541
    boolean containsSubchannel(OutlierDetectionSubchannel subchannel) {
542
      return subchannels.contains(subchannel);
×
543
    }
544

545
    @VisibleForTesting
546
    Set<OutlierDetectionSubchannel> getSubchannels() {
547
      return ImmutableSet.copyOf(subchannels);
1✔
548
    }
549

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

556
      if (success) {
1✔
557
        activeCallCounter.successCount.getAndIncrement();
1✔
558
      } else {
559
        activeCallCounter.failureCount.getAndIncrement();
1✔
560
      }
561
    }
1✔
562

563
    @VisibleForTesting
564
    long activeVolume() {
565
      return activeCallCounter.successCount.get() + activeCallCounter.failureCount.get();
1✔
566
    }
567

568
    long inactiveVolume() {
569
      return inactiveCallCounter.successCount.get() + inactiveCallCounter.failureCount.get();
1✔
570
    }
571

572
    double successRate() {
573
      return ((double) inactiveCallCounter.successCount.get()) / inactiveVolume();
1✔
574
    }
575

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

580
    void resetCallCounters() {
581
      activeCallCounter.reset();
1✔
582
      inactiveCallCounter.reset();
1✔
583
    }
1✔
584

585
    void decrementEjectionTimeMultiplier() {
586
      // The multiplier should not go negative.
587
      ejectionTimeMultiplier = ejectionTimeMultiplier == 0 ? 0 : ejectionTimeMultiplier - 1;
1✔
588
    }
1✔
589

590
    void resetEjectionTimeMultiplier() {
591
      ejectionTimeMultiplier = 0;
1✔
592
    }
1✔
593

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

608
    void ejectSubchannels(long ejectionTimeNanos) {
609
      this.ejectionTimeNanos = ejectionTimeNanos;
1✔
610
      ejectionTimeMultiplier++;
1✔
611
      for (OutlierDetectionSubchannel subchannel : subchannels) {
1✔
612
        subchannel.eject();
1✔
613
      }
1✔
614
    }
1✔
615

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

627
    boolean subchannelsEjected() {
628
      return ejectionTimeNanos != null;
1✔
629
    }
630

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

641
      return currentTimeNanos > maxEjectionTimeNanos;
1✔
642
    }
643

644
    /** Tracks both successful and failed call counts. */
645
    private static class CallCounter {
1✔
646
      AtomicLong successCount = new AtomicLong();
1✔
647
      AtomicLong failureCount = new AtomicLong();
1✔
648

649
      void reset() {
650
        successCount.set(0);
1✔
651
        failureCount.set(0);
1✔
652
      }
1✔
653
    }
654

655
    @Override
656
    public String toString() {
657
      return "EndpointTracker{"
×
658
              + "subchannels=" + subchannels
659
              + '}';
660
    }
661
  }
662

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

669
    EndpointTrackerMap() {
1✔
670
      trackerMap = new HashMap<>();
1✔
671
    }
1✔
672

673
    @Override
674
    protected Map<Set<SocketAddress>, EndpointTracker> delegate() {
675
      return trackerMap;
1✔
676
    }
677

678
    void updateTrackerConfigs(OutlierDetectionLoadBalancerConfig config) {
679
      for (EndpointTracker tracker: trackerMap.values()) {
1✔
680
        tracker.setConfig(config);
1✔
681
      }
1✔
682
    }
1✔
683

684
    /** Adds a new tracker for every given address. */
685
    void putNewTrackers(OutlierDetectionLoadBalancerConfig config,
686
        Set<Set<SocketAddress>> endpoints) {
687
      for (Set<SocketAddress> endpoint : endpoints) {
1✔
688
        if (!trackerMap.containsKey(endpoint)) {
1✔
689
          trackerMap.put(endpoint, new EndpointTracker(config));
1✔
690
        }
691
      }
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 Object childConfig;
961

962
    private OutlierDetectionLoadBalancerConfig(Long intervalNanos,
963
        Long baseEjectionTimeNanos,
964
        Long maxEjectionTimeNanos,
965
        Integer maxEjectionPercent,
966
        SuccessRateEjection successRateEjection,
967
        FailurePercentageEjection failurePercentageEjection,
968
        Object childConfig) {
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.childConfig = childConfig;
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
      Object childConfig;
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
      /**
1031
       * Sets the graceful child switch config the {@link OutlierDetectionLoadBalancer} delegates
1032
       * to.
1033
       */
1034
      public Builder setChildConfig(Object childConfig) {
1035
        checkState(childConfig != null);
1✔
1036
        this.childConfig = childConfig;
1✔
1037
        return this;
1✔
1038
      }
1039

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

1049
    /** The configuration for success rate ejection. */
1050
    public static class SuccessRateEjection {
1051

1052
      public final Integer stdevFactor;
1053
      public final Integer enforcementPercentage;
1054
      public final Integer minimumHosts;
1055
      public final Integer requestVolume;
1056

1057
      SuccessRateEjection(Integer stdevFactor, Integer enforcementPercentage, Integer minimumHosts,
1058
          Integer requestVolume) {
1✔
1059
        this.stdevFactor = stdevFactor;
1✔
1060
        this.enforcementPercentage = enforcementPercentage;
1✔
1061
        this.minimumHosts = minimumHosts;
1✔
1062
        this.requestVolume = requestVolume;
1✔
1063
      }
1✔
1064

1065
      /** Builds new instances of {@link SuccessRateEjection}. */
1066
      public static final class Builder {
1✔
1067

1068
        Integer stdevFactor = 1900;
1✔
1069
        Integer enforcementPercentage = 100;
1✔
1070
        Integer minimumHosts = 5;
1✔
1071
        Integer requestVolume = 100;
1✔
1072

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

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

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

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

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

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

1121
      FailurePercentageEjection(Integer threshold, Integer enforcementPercentage,
1122
          Integer minimumHosts, Integer requestVolume) {
1✔
1123
        this.threshold = threshold;
1✔
1124
        this.enforcementPercentage = enforcementPercentage;
1✔
1125
        this.minimumHosts = minimumHosts;
1✔
1126
        this.requestVolume = requestVolume;
1✔
1127
      }
1✔
1128

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

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

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

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

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

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

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