Skip to content

Commit

Permalink
Add support for Fallback.onFailedAttempt.
Browse files Browse the repository at this point in the history
Fixes #248.
  • Loading branch information
jhalterman committed May 12, 2020
1 parent 1586737 commit 17dc2f9
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 14 deletions.
22 changes: 19 additions & 3 deletions src/main/java/net/jodah/failsafe/Fallback.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
package net.jodah.failsafe;

import net.jodah.failsafe.event.ExecutionAttemptedEvent;
import net.jodah.failsafe.function.*;
import net.jodah.failsafe.function.CheckedConsumer;
import net.jodah.failsafe.function.CheckedFunction;
import net.jodah.failsafe.function.CheckedRunnable;
import net.jodah.failsafe.function.CheckedSupplier;
import net.jodah.failsafe.internal.EventListener;
import net.jodah.failsafe.internal.util.Assert;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import static net.jodah.failsafe.Functions.*;
import static net.jodah.failsafe.Functions.toFn;

/**
* A Policy that handles failures using a fallback function or result.
Expand All @@ -42,6 +46,7 @@ public class Fallback<R> extends FailurePolicy<Fallback<R>, R> {
private final CheckedFunction<ExecutionAttemptedEvent, R> fallback;
private final CheckedFunction<ExecutionAttemptedEvent, CompletableFuture<R>> fallbackStage;
private boolean async;
private EventListener failedAttemptListener;

private Fallback() {
this(null, null, false);
Expand Down Expand Up @@ -211,6 +216,17 @@ public boolean isAsync() {
return async;
}

/**
* Registers the {@code listener} to be called when an execution attempt fails. You can also use {@link
* #onFailure(CheckedConsumer) onFailure} to determine when the execution attempt fails <i>and</i> and the fallback
* result fails.
* <p>Note: Any exceptions that are thrown from within the {@code listener} are ignored.</p>
*/
public Fallback<R> onFailedAttempt(CheckedConsumer<? extends ExecutionAttemptedEvent<R>> listener) {
failedAttemptListener = EventListener.ofAttempt(Assert.notNull(listener, "listener"));
return this;
}

/**
* Returns the applied fallback result.
*/
Expand All @@ -229,6 +245,6 @@ CompletableFuture<R> applyStage(R result, Throwable failure, ExecutionContext co

@Override
public PolicyExecutor toExecutor(AbstractExecution execution) {
return new FallbackExecutor(this, execution);
return new FallbackExecutor(this, execution, failedAttemptListener);
}
}
13 changes: 12 additions & 1 deletion src/main/java/net/jodah/failsafe/FallbackExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package net.jodah.failsafe;

import net.jodah.failsafe.internal.EventListener;
import net.jodah.failsafe.util.concurrent.Scheduler;

import java.util.concurrent.*;
Expand All @@ -24,8 +25,11 @@
* A PolicyExecutor that handles failures according to a {@link Fallback}.
*/
class FallbackExecutor extends PolicyExecutor<Fallback> {
FallbackExecutor(Fallback fallback, AbstractExecution execution) {
private final EventListener failedAttemptListener;

FallbackExecutor(Fallback fallback, AbstractExecution execution, EventListener failedAttemptListener) {
super(fallback, execution);
this.failedAttemptListener = failedAttemptListener;
}

/**
Expand Down Expand Up @@ -92,4 +96,11 @@ protected Supplier<CompletableFuture<ExecutionResult>> supplyAsync(
return postExecuteAsync(result, scheduler, future);
});
}

@Override
protected ExecutionResult onFailure(ExecutionResult result) {
if (failedAttemptListener != null)
failedAttemptListener.handle(result, execution);
return result;
}
}
4 changes: 3 additions & 1 deletion src/main/java/net/jodah/failsafe/RetryPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ public RetryPolicy<R> onAbort(CheckedConsumer<? extends ExecutionCompletedEvent<
}

/**
* Registers the {@code listener} to be called when an execution attempt fails.
* Registers the {@code listener} to be called when an execution attempt fails. You can also use {@link
* #onFailure(CheckedConsumer) onFailure} to determine when the execution attempt fails <i>and</i> and all retries
* have failed.
* <p>Note: Any exceptions that are thrown from within the {@code listener} are ignored.</p>
*/
public RetryPolicy<R> onFailedAttempt(CheckedConsumer<? extends ExecutionAttemptedEvent<R>> listener) {
Expand Down
15 changes: 7 additions & 8 deletions src/main/java/net/jodah/failsafe/RetryPolicyExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ class RetryPolicyExecutor extends PolicyExecutor<RetryPolicy> {
private volatile long delayNanos = -1;

// Listeners
private EventListener abortListener;
private EventListener failedAttemptListener;
private EventListener retriesExceededListener;
private EventListener retryListener;
private final EventListener abortListener;
private final EventListener failedAttemptListener;
private final EventListener retriesExceededListener;
private final EventListener retryListener;

RetryPolicyExecutor(RetryPolicy retryPolicy, AbstractExecution execution, EventListener abortListener,
EventListener failedAttemptListener, EventListener retriesExceededListener, EventListener retryListener) {
Expand Down Expand Up @@ -148,6 +148,9 @@ else if (postResult != null) {
@Override
@SuppressWarnings("unchecked")
protected ExecutionResult onFailure(ExecutionResult result) {
if (failedAttemptListener != null)
failedAttemptListener.handle(result, execution);

failedAttempts++;
long waitNanos = delayNanos;

Expand Down Expand Up @@ -175,10 +178,6 @@ protected ExecutionResult onFailure(ExecutionResult result) {
boolean completed = isAbortable || !shouldRetry;
boolean success = completed && result.isSuccess() && !isAbortable;

// Call attempt listeners
if (failedAttemptListener != null && !success)
failedAttemptListener.handle(result, execution);

// Call completion listeners
if (abortListener != null && isAbortable)
abortListener.handle(result, execution);
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/net/jodah/failsafe/ListenersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class ListenersTest {
Waiter waiter;

// RetryPolicy listener counters
ListenerCounter rpHandle = new ListenerCounter();
ListenerCounter rpAbort = new ListenerCounter();
ListenerCounter rpFailedAttempt = new ListenerCounter();
ListenerCounter rpRetriesExceeded = new ListenerCounter();
Expand All @@ -54,6 +55,7 @@ public class ListenersTest {
ListenerCounter cbFailure = new ListenerCounter();

// Fallback listener counters
ListenerCounter fbFailedAttempt = new ListenerCounter();
ListenerCounter fbSuccess = new ListenerCounter();
ListenerCounter fbFailure = new ListenerCounter();

Expand Down Expand Up @@ -99,6 +101,7 @@ void beforeMethod() {
cbSuccess.reset();
cbFailure.reset();

fbFailedAttempt.reset();
fbSuccess.reset();
fbFailure.reset();

Expand Down Expand Up @@ -127,6 +130,7 @@ private <T> FailsafeExecutor<T> registerListeners(RetryPolicy<T> retryPolicy, Ci
circuitBreaker.onFailure(e -> cbFailure.record());

if (fallback != null) {
fallback.onFailedAttempt(e -> fbFailedAttempt.record());
fallback.onSuccess(e -> fbSuccess.record());
fallback.onFailure(e -> fbFailure.record());
}
Expand Down Expand Up @@ -173,6 +177,7 @@ private void assertForSuccess(boolean sync) throws Throwable {
cbSuccess.assertEquals(1);
cbFailure.assertEquals(4);

fbFailedAttempt.assertEquals(0);
fbSuccess.assertEquals(1);
fbFailure.assertEquals(0);

Expand Down Expand Up @@ -351,6 +356,7 @@ private void assertForFailingRetryPolicy(boolean sync) throws Throwable {
cbSuccess.assertEquals(3);
cbFailure.assertEquals(0);

fbFailedAttempt.assertEquals(0);
fbSuccess.assertEquals(1);
fbFailure.assertEquals(0);

Expand Down Expand Up @@ -392,6 +398,7 @@ private void assertForFailingCircuitBreaker(boolean sync) throws Throwable {
cbSuccess.assertEquals(0);
cbFailure.assertEquals(1);

fbFailedAttempt.assertEquals(0);
fbSuccess.assertEquals(1);
fbFailure.assertEquals(0);

Expand Down Expand Up @@ -433,6 +440,7 @@ private void assertForFailingFallback(boolean sync) throws Throwable {
cbSuccess.assertEquals(1);
cbFailure.assertEquals(0);

fbFailedAttempt.assertEquals(1);
fbSuccess.assertEquals(0);
fbFailure.assertEquals(1);

Expand Down
14 changes: 13 additions & 1 deletion src/test/java/net/jodah/failsafe/issues/Issue284Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,44 @@
import org.testng.annotations.Test;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static org.testng.Assert.*;

@Test
public class Issue284Test {
AtomicInteger failedAttempt;
AtomicBoolean success;
AtomicBoolean failure;
AtomicBoolean executed;
Fallback<String> fallback;
RetryPolicy<String> retryPolicy = new RetryPolicy<String>().handleResult(null)
.onFailedAttempt(e -> failedAttempt.incrementAndGet())
.onSuccess(e -> success.set(true))
.onFailure(e -> failure.set(true));

@BeforeMethod
protected void beforeMethod() {
failedAttempt = new AtomicInteger();
success = new AtomicBoolean();
failure = new AtomicBoolean();
executed = new AtomicBoolean();
}

private Fallback<String> fallbackFor(String result) {
return Fallback.of(result).handleResult(null).onSuccess(e -> success.set(true)).onFailure(e -> failure.set(true));
return Fallback.of(result)
.handleResult(null)
.onFailedAttempt(e -> failedAttempt.incrementAndGet())
.onSuccess(e -> success.set(true))
.onFailure(e -> failure.set(true));
}

public void testFallbackSuccess() {
fallback = fallbackFor("hello");
String result = Failsafe.with(fallback).get(() -> null);

assertEquals(result, "hello");
assertEquals(failedAttempt.get(), 0);
assertTrue(success.get(), "Fallback should have been successful");
}

Expand All @@ -44,20 +53,23 @@ public void testFallbackFailure() {
String result = Failsafe.with(fallback).get(() -> null);

assertNull(result);
assertEquals(failedAttempt.get(), 1);
assertTrue(failure.get(), "Fallback should have failed");
}

public void testRetryPolicySuccess() {
String result = Failsafe.with(retryPolicy).get(() -> !executed.getAndSet(true) ? null : "hello");

assertEquals(result, "hello");
assertEquals(failedAttempt.get(), 1);
assertTrue(success.get(), "RetryPolicy should have been successful");
}

public void testRetryPolicyFailure() {
String result = Failsafe.with(retryPolicy).get(() -> null);

assertNull(result);
assertEquals(failedAttempt.get(), 3);
assertTrue(failure.get(), "RetryPolicy should have failed");
}
}

0 comments on commit 17dc2f9

Please sign in to comment.