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

grpc / grpc-java / #19596

19 Dec 2024 03:54PM UTC coverage: 88.598% (-0.009%) from 88.607%
#19596

push

github

web-flow
Re-enable animalsniffer, fixing violations

In 61f19d707a I swapped the signatures to use the version catalog. But I
failed to preserve the `@signature` extension and it all seemed to
work... But in fact all the animalsniffer tasks were completing as
SKIPPED as they lacked signatures. The build.gradle changes in this
commit are to fix that while still using version catalog.

But while it was broken violations crept in. Most violations weren't
too important and we're not surprised went unnoticed. For example, Netty
with TLS has long required the Java 8 API
`setEndpointIdentificationAlgorithm()`, so using `Optional` in the same
code path didn't harm anything in particular. I still swapped it to
Guava's `Optional` to avoid overuse of `@IgnoreJRERequirement`.

One important violation has not been fixed and instead I've disabled the
android signature in api/build.gradle for the moment.  The violation is
in StatusException using the `fillInStackTrace` overload of Exception.
This problem [had been noticed][PR11066], but we couldn't figure out
what was going on. AnimalSniffer is now noticing this and agreeing with
the internal linter. There is still a question of why our interop tests
failed to notice this, but given they are no longer running on pre-API
level 24, that may forever be a mystery.

[PR11066]: https://github.com/grpc/grpc-java/pull/11066

33481 of 37790 relevant lines covered (88.6%)

0.89 hits per line

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

96.94
/../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
    switchLb.handleResolvedAddresses(
1✔
175
        resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config.childConfig).build());
1✔
176
    return Status.OK;
1✔
177
  }
178

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

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

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

195
    OutlierDetectionLoadBalancerConfig config;
196
    ChannelLogger logger;
197

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

203
    @Override
204
    public void run() {
205
      detectionTimerStartNanos = timeProvider.currentTimeNanos();
1✔
206

207
      endpointTrackerMap.swapCounters();
1✔
208

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

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

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

223
    private Helper delegate;
224

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

229
    @Override
230
    protected Helper delegate() {
231
      return delegate;
1✔
232
    }
233

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

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

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

254
      return subchannel;
1✔
255
    }
256

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

263
  class OutlierDetectionSubchannel extends ForwardingSubchannel {
264

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

404
      private final SubchannelStateListener delegate;
405

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

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

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

427

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

434
    private final SubchannelPicker delegate;
435

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

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

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

451
      return pickResult;
×
452
    }
453

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

460
      private final EndpointTracker tracker;
461

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

642
      return currentTimeNanos > maxEjectionTimeNanos;
1✔
643
    }
644

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

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

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

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

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

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

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

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

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

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

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

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

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

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

759

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

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

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

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

790
    private final OutlierDetectionLoadBalancerConfig config;
791

792
    private final ChannelLogger logger;
793

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

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

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

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

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

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

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

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

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

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

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

873
  static class FailurePercentageOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm {
874

875
    private final OutlierDetectionLoadBalancerConfig config;
876

877
    private final ChannelLogger logger;
878

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

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

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

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

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

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

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

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

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

955
    public final Long intervalNanos;
956
    public final Long baseEjectionTimeNanos;
957
    public final Long maxEjectionTimeNanos;
958
    public final Integer maxEjectionPercent;
959
    public final SuccessRateEjection successRateEjection;
960
    public final FailurePercentageEjection failurePercentageEjection;
961
    public final Object childConfig;
962

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

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

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

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

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

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

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

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

1031
      /**
1032
       * Sets the graceful child switch config the {@link OutlierDetectionLoadBalancer} delegates
1033
       * to.
1034
       */
1035
      public Builder setChildConfig(Object childConfig) {
1036
        checkState(childConfig != null);
1✔
1037
        this.childConfig = childConfig;
1✔
1038
        return this;
1✔
1039
      }
1040

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc