package com.nike.fastbreak; import com.nike.fastbreak.exception.CircuitBreakerOpenException; import com.nike.fastbreak.exception.CircuitBreakerTimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; /** * Implementation of {@link CircuitBreaker} that is modeled after the * <a href="http://doc.akka.io/docs/akka/2.4.1/common/circuitbreaker.html">Akka circuit breaker</a> - see that link for * more info. * * <p>See the {@link Builder} (and associated static factory methods {@link #newBuilder()} and {@link * #newBuilder(BreakingEventStrategy)}) for an easy way to build instances of this class with the behavior options you * want. * * <p>Details: * <ul> * <li>Circuit breaking events are determined by passing the event through {@link #breakingEventStrategy}</li> * <li> * Circuit breaking exceptions are determined by passing the exception through {@link * #breakingExceptionStrategy} * </li> * <li> * After {@link #maxConsecutiveFailuresAllowed} consecutive breaking failures the circuit will be set to OPEN * state, causing all subsequent calls to short circuit and immediately throw a {@link * CircuitBreakerOpenException} as long as the circuit is OPEN. * </li> * <li> * Once opened, the circuit will stay open for {@link #resetTimeoutNanos}, after which a single call will be * allowed through. This is the "half open" state. * </li> * <li> * If the half-open call succeeds then the circuit will be closed again, allowing all calls through. If the * half-open call fails, then the circuit will remain open for another {@link #resetTimeoutNanos}. * </li> * <li> * For {@link #executeAsyncCall(Supplier)} and {@link #executeBlockingCall(Callable)} there's an optional * {@link #callTimeoutNanos}. If the call takes longer than the {@link #callTimeoutNanos} then a {@link * CircuitBreakerTimeoutException} will be thrown and will count against the number of consecutive failures * allowed. * </li> * </ul> * * @param <ET> The event type this circuit breaker knows how to handle. {@link #breakingEventStrategy} is used to * determine whether a given event contributes to the consecutive breaking failures count or not. * * @author Nic Munroe */ @SuppressWarnings({"WeakerAccess", "OptionalUsedAsFieldOrParameterType"}) public class CircuitBreakerImpl<ET> implements CircuitBreaker<ET>, CircuitBreaker.ManualModeTask<ET> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); // Static defaults /** * Override the default max consecutive failures ({@link #FALLBACK_DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED}) by * setting this System Property key to an integer value on JVM startup. */ public static final String DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED_SYSTEM_PROP_KEY = "fastbreak.defaultMaxConsecutiveFailuresAllowed"; /** * Override the default reset timeout in seconds ({@link #FALLBACK_DEFAULT_RESET_TIMEOUT_SECONDS}) by setting this * System Property key to an integer value on JVM startup. */ public static final String DEFAULT_RESET_TIMEOUT_SECONDS_SYSTEM_PROP_KEY = "fastbreak.defaultResetTimeoutInSeconds"; /** * This is the default number of max consecutive failures that will be used if unspecified for a given circuit * breaker. It will be set to {@link #DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED_SYSTEM_PROP_KEY} if that System * Property is set (and greater than or equal to 1), otherwise it will fallback to {@link * #FALLBACK_DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED}. */ public static final int DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED; /** * This is the default reset timeout value (in seconds) will be used if unspecified for a given circuit * breaker. It will be set to {@link #DEFAULT_RESET_TIMEOUT_SECONDS_SYSTEM_PROP_KEY} if that System * Property is set (and greater than or equal to 1), otherwise it will fallback to {@link * #FALLBACK_DEFAULT_RESET_TIMEOUT_SECONDS}. */ public static final long DEFAULT_RESET_TIMEOUT_SECONDS; /** * This will be used to set {@link #DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED} if the {@link * #DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED_SYSTEM_PROP_KEY} System Property is not set. */ public static final int FALLBACK_DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED = 20; /** * This will be used to set {@link #DEFAULT_RESET_TIMEOUT_SECONDS} if the {@link * #DEFAULT_RESET_TIMEOUT_SECONDS_SYSTEM_PROP_KEY} System Property is not set. */ public static final long FALLBACK_DEFAULT_RESET_TIMEOUT_SECONDS = 15; /** * The default {@link BreakingEventStrategy} that will be used if not specified for a given circuit breaker * instance - assumes all events are successful/healthy events. */ public static final BreakingEventStrategy<?> DEFAULT_BREAKING_EVENT_STRATEGY = event -> false; /** * The default {@link BreakingExceptionStrategy} that will be used if not specified for a given circuit breaker * instance - assumes all exceptions are failure/unhealthy events. */ public static final BreakingExceptionStrategy DEFAULT_BREAKING_EXCEPTION_STRATEGY = throwable -> true; // The default ScheduledExecutorService and ExecutorService will not be created until required - that way if your // application never needs the defaults (because you pass in custom ones for each circuit breaker) you won't // have extra threads spun up that will never be used. private static ScheduledExecutorService DEFAULT_SCHEDULED_EXECUTOR_SERVICE; private static ExecutorService DEFAULT_STATE_CHANGE_NOTIFICATION_EXECUTOR_SERVICE; static { // Retrieve the configurable defaults from System properties if available, otherwise use hardcoded defaults. DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED = Math.max( Integer.parseInt( System.getProperty(DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED_SYSTEM_PROP_KEY, String.valueOf(FALLBACK_DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED) ) ), 1 // Minimum of 1, no matter what is found in the System Properties. ); DEFAULT_RESET_TIMEOUT_SECONDS = Math.max( Long.parseLong( System.getProperty(DEFAULT_RESET_TIMEOUT_SECONDS_SYSTEM_PROP_KEY, String.valueOf(FALLBACK_DEFAULT_RESET_TIMEOUT_SECONDS) ) ), 1 // Minimum of 1, no matter what is found in the System Properties. ); } // Things that may change during CircuitBreaker execution (either reference changes or object state changes) protected int consecutiveFailureCount = 0; protected final AtomicBoolean halfOpenAllowSingleCall = new AtomicBoolean(false); protected final AtomicReference<ScheduledFuture<?>> halfOpenScheduledFuture = new AtomicReference<>(); protected State currentState = State.CLOSED; protected final List<Runnable> onCloseListeners = new CopyOnWriteArrayList<>(); protected final List<Runnable> onHalfOpenListeners = new CopyOnWriteArrayList<>(); protected final List<Runnable> onOpenListeners = new CopyOnWriteArrayList<>(); // Immutables (set in the constructor) protected final ScheduledExecutorService scheduler; protected final ExecutorService stateChangeNotificationExecutor; protected final long resetTimeoutNanos; protected final String resetTimeoutDurationString; protected final Optional<Long> callTimeoutNanos; protected final int maxConsecutiveFailuresAllowed; protected final String id; protected final BreakingEventStrategy<ET> breakingEventStrategy; protected final BreakingExceptionStrategy breakingExceptionStrategy; protected enum State { CLOSED, OPEN } /** * Constructor that passes {@link Optional#empty()} to {@link CircuitBreakerImpl#CircuitBreakerImpl(Optional, * Optional, Optional, Optional, Optional, Optional, Optional, Optional)} for every argument, creating an instance * that uses defaults for everything. See that constructor for details on the defaults that will be used. */ public CircuitBreakerImpl() { this(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); } /** * The kitchen-sink constructor. * * @param scheduler The scheduler to use for scheduling half-open timeouts and call timeouts. If this is empty, then * a default scheduler will be used. Passing in your own can save you some threads if you already * have a scheduler you're using (e.g. Netty event loop). * @param stateChangeNotificationExecutor * An executor that will be used for executing the {@link #onClose(Runnable)}, {@link * #onHalfOpen(Runnable)}, and {@link #onOpen(Runnable)} notifications. If this is empty, then a default * executor will be used. If you want control over the extra threads then you should pass in your own * executor. * @param maxConsecutiveFailuresAllowed * The max number of consecutive failures that are allowed before the circuit enters OPEN state. If this is * empty then a default will be used. The default value can be controlled by the {@link * #DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED_SYSTEM_PROP_KEY} System property, or if all else fails a * hardcoded default value will be used ({@link #FALLBACK_DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED}). * @param resetTimeout The amount of time to wait after entering OPEN state before allowing a half-open call * through. If this is empty then a default will be used. The default value can be controlled by * the {@link #DEFAULT_RESET_TIMEOUT_SECONDS_SYSTEM_PROP_KEY} System property, or if all else * fails a hardcoded default value will be used * ({@link #FALLBACK_DEFAULT_RESET_TIMEOUT_SECONDS}). * @param callTimeout The amount of time to allow a call to execute (via {@link #executeAsyncCall(Supplier)} or * {@link #executeBlockingCall(Callable)}) before a {@link CircuitBreakerTimeoutException} will * be thrown. If this is empty then calls will never timeout. * @param id The ID of this circuit breaker. Used in log messages to ID this instance. If this is empty then {@link * CircuitBreaker#DEFAULT_ID} + "-" + {@link UUID#randomUUID()} will be used. * @param breakingEventStrategy The function to decide whether an event contributes to the consecutive breaking * failures count or not. * @param breakingExceptionStrategy The function to decide whether an exception contributes to the consecutive * breaking failures count or not. */ public CircuitBreakerImpl(Optional<ScheduledExecutorService> scheduler, Optional<ExecutorService> stateChangeNotificationExecutor, Optional<Integer> maxConsecutiveFailuresAllowed, Optional<Duration> resetTimeout, Optional<Duration> callTimeout, Optional<String> id, Optional<BreakingEventStrategy<ET>> breakingEventStrategy, Optional<BreakingExceptionStrategy> breakingExceptionStrategy) { this.scheduler = scheduler.orElseGet(this::getDefaultScheduledExecutorService); this.stateChangeNotificationExecutor = stateChangeNotificationExecutor.orElseGet(this::getDefaultStateChangeNotificationExecutorService); this.maxConsecutiveFailuresAllowed = maxConsecutiveFailuresAllowed.orElse(DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED); this.resetTimeoutNanos = resetTimeout.map(Duration::toNanos) .orElse(Duration.ofSeconds(DEFAULT_RESET_TIMEOUT_SECONDS).toNanos()); this.resetTimeoutDurationString = Duration.ofNanos(this.resetTimeoutNanos).toString(); this.callTimeoutNanos = callTimeout.map(Duration::toNanos); this.id = id.orElse(DEFAULT_ID + "-" + UUID.randomUUID().toString()); //noinspection unchecked this.breakingEventStrategy = breakingEventStrategy.orElse((BreakingEventStrategy<ET>) DEFAULT_BREAKING_EVENT_STRATEGY); this.breakingExceptionStrategy = breakingExceptionStrategy.orElse(DEFAULT_BREAKING_EXCEPTION_STRATEGY); } /** * @return A new blank {@link Builder} for creating a {@link CircuitBreakerImpl} with the options you want. */ public static <E> Builder<E> newBuilder() { return new Builder<>(); } /** * @return A new {@link Builder} for creating a {@link CircuitBreakerImpl} with the options you want that is * initialized with the given {@link BreakingEventStrategy}. */ public static <E> Builder<E> newBuilder(BreakingEventStrategy<E> breakingEventStrategy) { return new Builder<>(breakingEventStrategy); } protected ScheduledExecutorService getDefaultScheduledExecutorService() { synchronized (CircuitBreakerImpl.class) { if (DEFAULT_SCHEDULED_EXECUTOR_SERVICE == null) { logger.info("Creating default ScheduledExecutorService for CircuitBreaker"); ScheduledThreadPoolExecutor schedulerToUse = new ScheduledThreadPoolExecutor(8); // Set the scheduler to remove the tasks immediately upon cancellation (prevents holding on to memory // that we'd prefer get GC'd immediately). schedulerToUse.setRemoveOnCancelPolicy(true); DEFAULT_SCHEDULED_EXECUTOR_SERVICE = schedulerToUse; } } return DEFAULT_SCHEDULED_EXECUTOR_SERVICE; } protected ExecutorService getDefaultStateChangeNotificationExecutorService() { synchronized (CircuitBreakerImpl.class) { if (DEFAULT_STATE_CHANGE_NOTIFICATION_EXECUTOR_SERVICE == null) { logger.info("Creating default state change notification ExecutorService for CircuitBreaker"); DEFAULT_STATE_CHANGE_NOTIFICATION_EXECUTOR_SERVICE = Executors.newCachedThreadPool(); } } return DEFAULT_STATE_CHANGE_NOTIFICATION_EXECUTOR_SERVICE; } @Override public ManualModeTask<ET> newManualModeTask() { // This CircuitBreaker implementation does not have any state that matters on a per-task basis, so we are safe // to implement ManualModeTask at the CircuitBreaker level and save the cost of creating new ManualModeTask // objects every time this method is called. return this; } @Override public void throwExceptionIfCircuitBreakerIsOpen() throws CircuitBreakerOpenException { // If the circuit is closed then there's no need for an exception if (State.CLOSED.equals(currentState)) return; // The circuit is open. See if it's half-open or fully open. Half-open should allow a single call through. // Fully open should throw an exception. boolean allowThisCallThrough = halfOpenAllowSingleCall.getAndSet(false); if (allowThisCallThrough) { logger.debug("Allowing a single half-open call through. circuit_breaker_id={}", id); // Schedule another half-open timeout check in case this call fails. scheduleHalfOpenStateTimeout(); return; } // The circuit is fully open - throw the exception throw new CircuitBreakerOpenException( id, "Cannot make the desired call - the circuit breaker is open. circuit_breaker_id=" + id ); } @Override public void handleEvent(ET event) { try { if (isEventACircuitBreakerFailure(event)) { // This event is one that might have tripped the circuit breaker. processFailureCall(); } else { // This event is considered a successful call. processSuccessfulCall(); } } catch (Throwable t) { logger.error( "Unexpected exception caught while trying to handleEvent(...). This indicates the CircuitBreaker is " + "malfunctioning, but since this method should never throw an exception it will be swallowed.", t ); } } @Override public void handleException(Throwable throwable) { try { throwable = unwrapAsyncExceptions(throwable); // CircuitBreakerOpenExceptions are not considered successes or failures. It's just the status quo while the // circuit is open. if (throwable instanceof CircuitBreakerOpenException) return; if (isExceptionACircuitBreakerFailure(throwable)) { // This exception is one that might have tripped the circuit breaker. processFailureCall(); } else { // This exception is considered a successful call. processSuccessfulCall(); } } catch (Throwable t) { logger.error( "Unexpected exception caught while trying to handleException(...). This indicates the CircuitBreaker is " + "malfunctioning, but since this method should never throw an exception it will be swallowed.", t ); } } @Override public CircuitBreaker<ET> originatingCircuitBreaker() { return this; } @Override public CompletableFuture<ET> executeAsyncCall(Supplier<CompletableFuture<ET>> eventFutureSupplier) throws CircuitBreakerOpenException { // Explode if the circuit breaker is open throwExceptionIfCircuitBreakerIsOpen(); // No CircuitBreakerOpenException was thrown so this call is allowed through. // Get the future and wire it up to call the appropriate handle*() method when it finishes. CompletableFuture<ET> eventFuture = eventFutureSupplier.get(); eventFuture.whenComplete((event, error) -> { if (error == null) handleEvent(event); else handleException(error); }); // If desired then schedule a timeout for this future in case it takes too long. callTimeoutNanos.ifPresent(timeoutNanos -> { // Schedule the timeout check ScheduledFuture<?> timeoutCheckFuture = scheduler.schedule(() -> { if (!eventFuture.isDone()) { eventFuture.completeExceptionally(generateNewCircuitBreakerTimeoutException(timeoutNanos)); } }, timeoutNanos, TimeUnit.NANOSECONDS); // Configure the event future to delete the timeout check immediately when it finishes. This is necessary // for the event future to be GC'd immediately. If we don't do this then the event future and all its // memory is stuck waiting for the timeout check future to complete. eventFuture.whenComplete((event, error) -> { if (!timeoutCheckFuture.isDone()) timeoutCheckFuture.cancel(false); }); }); return eventFuture; } @Override public ET executeBlockingCall(Callable<ET> eventSupplier) throws Exception { // Explode if the circuit breaker is open throwExceptionIfCircuitBreakerIsOpen(); // No CircuitBreakerOpenException was thrown so this call is allowed through. // Get the event or an exception thrown by the eventSupplier and time how long it takes. ET event = null; Exception eventException = null; long beforeCallNanos = System.nanoTime(); try { // Time the call so we know whether we need to throw a CircuitBreakerTimeoutException or not. event = eventSupplier.call(); } catch (Exception ex) { eventException = ex; } long afterCallNanos = System.nanoTime(); // Determine if the call took longer than the callTimeout. Exception timeoutException = null; if (callTimeoutNanos.isPresent()) { long timeoutNanos = callTimeoutNanos.get(); if ((afterCallNanos - beforeCallNanos) > timeoutNanos) { // The call took longer than our callTimeout value. //noinspection ThrowableResultOfMethodCallIgnored timeoutException = generateNewCircuitBreakerTimeoutException(timeoutNanos); } } // Determine if we need to handleException(). Call timeouts take precedence. Exception exceptionToHandle = (timeoutException == null) ? eventException : timeoutException; if (exceptionToHandle != null) { handleException(exceptionToHandle); throw exceptionToHandle; } // Call completed under the timeout limit (or there was no timeout limit), and the call did not throw an // exception. Handle the event and return it. handleEvent(event); return event; } @Override public CircuitBreakerImpl<ET> onClose(Runnable listener) { onCloseListeners.add(listener); return this; } @Override public CircuitBreakerImpl<ET> onHalfOpen(Runnable listener) { onHalfOpenListeners.add(listener); return this; } @Override public CircuitBreakerImpl<ET> onOpen(Runnable listener) { onOpenListeners.add(listener); return this; } @Override public String getId() { return id; } protected CircuitBreakerTimeoutException generateNewCircuitBreakerTimeoutException(long timeoutNanos) { Duration duration = Duration.ofNanos(timeoutNanos); return new CircuitBreakerTimeoutException( id, "The CompletableFuture took longer than the CircuitBreaker timeout of " + duration.toString() + ". circuit_breaker_id=" + id ); } protected void processSuccessfulCall() { boolean stateChanged = false; synchronized (this) { // Reset the consecutive failure counter, make sure we're set to CLOSED state, and capture whether this is a // state change or not. consecutiveFailureCount = 0; if (!State.CLOSED.equals(currentState)) { logger.info( "Setting circuit breaker state to CLOSED after successful call. " + "circuit_breaker_state_changed_to={}, circuit_breaker_id={}", State.CLOSED.name(), id ); currentState = State.CLOSED; // Cancel any existing half-open scheduled timer. ScheduledFuture<?> oldHalfOpenScheduledFuture = halfOpenScheduledFuture.getAndSet(null); if (oldHalfOpenScheduledFuture != null && !oldHalfOpenScheduledFuture.isDone()) { logger.debug( "Cancelling half-open timeout check now that the circuit is closed. circuit_breaker_id={}", id ); oldHalfOpenScheduledFuture.cancel(false); } stateChanged = true; } } if (stateChanged) { // On this particular call we went from OPEN to CLOSED. Notify listeners. notifyOnCloseListeners(); } } protected void processFailureCall() { boolean stateChanged = false; synchronized (this) { // Increment the consecutive failure counter and then see if the breaker is tripped. consecutiveFailureCount++; if (consecutiveFailureCount >= maxConsecutiveFailuresAllowed) { // We are over the limit. Make sure our state is set to OPEN, and capture whether this is a state change // or not. if (!State.OPEN.equals(currentState)) { logger.info( "Tripping circuit breaker OPEN after {} consecutive failure calls. " + "circuit_breaker_state_changed_to={}, circuit_breaker_id={}", consecutiveFailureCount, State.OPEN.name(), id ); halfOpenAllowSingleCall.set(false); currentState = State.OPEN; // Schedule the half-open timeout scheduleHalfOpenStateTimeout(); stateChanged = true; } } } if (stateChanged) { // On this particular call we went from CLOSED to OPEN, so the circuit is tripped. Notify listeners. notifyOnOpenListeners(); } } protected void scheduleHalfOpenStateTimeout() { logger.debug("Scheduling half-open state to occur in {}. circuit_breaker_id={}", resetTimeoutDurationString, id); // Schedule a half-open check after the reset timeout halfOpenScheduledFuture.set(scheduler.schedule(() -> { boolean stateChanged = false; synchronized (this) { if (State.CLOSED.equals(currentState)) { // The circuit has closed since this timer started. We have nothing to do. logger.debug( "Half-open state check - breaker is already CLOSED. Nothing to do. circuit_breaker_id={}", id); } else { // The circuit is still open. Allow a single half-open call. logger.info( "Half-open state check - breaker is OPEN. The next call will be allowed through in a half-open " + "state. circuit_breaker_state_changed_to={}, circuit_breaker_id={}", "HALF_OPEN", id ); halfOpenAllowSingleCall.set(true); stateChanged = true; } } if (stateChanged) { notifyOnHalfOpenListeners(); } }, resetTimeoutNanos, TimeUnit.NANOSECONDS)); } protected void notifyOnCloseListeners() { onCloseListeners.forEach(stateChangeNotificationExecutor::execute); } protected void notifyOnHalfOpenListeners() { onHalfOpenListeners.forEach(stateChangeNotificationExecutor::execute); } protected void notifyOnOpenListeners() { onOpenListeners.forEach(stateChangeNotificationExecutor::execute); } protected boolean isEventACircuitBreakerFailure(ET event) { return breakingEventStrategy.isEventACircuitBreakerFailure(event); } protected boolean isExceptionACircuitBreakerFailure(Throwable throwable) { return breakingExceptionStrategy.isExceptionACircuitBreakerFailure(throwable); } protected Throwable unwrapAsyncExceptions(Throwable error) { if (error == null || error.getCause() == null) return error; if (error instanceof CompletionException || error instanceof ExecutionException) { error = error.getCause(); // Recursively unwrap until we get something that is not unwrappable error = unwrapAsyncExceptions(error); } return error; } /** * Builder class for {@link CircuitBreakerImpl}. * * @param <E> The event type the resulting circuit breaker knows how to handle. {@link #breakingEventStrategy} is * used to determine whether a given event contributes to the consecutive breaking failures count or not. */ public static class Builder<E> { private ScheduledExecutorService scheduler; private ExecutorService stateChangeNotificationExecutor; private Integer maxConsecutiveFailuresAllowed; private Duration resetTimeout; private Duration callTimeout; private String id; private BreakingEventStrategy<E> breakingEventStrategy; private BreakingExceptionStrategy breakingExceptionStrategy; public Builder() { // Do nothing } public Builder(BreakingEventStrategy<E> breakingEventStrategy) { withBreakingEventStrategy(breakingEventStrategy); } /** * @param scheduler The scheduler to use for scheduling half-open timeouts and call timeouts. If this is null, * then a default scheduler will be used. Passing in your own can save you some threads if you * already have a scheduler you're using (e.g. Netty event loop). * @return this builder instance. */ public Builder<E> withScheduler(ScheduledExecutorService scheduler) { this.scheduler = scheduler; return this; } /** * @param stateChangeNotificationExecutor * An executor that will be used for executing the {@link #onClose(Runnable)}, {@link * #onHalfOpen(Runnable)}, and {@link #onOpen(Runnable)} notifications. If this is null, then a * default executor will be used. If you want control over the extra threads then you should pass in * your own executor. * @return this builder instance. */ public Builder<E> withStateChangeNotificationExecutor(ExecutorService stateChangeNotificationExecutor) { this.stateChangeNotificationExecutor = stateChangeNotificationExecutor; return this; } /** * @param maxConsecutiveFailuresAllowed * The max number of consecutive failures that are allowed before the circuit enters OPEN state. If * this is null then a default will be used. The default value can be controlled by the {@link * #DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED_SYSTEM_PROP_KEY} System property, or if all else fails a * hardcoded default value will be used ({@link #FALLBACK_DEFAULT_MAX_CONSECUTIVE_FAILURES_ALLOWED}). * @return this builder instance. */ public Builder<E> withMaxConsecutiveFailuresAllowed(Integer maxConsecutiveFailuresAllowed) { this.maxConsecutiveFailuresAllowed = maxConsecutiveFailuresAllowed; return this; } /** * @param resetTimeout The amount of time to wait after entering OPEN state before allowing a half-open call * through. If this is null then a default will be used. The default value can be * controlled by the {@link #DEFAULT_RESET_TIMEOUT_SECONDS_SYSTEM_PROP_KEY} System property, * or if all else fails a hardcoded default value will be used ({@link * #FALLBACK_DEFAULT_RESET_TIMEOUT_SECONDS}). * @return this builder instance. */ public Builder<E> withResetTimeout(Duration resetTimeout) { this.resetTimeout = resetTimeout; return this; } /** * @param callTimeout The amount of time to allow a call to execute (via {@link #executeAsyncCall(Supplier)} or * {@link #executeBlockingCall(Callable)}) before a {@link CircuitBreakerTimeoutException} * will be thrown. If this is null then calls will never timeout. * @return this builder instance. */ public Builder<E> withCallTimeout(Duration callTimeout) { this.callTimeout = callTimeout; return this; } /** * @param id The ID of the circuit breaker. Used in log messages to ID this instance. If this is empty then * {@link #DEFAULT_ID} will be used. * @return this builder instance. */ public Builder<E> withId(String id) { this.id = id; return this; } /** * @param breakingEventStrategy The function to decide whether an event contributes to the consecutive breaking * failures count or not. * @return this builder instance. */ public Builder<E> withBreakingEventStrategy(BreakingEventStrategy<E> breakingEventStrategy) { this.breakingEventStrategy = breakingEventStrategy; return this; } /** * @param breakingExceptionStrategy The function to decide whether an exception contributes to the consecutive * breaking failures count or not. * @return this builder instance. */ public Builder<E> withBreakingExceptionStrategy(BreakingExceptionStrategy breakingExceptionStrategy) { this.breakingExceptionStrategy = breakingExceptionStrategy; return this; } /** * @return The {@link CircuitBreakerImpl} created from the options set on this builder. */ public CircuitBreakerImpl<E> build() { return new CircuitBreakerImpl<>( Optional.ofNullable(scheduler), Optional.ofNullable(stateChangeNotificationExecutor), Optional.ofNullable(maxConsecutiveFailuresAllowed), Optional.ofNullable(resetTimeout), Optional.ofNullable(callTimeout), Optional.ofNullable(id), Optional.ofNullable(breakingEventStrategy), Optional.ofNullable(breakingExceptionStrategy) ); } } }