All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.testifyproject.failsafe.CircuitBreaker Maven / Gradle / Ivy

There is a newer version: 1.0.6
Show newest version
/*
 * Copyright 2016 the original author or authors.
 *
 * Licensed 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 org.testifyproject.failsafe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.testifyproject.failsafe.function.BiPredicate;
import org.testifyproject.failsafe.function.CheckedRunnable;
import org.testifyproject.failsafe.function.Predicate;
import org.testifyproject.failsafe.internal.CircuitBreakerStats;
import org.testifyproject.failsafe.internal.CircuitState;
import org.testifyproject.failsafe.internal.ClosedState;
import org.testifyproject.failsafe.internal.HalfOpenState;
import org.testifyproject.failsafe.internal.OpenState;
import org.testifyproject.failsafe.internal.util.Assert;
import org.testifyproject.failsafe.util.Duration;
import org.testifyproject.failsafe.util.Ratio;

/**
 * A circuit breaker that temporarily halts execution when configurable thresholds are exceeded.
 * 
 * @author Jonathan Halterman
 */
public class CircuitBreaker {
  /** Writes guarded by "this" */
  private final AtomicReference state = new AtomicReference();
  private final AtomicInteger currentExecutions = new AtomicInteger();
  private final CircuitBreakerStats stats = new CircuitBreakerStats() {
    @Override
    public int getCurrentExecutions() {
      return currentExecutions.get();
    }
  };
  private Duration delay = Duration.NONE;
  private Duration timeout;
  private Ratio failureThreshold;
  private Ratio successThreshold;
  /** Indicates whether failures are checked by a configured failure condition */
  private boolean failuresChecked;
  private List> failureConditions;
  CheckedRunnable onOpen;
  CheckedRunnable onHalfOpen;
  CheckedRunnable onClose;

  /**
   * Creates a Circuit that opens after a single failure, closes after a single success, and has no delay by default.
   */
  public CircuitBreaker() {
    failureConditions = new ArrayList>();
    state.set(new ClosedState(this));
  }

  /**
   * The state of the circuit.
   */
  public enum State {
    /* The circuit is closed and fully functional, allowing executions to occur. */
    CLOSED,
    /* The circuit is opened and not allowing executions to occur. */
    OPEN,
    /* The circuit is temporarily allowing executions to occur. */
    HALF_OPEN;
  }

  /**
   * Returns whether the circuit allows execution, possibly triggering a state transition.
   */
  public boolean allowsExecution() {
    return state.get().allowsExecution(stats);
  }

  /**
   * Closes the circuit.
   */
  public void close() {
    transitionTo(State.CLOSED, onClose);
  }

  /**
   * Specifies that a failure should be recorded if the {@code completionPredicate} matches the completion result.
   * 
   * @throws NullPointerException if {@code completionPredicate} is null
   */
  @SuppressWarnings("unchecked")
  public  CircuitBreaker failIf(BiPredicate completionPredicate) {
    Assert.notNull(completionPredicate, "completionPredicate");
    failuresChecked = true;
    failureConditions.add((BiPredicate) completionPredicate);
    return this;
  }

  /**
   * Specifies that a failure should be recorded if the {@code resultPredicate} matches the result.
   * 
   * @throws NullPointerException if {@code resultPredicate} is null
   */
  public  CircuitBreaker failIf(Predicate resultPredicate) {
    Assert.notNull(resultPredicate, "resultPredicate");
    failureConditions.add(Predicates.resultPredicateFor(resultPredicate));
    return this;
  }

  /**
   * Specifies the type to fail on. Applies to any type that is assignable from the {@code failure}.
   * 
   * @throws NullPointerException if {@code failure} is null
   */
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public CircuitBreaker failOn(Class failure) {
    Assert.notNull(failure, "failure");
    return failOn((List)Arrays.asList(failure));
  }
  
  /**
   * Specifies the types to fail on. Applies to any type that is assignable from the {@code failures}.
   * 
   * @throws NullPointerException if {@code failures} is null
   * @throws IllegalArgumentException if failures is empty
   */
  @SuppressWarnings("unchecked")
  public CircuitBreaker failOn(Class... failures) {
    Assert.notNull(failures, "failures");
    Assert.isTrue(failures.length > 0, "failures cannot be empty");
    return failOn(Arrays.asList(failures));
  }

  /**
   * Specifies the types to fail on. Applies to any type that is assignable from the {@code failures}.
   * 
   * @throws NullPointerException if {@code failures} is null
   * @throws IllegalArgumentException if failures is empty
   */
  public CircuitBreaker failOn(List> failures) {
    Assert.notNull(failures, "failures");
    Assert.isTrue(!failures.isEmpty(), "failures cannot be empty");
    failuresChecked = true;
    failureConditions.add(Predicates.failurePredicateFor(failures));
    return this;
  }

  /**
   * Specifies that a failure should be recorded if the {@code failurePredicate} matches the failure.
   * 
   * @throws NullPointerException if {@code failurePredicate} is null
   */
  public CircuitBreaker failOn(Predicate failurePredicate) {
    Assert.notNull(failurePredicate, "failurePredicate");
    failuresChecked = true;
    failureConditions.add(Predicates.failurePredicateFor(failurePredicate));
    return this;
  }

  /**
   * Specifies that a failure should be recorded if the execution result matches the {@code result}.
   */
  public CircuitBreaker failWhen(Object result) {
    failureConditions.add(Predicates.resultPredicateFor(result));
    return this;
  }

  /**
   * Returns the delay before allowing another execution on the circuit. Defaults to {@link Duration#NONE}.
   * 
   * @see #withDelay(long, TimeUnit)
   */
  public Duration getDelay() {
    return delay;
  }

  /**
   * Gets the ratio of successive failures that must occur when in a closed state in order to open the circuit else
   * {@code null} if none has been configured.
   * 
   * @see #withFailureThreshold(int)
   * @see #withFailureThreshold(int, int)
   */
  public Ratio getFailureThreshold() {
    return failureThreshold;
  }

  /**
   * Gets the state of the circuit.
   */
  public State getState() {
    return state.get().getState();
  }

  /**
   * Gets the ratio of successive successful executions that must occur when in a half-open state in order to close the
   * circuit else {@code null} if none has been configured.
   * 
   * @see #withSuccessThreshold(int)
   * @see #withSuccessThreshold(int, int)
   */
  public Ratio getSuccessThreshold() {
    return successThreshold;
  }

  /**
   * Returns timeout for executions else {@code null} if none has been configured.
   * 
   * @see #withTimeout(long, TimeUnit)
   */
  public Duration getTimeout() {
    return timeout;
  }

  /**
   * Half-opens the circuit.
   */
  public void halfOpen() {
    transitionTo(State.HALF_OPEN, onHalfOpen);
  }

  /**
   * Returns whether the circuit is closed.
   */
  public boolean isClosed() {
    return State.CLOSED.equals(getState());
  }

  /**
   * Returns whether the circuit breaker considers the {@code result} or {@code throwable} a failure based on the
   * configured conditions, or if {@code failure} is not null it is not checked by any configured condition.
   * 
   * @see #failIf(BiPredicate)
   * @see #failIf(Predicate)
   * @see #failOn(Class...)
   * @see #failOn(List)
   * @see #failOn(Predicate)
   * @see #failWhen(Object)
   */
  public boolean isFailure(Object result, Throwable failure) {
    for (BiPredicate predicate : failureConditions) {
      if (predicate.test(result, failure))
        return true;
    }

    // Return true if the failure is not checked by a configured condition
    return failure != null && !failuresChecked;
  }

  /**
   * Returns whether the circuit is half open.
   */
  public boolean isHalfOpen() {
    return State.HALF_OPEN.equals(getState());
  }

  /**
   * Returns whether the circuit is open.
   */
  public boolean isOpen() {
    return State.OPEN.equals(getState());
  }

  /**
   * Calls the {@code runnable} when the circuit is closed.
   */
  public CircuitBreaker onClose(CheckedRunnable runnable) {
    onClose = runnable;
    return this;
  }

  /**
   * Calls the {@code runnable} when the circuit is half-opened.
   */
  public CircuitBreaker onHalfOpen(CheckedRunnable runnable) {
    onHalfOpen = runnable;
    return this;
  }

  /**
   * Calls the {@code runnable} when the circuit is opened.
   */
  public CircuitBreaker onOpen(CheckedRunnable runnable) {
    onOpen = runnable;
    return this;
  }

  /**
   * Opens the circuit.
   */
  public void open() {
    transitionTo(State.OPEN, onOpen);
  }

  /**
   * Records an execution {@code failure} as a success or failure based on the failure configuration as determined by
   * {@link #isFailure(Object, Throwable)}.
   * 
   * @see #isFailure(Object, Throwable)
   */
  public void recordFailure(Throwable failure) {
    recordResult(null, failure);
  }

  /**
   * Records an execution {@code result} as a success or failure based on the failure configuration as determined by
   * {@link #isFailure(Object, Throwable)}.
   * 
   * @see #isFailure(Object, Throwable)
   */
  public void recordResult(Object result) {
    recordResult(result, null);
  }

  /**
   * Records an execution success.
   */
  public void recordSuccess() {
    try {
      state.get().recordSuccess();
    } finally {
      currentExecutions.decrementAndGet();
    }
  }

  @Override
  public String toString() {
    return getState().toString();
  }

  /**
   * Sets the {@code delay} to wait in open state before transitioning to half-open.
   * 
   * @throws NullPointerException if {@code timeUnit} is null
   * @throws IllegalArgumentException if {@code delay} <= 0
   */
  public CircuitBreaker withDelay(long delay, TimeUnit timeUnit) {
    Assert.notNull(timeUnit, "timeUnit");
    Assert.isTrue(delay > 0, "delay must be greater than 0");
    this.delay = new Duration(delay, timeUnit);
    return this;
  }

  /**
   * Sets the number of successive failures that must occur when in a closed state in order to open the circuit.
   * 
   * @throws IllegalArgumentException if {@code failureThresh} < 1
   */
  public CircuitBreaker withFailureThreshold(int failureThreshold) {
    Assert.isTrue(failureThreshold >= 1, "failureThreshold must be greater than or equal to 1");
    return withFailureThreshold(failureThreshold, failureThreshold);
  }

  /**
   * Sets the ratio of successive failures that must occur when in a closed state in order to open the circuit. For
   * example: 5, 10 would open the circuit if 5 out of the last 10 executions result in a failure. The circuit will not
   * be opened until at least the given number of {@code executions} have taken place.
   * 
   * @param failures The number of failures that must occur in order to open the circuit
   * @param executions The number of executions to measure the {@code failures} against
   * @throws IllegalArgumentException if {@code failures} < 1, {@code executions} < 1, or {@code failures} is <
   *           {@code executions}
   */
  public synchronized CircuitBreaker withFailureThreshold(int failures, int executions) {
    Assert.isTrue(failures >= 1, "failures must be greater than or equal to 1");
    Assert.isTrue(executions >= 1, "executions must be greater than or equal to 1");
    Assert.isTrue(executions >= failures, "executions must be greater than or equal to failures");
    this.failureThreshold = new Ratio(failures, executions);
    state.get().setFailureThreshold(failureThreshold);
    return this;
  }

  /**
   * Sets the number of successive successful executions that must occur when in a half-open state in order to close the
   * circuit, else the circuit is re-opened when a failure occurs.
   * 
   * @throws IllegalArgumentException if {@code successThreshold} < 1
   */
  public CircuitBreaker withSuccessThreshold(int successThreshold) {
    Assert.isTrue(successThreshold >= 1, "successThreshold must be greater than or equal to 1");
    return withSuccessThreshold(successThreshold, successThreshold);
  }

  /**
   * Sets the ratio of successive successful executions that must occur when in a half-open state in order to close the
   * circuit. For example: 5, 10 would close the circuit if 5 out of the last 10 executions were successful. The circuit
   * will not be closed until at least the given number of {@code executions} have taken place.
   * 
   * @param successes The number of successful executions that must occur in order to open the circuit
   * @param executions The number of executions to measure the {@code successes} against
   * @throws IllegalArgumentException if {@code successes} < 1, {@code executions} < 1, or {@code successes} is <
   *           {@code executions}
   */
  public synchronized CircuitBreaker withSuccessThreshold(int successes, int executions) {
    Assert.isTrue(successes >= 1, "successes must be greater than or equal to 1");
    Assert.isTrue(executions >= 1, "executions must be greater than or equal to 1");
    Assert.isTrue(executions >= successes, "executions must be greater than or equal to successes");
    this.successThreshold = new Ratio(successes, executions);
    state.get().setSuccessThreshold(successThreshold);
    return this;
  }

  /**
   * Sets the {@code timeout} for executions. Executions that exceed this timeout are not interrupted, but are recorded
   * as failures once they naturally complete.
   * 
   * @throws NullPointerException if {@code timeUnit} is null
   * @throws IllegalArgumentException if {@code timeout} <= 0
   */
  public CircuitBreaker withTimeout(long timeout, TimeUnit timeUnit) {
    Assert.notNull(timeUnit, "timeUnit");
    Assert.isTrue(timeout > 0, "timeout must be greater than 0");
    this.timeout = new Duration(timeout, timeUnit);
    return this;
  }

  void before() {
    currentExecutions.incrementAndGet();
  }

  /**
   * Records an execution failure.
   */
  void recordFailure() {
    try {
      state.get().recordFailure();
    } finally {
      currentExecutions.decrementAndGet();
    }
  }

  void recordResult(Object result, Throwable failure) {
    try {
      if (isFailure(result, failure))
        state.get().recordFailure();
      else
        state.get().recordSuccess();
    } finally {
      currentExecutions.decrementAndGet();
    }
  }

  /**
   * Transitions to the {@code newState} if not already in that state and calls any associated event listener.
   */
  private void transitionTo(State newState, CheckedRunnable listener) {
    boolean transitioned = false;
    synchronized (this) {
      if (!getState().equals(newState)) {
        switch (newState) {
          case CLOSED:
            state.set(new ClosedState(this));
            break;
          case OPEN:
            state.set(new OpenState(this));
            break;
          case HALF_OPEN:
            state.set(new HalfOpenState(this));
            break;
        }
        transitioned = true;
      }
    }

    if (transitioned && listener != null) {
      try {
        listener.run();
      } catch (Exception ignore) {
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy