/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.client.circuitbreaker;
import static java.util.Objects.requireNonNull;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
/**
* Builds a {@link CircuitBreaker} instance using builder pattern.
*/
public final class CircuitBreakerBuilder {
private static final class Defaults {
private static final double FAILURE_RATE_THRESHOLD = 0.8;
private static final ExceptionFilter EXCEPTION_FILTER = cause -> true;
private static final long MINIMUM_REQUEST_THRESHOLD = 10;
private static final Duration TRIAL_REQUEST_INTERVAL = Duration.ofSeconds(3);
private static final Duration CIRCUIT_OPEN_WINDOW = Duration.ofSeconds(10);
private static final Duration COUNTER_SLIDING_WINDOW = Duration.ofSeconds(20);
private static final Duration COUNTER_UPDATE_INTERVAL = Duration.ofSeconds(1);
private static final Ticker TICKER = Ticker.systemTicker();
}
private final Optional<String> name;
private double failureRateThreshold = Defaults.FAILURE_RATE_THRESHOLD;
private ExceptionFilter exceptionFilter = Defaults.EXCEPTION_FILTER;
private long minimumRequestThreshold = Defaults.MINIMUM_REQUEST_THRESHOLD;
private Duration trialRequestInterval = Defaults.TRIAL_REQUEST_INTERVAL;
private Duration circuitOpenWindow = Defaults.CIRCUIT_OPEN_WINDOW;
private Duration counterSlidingWindow = Defaults.COUNTER_SLIDING_WINDOW;
private Duration counterUpdateInterval = Defaults.COUNTER_UPDATE_INTERVAL;
private Ticker ticker = Defaults.TICKER;
private List<CircuitBreakerListener> listeners = Collections.emptyList();
/**
* Creates a new {@link CircuitBreakerBuilder} with the specified name.
*
* @param name The name of the circuit breaker.
*/
public CircuitBreakerBuilder(String name) {
requireNonNull(name, "name");
if (name.isEmpty()) {
throw new IllegalArgumentException("name: <empty> (expected: a non-empty string)");
}
this.name = Optional.of(name);
}
/**
* Creates a new {@link CircuitBreakerBuilder}.
*/
public CircuitBreakerBuilder() {
name = Optional.empty();
}
/**
* Sets the threshold of failure rate to detect a remote service fault.
*
* @param failureRateThreshold The rate between 0 (exclusive) and 1 (inclusive)
*/
public CircuitBreakerBuilder failureRateThreshold(double failureRateThreshold) {
if (failureRateThreshold <= 0 || 1 < failureRateThreshold) {
throw new IllegalArgumentException(
"failureRateThreshold: " + failureRateThreshold + " (expected: > 0 and <= 1)");
}
this.failureRateThreshold = failureRateThreshold;
return this;
}
/**
* Sets the minimum number of requests within a time window necessary to detect a remote service fault.
*/
public CircuitBreakerBuilder minimumRequestThreshold(long minimumRequestThreshold) {
if (minimumRequestThreshold < 0) {
throw new IllegalArgumentException(
"minimumRequestThreshold: " + minimumRequestThreshold + " (expected: >= 0)");
}
this.minimumRequestThreshold = minimumRequestThreshold;
return this;
}
/**
* Sets the trial request interval in HALF_OPEN state.
*/
public CircuitBreakerBuilder trialRequestInterval(Duration trialRequestInterval) {
requireNonNull(trialRequestInterval, "trialRequestInterval");
if (trialRequestInterval.isNegative() || trialRequestInterval.isZero()) {
throw new IllegalArgumentException(
"trialRequestInterval: " + trialRequestInterval + " (expected: > 0)");
}
this.trialRequestInterval = trialRequestInterval;
return this;
}
/**
* Sets the trial request interval in HALF_OPEN state.
*/
public CircuitBreakerBuilder trialRequestIntervalMillis(long trialRequestIntervalMillis) {
trialRequestInterval(Duration.ofMillis(trialRequestIntervalMillis));
return this;
}
/**
* Sets the duration of OPEN state.
*/
public CircuitBreakerBuilder circuitOpenWindow(Duration circuitOpenWindow) {
requireNonNull(circuitOpenWindow, "circuitOpenWindow");
if (circuitOpenWindow.isNegative() || circuitOpenWindow.isZero()) {
throw new IllegalArgumentException(
"circuitOpenWindow: " + circuitOpenWindow + " (expected: > 0)");
}
this.circuitOpenWindow = circuitOpenWindow;
return this;
}
/**
* Sets the duration of OPEN state.
*/
public CircuitBreakerBuilder circuitOpenWindowMillis(long circuitOpenWindowMillis) {
circuitOpenWindow(Duration.ofMillis(circuitOpenWindowMillis));
return this;
}
/**
* Sets the time length of sliding window to accumulate the count of events.
*/
public CircuitBreakerBuilder counterSlidingWindow(Duration counterSlidingWindow) {
requireNonNull(counterSlidingWindow, "counterSlidingWindow");
if (counterSlidingWindow.isNegative() || counterSlidingWindow.isZero()) {
throw new IllegalArgumentException(
"counterSlidingWindow: " + counterSlidingWindow + " (expected: > 0)");
}
this.counterSlidingWindow = counterSlidingWindow;
return this;
}
/**
* Sets the time length of sliding window to accumulate the count of events.
*/
public CircuitBreakerBuilder counterSlidingWindowMillis(long counterSlidingWindowMillis) {
counterSlidingWindow(Duration.ofMillis(counterSlidingWindowMillis));
return this;
}
/**
* Sets the interval that a circuit breaker can see the latest accumulated count of events.
*/
public CircuitBreakerBuilder counterUpdateInterval(Duration counterUpdateInterval) {
requireNonNull(counterUpdateInterval, "counterUpdateInterval");
if (counterUpdateInterval.isNegative() || counterUpdateInterval.isZero()) {
throw new IllegalArgumentException(
"counterUpdateInterval: " + counterUpdateInterval + " (expected: > 0)");
}
this.counterUpdateInterval = counterUpdateInterval;
return this;
}
/**
* Sets the interval that a circuit breaker can see the latest accumulated count of events.
*/
public CircuitBreakerBuilder counterUpdateIntervalMillis(long counterUpdateIntervalMillis) {
counterUpdateInterval(Duration.ofMillis(counterUpdateIntervalMillis));
return this;
}
/**
* Sets the {@link ExceptionFilter} that decides whether the circuit breaker should deal with a given error.
*/
public CircuitBreakerBuilder exceptionFilter(ExceptionFilter exceptionFilter) {
this.exceptionFilter = requireNonNull(exceptionFilter, "exceptionFilter");
return this;
}
/**
* Adds a {@link CircuitBreakerListener}.
*/
public CircuitBreakerBuilder listener(CircuitBreakerListener listener) {
requireNonNull(listener, "listener");
if (listeners.isEmpty()) {
listeners = new ArrayList<>(3);
}
listeners.add(listener);
return this;
}
@VisibleForTesting
CircuitBreakerBuilder ticker(Ticker ticker) {
this.ticker = requireNonNull(ticker, "ticker");
return this;
}
/**
* Builds a {@link CircuitBreaker} instance.
*/
public CircuitBreaker build() {
if (counterSlidingWindow.compareTo(counterUpdateInterval) <= 0) {
throw new IllegalStateException(
"counterSlidingWindow: " + counterSlidingWindow + " (expected: > counterUpdateInterval)");
}
return new NonBlockingCircuitBreaker(
ticker,
new CircuitBreakerConfig(name, failureRateThreshold, minimumRequestThreshold,
circuitOpenWindow, trialRequestInterval,
counterSlidingWindow, counterUpdateInterval,
exceptionFilter, Collections.unmodifiableList(listeners)));
}
}