Skip to content

Commit

Permalink
Add unified Policy condition API
Browse files Browse the repository at this point in the history
- Add a unified Policy condition API
- Add separate onSuccess and onFailure handling for policy executors
- Move isFailure base logic to PolicyExecutor
- Updated sync and async FailsafeConfig tests to expect success when an unhandled exception type is thrown
  • Loading branch information
jhalterman committed Dec 28, 2018
1 parent c3da210 commit 788e936
Show file tree
Hide file tree
Showing 27 changed files with 497 additions and 488 deletions.
153 changes: 153 additions & 0 deletions src/main/java/net/jodah/failsafe/AbstractPolicy.java
@@ -0,0 +1,153 @@
package net.jodah.failsafe;

import net.jodah.failsafe.function.Predicate;
import net.jodah.failsafe.internal.util.Assert;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiPredicate;

/**
* Base Policy implementation that captures conditions to determine whether an execution is a failure. If no failure
* conditions are configured:
* <ul>
* <li>If no other policies are configured, the execution is considered a failure if an Exception was thrown.</li>
* <li>If other policies were configured, the execution is considered a failure if the previous configured Policy's
* handling of the execution failed.</li>
* </ul>
*
* @param <R> result type
*/
@SuppressWarnings("unchecked")
public abstract class AbstractPolicy<R> implements Policy {
/** Indicates whether failures are checked by a configured failure condition */
protected boolean failuresChecked;
/** Conditions that determine whether an execution is a failure */
protected List<BiPredicate<Object, Throwable>> failureConditions;

AbstractPolicy() {
failureConditions = new ArrayList<>();
}

/**
* Specifies the failure to retryOn. Any failure that is assignable from the {@code failure} will be handled.
*
* @throws NullPointerException if {@code failure} is null
*/
@SuppressWarnings({ "rawtypes" })
public R handle(Class<? extends Throwable> failure) {
Assert.notNull(failure, "failure");
return handle(Arrays.asList(failure));
}

/**
* Specifies the failures to retryOn. Any failures that are assignable from the {@code failures} will be handled.
*
* @throws NullPointerException if {@code failures} is null
* @throws IllegalArgumentException if failures is empty
*/
@SuppressWarnings("unchecked")
public R handle(Class<? extends Throwable>... failures) {
Assert.notNull(failures, "failures");
Assert.isTrue(failures.length > 0, "Failures cannot be empty");
return handle(Arrays.asList(failures));
}

/**
* Specifies the failures to retryOn. Any failures that are assignable from the {@code failures} will be handled.
*
* @throws NullPointerException if {@code failures} is null
* @throws IllegalArgumentException if failures is null or empty
*/
public R handle(List<Class<? extends Throwable>> failures) {
Assert.notNull(failures, "failures");
Assert.isTrue(!failures.isEmpty(), "failures cannot be empty");
failuresChecked = true;
failureConditions.add(Predicates.failurePredicateFor(failures));
return (R) this;
}

/**
* Specifies that a failure has occurred if the {@code failurePredicate} matches the failure.
*
* @throws NullPointerException if {@code failurePredicate} is null
*/
public R handleIf(Predicate<? extends Throwable> failurePredicate) {
Assert.notNull(failurePredicate, "failurePredicate");
failuresChecked = true;
failureConditions.add(Predicates.failurePredicateFor(failurePredicate));
return (R) this;
}

/**
* Specifies that a failure has occurred if the {@code resultPredicate} matches the execution result.
*
* @throws NullPointerException if {@code resultPredicate} is null
*/
@SuppressWarnings("unchecked")
public <T> R handleIf(BiPredicate<T, ? extends Throwable> resultPredicate) {
Assert.notNull(resultPredicate, "resultPredicate");
failuresChecked = true;
failureConditions.add((BiPredicate<Object, Throwable>) resultPredicate);
return (R) this;
}

/**
* Specifies that a failure has occurred if the {@code result} matches the execution result.
*/
public R handleResult(Object result) {
failureConditions.add(Predicates.resultPredicateFor(result));
return (R) this;
}

/**
* Specifies that a failure has occurred if the {@code resultPredicate} matches the execution result.
*
* @throws NullPointerException if {@code resultPredicate} is null
*/
public <T> R handleResultIf(Predicate<T> resultPredicate) {
Assert.notNull(resultPredicate, "resultPredicate");
failureConditions.add(Predicates.resultPredicateFor(resultPredicate));
return (R) this;
}

/**
* Returns whether an execution result can be retried given the configured failure conditions.
*
* @see #handle(Class...)
* @see #handle(List)
* @see #handleIf(BiPredicate)
* @see #handleIf(Predicate)
* @see #handleResult(Object)
* @see #handleResultIf(Predicate)
*/
boolean isFailure(ExecutionResult result) {
return failureConditions.isEmpty() ? !result.success : isFailure(result.result, result.failure);
}

/**
* Returns whether an execution result can be retried given the configured failure conditions.
*
* @see #handle(Class...)
* @see #handle(List)
* @see #handleIf(BiPredicate)
* @see #handleIf(Predicate)
* @see #handleResult(Object)
* @see #handleResultIf(Predicate)
*/
public boolean isFailure(Object result, Throwable failure) {
for (BiPredicate<Object, Throwable> predicate : failureConditions) {
try {
if (predicate.test(result, failure))
return true;
} catch (Exception ignored) {
// Ignore confused user-supplied predicates.
// They should not be allowed to halt execution of the operation.
}
}

// Fail by default if a failure is not checked by a condition
return failure != null && !failuresChecked;
}
}
126 changes: 5 additions & 121 deletions src/main/java/net/jodah/failsafe/CircuitBreaker.java
Expand Up @@ -16,7 +16,6 @@
package net.jodah.failsafe;

import net.jodah.failsafe.function.CheckedRunnable;
import net.jodah.failsafe.function.Predicate;
import net.jodah.failsafe.internal.*;
import net.jodah.failsafe.internal.executor.CircuitBreakerExecutor;
import net.jodah.failsafe.internal.util.Assert;
Expand All @@ -25,20 +24,17 @@

import java.time.Duration;
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 java.util.function.BiPredicate;

/**
* A circuit breaker that temporarily halts execution when configurable thresholds are exceeded.
*
* @author Jonathan Halterman
*/
@SuppressWarnings("WeakerAccess")
public class CircuitBreaker implements Policy {
public class CircuitBreaker extends AbstractPolicy<CircuitBreaker> {
/** Writes guarded by "this" */
private final AtomicReference<CircuitState> state = new AtomicReference<>();
private final AtomicInteger currentExecutions = new AtomicInteger();
Expand All @@ -47,9 +43,6 @@ public class CircuitBreaker implements Policy {
private Duration timeout;
private Ratio failureThreshold;
private Ratio successThreshold;
/** Indicates whether failures are checked by a configured failure condition */
private boolean failuresChecked;
private List<BiPredicate<Object, Throwable>> failureConditions;
CheckedRunnable onOpen;
CheckedRunnable onHalfOpen;
CheckedRunnable onClose;
Expand All @@ -66,11 +59,11 @@ public CircuitBreaker() {
* The state of the circuit.
*/
public enum State {
/* The circuit is closed and fully functional, allowing executions to occur. */
/** The circuit is closed and fully functional, allowing executions to occur. */
CLOSED,
/* The circuit is opened and not allowing executions to occur. */
/** The circuit is opened and not allowing executions to occur. */
OPEN,
/* The circuit is temporarily allowing executions to occur. */
/** The circuit is temporarily allowing executions to occur. */
HALF_OPEN
}

Expand All @@ -89,90 +82,7 @@ public void close() {
}

/**
* 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 <T> CircuitBreaker failIf(BiPredicate<T, ? extends Throwable> completionPredicate) {
Assert.notNull(completionPredicate, "completionPredicate");
failuresChecked = true;
failureConditions.add((BiPredicate<Object, Throwable>) completionPredicate);
return this;
}

/**
* Specifies that a failure should be recorded if the {@code resultPredicate} matches the result. Predicate is not
* invoked when the operation fails.
*
* @throws NullPointerException if {@code resultPredicate} is null
*/
public <T> CircuitBreaker failIf(Predicate<T> 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({ "rawtypes" })
public CircuitBreaker failOn(Class<? extends Throwable> failure) {
Assert.notNull(failure, "failure");
return failOn(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<? extends Throwable>... 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<Class<? extends Throwable>> 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<? extends Throwable> 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}.
* Returns the delay before allowing another execution on the circuit. Defaults to {@link Duration#ZERO}.
*
* @see #withDelay(long, TimeUnit)
*/
Expand Down Expand Up @@ -232,32 +142,6 @@ 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<Object, Throwable> predicate : failureConditions) {
try {
if (predicate.test(result, failure))
return true;
} catch (Exception t) {
// Ignore confused user-supplied predicates.
// They should not be allowed to halt execution of the operation.
}
}

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

/**
* Returns whether the circuit is half open.
*/
Expand Down
28 changes: 23 additions & 5 deletions src/main/java/net/jodah/failsafe/ExecutionResult.java
Expand Up @@ -22,8 +22,11 @@ public class ExecutionResult {
/** Whether an async execution scheduling error occurred */
public final boolean schedulingError;

/**
* Creates a new ExecutionResult where {@code success} is set to true if {@code failure} is not null.
*/
public ExecutionResult(Object result, Throwable failure) {
this(result, failure, false, 0, false, false, false);
this(result, failure, false, 0, false, failure == null, false);
}

public ExecutionResult(Object result, Throwable failure, boolean completed, boolean success) {
Expand All @@ -42,17 +45,32 @@ public ExecutionResult(Object result, Throwable failure, boolean noResult, long
}

/**
* Returns a copy of the ExecutionResult with the {@code completed} and {@code success} values.
* Returns a copy of the ExecutionResult with the {@code completed} value, else this if nothing has changed.
*/
public ExecutionResult with(boolean completed) {
return this.completed == completed ?
this :
new ExecutionResult(result, failure, noResult, waitNanos, completed, success, schedulingError);
}

/**
* Returns a copy of the ExecutionResult with the {@code completed} and {@code success} values, else this if nothing
* has changed.
*/
public ExecutionResult with(boolean completed, boolean success) {
return new ExecutionResult(result, failure, noResult, waitNanos, completed, success, schedulingError);
return this.completed == completed && this.success == success ?
this :
new ExecutionResult(result, failure, noResult, waitNanos, completed, success, schedulingError);
}

/**
* Returns a copy of the ExecutionResult with the {@code waitNanos}, {@code completed} and {@code success} values.
* Returns a copy of the ExecutionResult with the {@code waitNanos}, {@code completed} and {@code success} values,
* else this if nothing has changed.
*/
public ExecutionResult with(long waitNanos, boolean completed, boolean success) {
return new ExecutionResult(result, failure, noResult, waitNanos, completed, success, schedulingError);
return this.waitNanos == waitNanos && this.completed == completed && this.success == success ?
this :
new ExecutionResult(result, failure, noResult, waitNanos, completed, success, schedulingError);
}

static ExecutionResult noResult() {
Expand Down

0 comments on commit 788e936

Please sign in to comment.