Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,154 @@ public void onFailure(
executionOperations.reschedule(executionComplete, nextExecutionTime);
}
}

/**
* Failure handler to create a sequence of failure handlers
*
* <p>The SequenceFailureHandler facilitates the construction of intricate failure handling
* mechanisms. It allows for behaviors such as retrying an operation up to five times with
* specified delays between attempts, and if unsuccessful, automatically rescheduling the task for
* the following day to maintain operational continuity.
*/
class SequenceFailureHandler<T> implements FailureHandler<T> {

private static final Logger LOG = LoggerFactory.getLogger(SequenceFailureHandler.class);
private final FailureHandler<T> primaryFailureHandler;
private final FailureHandler<T> secondaryFailureHandler;
private final int primaryHandlerRetryCount;

/**
* Create a sequence of failure handlers. The primary failure handler is used for the first
* failure. If the execution fails again, the secondary failure handler is used.
*
* <p>The PrimaryFailureHandler cannot be a MaxRetriesFailureHandler as it stops the execution.
* Use the primaryHandlerRetryCount parameter instead if retries are needed.
*
* @param primaryFailureHandler The primary failure handler. Cannot be a
* MaxRetriesFailureHandler
* @param secondaryFailureHandler The secondary failure handler
*/
public SequenceFailureHandler(
FailureHandler<T> primaryFailureHandler, FailureHandler<T> secondaryFailureHandler) {
this(primaryFailureHandler, secondaryFailureHandler, 1);
}

/**
* Create a sequence of failure handlers. The primary failure handler is used for failures until
* the primaryHandlerRetryCount is reached. If the execution fails again, the secondary failure
* handler is used.
*
* <p>For example, if primaryHandlerRetryCount is 2, the primary failure handler will be used
* for the first two failures. If the execution fails a third time, the secondary failure
* handler is used.
*
* <p>The PrimaryFailureHandler cannot be a MaxRetriesFailureHandler as it stops the execution.
* Use the primaryHandlerRetryCount parameter instead if retries are needed.
*
* <p>Primary handler retry count must be at least 1.
*
* @param primaryFailureHandler The primary failure handler. Cannot be a
* MaxRetriesFailureHandler
* @param secondaryFailureHandler The secondary failure handler
* @param primaryHandlerRetryCount The number of retries to use the primary failure handler.
* Must be at least 1.
*/
public SequenceFailureHandler(
FailureHandler<T> primaryFailureHandler,
FailureHandler<T> secondaryFailureHandler,
int primaryHandlerRetryCount) {
if (primaryFailureHandler instanceof MaxRetriesFailureHandler) {
throw new IllegalArgumentException(
"Primary failure handler cannot be a MaxRetriesFailureHandler as it stops the execution. Use the primaryHandlerRetryCount parameter instead if retries are needed.");
}
this.primaryFailureHandler = primaryFailureHandler;
this.secondaryFailureHandler = secondaryFailureHandler;
if (primaryHandlerRetryCount < 1) {
throw new IllegalArgumentException("Primary handler retry count must be at least 1.");
}
this.primaryHandlerRetryCount = primaryHandlerRetryCount;
}

@Override
public void onFailure(
ExecutionComplete executionComplete, ExecutionOperations<T> executionOperations) {
int consecutiveFailures = executionComplete.getExecution().consecutiveFailures;
int totalNumberOfFailures = consecutiveFailures + 1;
if (totalNumberOfFailures > primaryHandlerRetryCount) {
LOG.debug(
"Primary handler retry count exceeded for task instance {}. Switching to secondary failure handler.",
executionComplete.getExecution().taskInstance);
secondaryFailureHandler.onFailure(executionComplete, executionOperations);
} else {
LOG.debug(
"Using primary failure handler for task instance {} with {} failures.",
executionComplete.getExecution().taskInstance,
totalNumberOfFailures);
primaryFailureHandler.onFailure(executionComplete, executionOperations);
}
}

public static <T> SequenceFailureHandler<T> of(
FailureHandler<T> primaryFailureHandler, FailureHandler<T> secondaryFailureHandler) {
return SequenceFailureHandler.of(primaryFailureHandler, secondaryFailureHandler, 1);
}

public static <T> SequenceFailureHandler<T> of(
FailureHandler<T> primaryFailureHandler,
FailureHandler<T> secondaryFailureHandler,
int primaryHandlerRetryCount) {
return new SequenceFailureHandler<>(
primaryFailureHandler, secondaryFailureHandler, primaryHandlerRetryCount);
}

public static <T> SequenceFailureHandlerBuilder<T> builder() {
return new SequenceFailureHandlerBuilder<>();
}

public static class SequenceFailureHandlerBuilder<T> {
private FailureHandler<T> primaryFailureHandler;
private FailureHandler<T> secondaryFailureHandler;
private int primaryHandlerRetryCount;

public SequenceFailureHandlerBuilder() {
this(null, null, 1);
}

public SequenceFailureHandlerBuilder(
FailureHandler<T> primaryFailureHandler, FailureHandler<T> secondaryFailureHandler) {
this(primaryFailureHandler, secondaryFailureHandler, 1);
}

public SequenceFailureHandlerBuilder(
FailureHandler<T> primaryFailureHandler,
FailureHandler<T> secondaryFailureHandler,
int primaryHandlerRetryCount) {
this.primaryFailureHandler = primaryFailureHandler;
this.secondaryFailureHandler = secondaryFailureHandler;
this.primaryHandlerRetryCount = primaryHandlerRetryCount;
}

public SequenceFailureHandlerBuilder<T> primary(FailureHandler<T> primaryFailureHandler) {
this.primaryFailureHandler = primaryFailureHandler;
return this;
}

public SequenceFailureHandlerBuilder<T> secondary(FailureHandler<T> secondaryFailureHandler) {
this.secondaryFailureHandler = secondaryFailureHandler;
return this;
}

public SequenceFailureHandlerBuilder<T> afterTries(
int primaryHandlerRetryCount, FailureHandler<T> secondaryFailureHandler) {
this.secondaryFailureHandler = secondaryFailureHandler;
this.primaryHandlerRetryCount = primaryHandlerRetryCount;
return this;
}

public SequenceFailureHandler<T> build() {
return new SequenceFailureHandler<>(
primaryFailureHandler, secondaryFailureHandler, primaryHandlerRetryCount);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
import static java.time.Instant.now;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;

import com.github.kagkarlsson.scheduler.TestTasks;
import com.github.kagkarlsson.scheduler.task.FailureHandler.MaxRetriesFailureHandler;
import com.github.kagkarlsson.scheduler.task.FailureHandler.SequenceFailureHandler;
import com.github.kagkarlsson.scheduler.task.helper.OneTimeTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class FailureHandlerTest {
@Nested
Expand Down Expand Up @@ -79,4 +85,136 @@ private Execution getExecutionWithFails(int consecutiveFailures) {
now(), task.instance("1"), false, null, null, null, consecutiveFailures, null, 1L);
}
}

@Nested
class SequenceFailureHandlerTest {

private final OneTimeTask<Integer> task =
TestTasks.oneTime("some-task", Integer.class, (instance, executionContext) -> {});

private final AtomicInteger primaryFailureHandlerCallCount = new AtomicInteger(0);
private final FailureHandler<String> primaryFailureHandler =
(executionComplete, executionOperations) ->
primaryFailureHandlerCallCount.incrementAndGet();

private final AtomicBoolean secondaryFailureHandlerCalled = new AtomicBoolean(false);
private final FailureHandler<String> secondaryFailureHandler =
(executionComplete, executionOperations) -> secondaryFailureHandlerCalled.set(true);

private final ExecutionComplete executionComplete = mock(ExecutionComplete.class);
private final ExecutionOperations<String> executionOperations = mock(ExecutionOperations.class);

@Test
void should_run_secondary_handler_when_primary_handler_fails() {
SequenceFailureHandler<String> sequenceFailureHandler =
new SequenceFailureHandler<>(primaryFailureHandler, secondaryFailureHandler);

// First failure
Execution execution = getExecutionWithFails(0);
when(executionComplete.getExecution()).thenReturn(execution);

sequenceFailureHandler.onFailure(executionComplete, executionOperations);

assertThat(primaryFailureHandlerCallCount.get(), is(1));
assertThat(secondaryFailureHandlerCalled.get(), is(false));

// Second failure
execution = getExecutionWithFails(1);
when(executionComplete.getExecution()).thenReturn(execution);

sequenceFailureHandler.onFailure(executionComplete, executionOperations);

assertThat(primaryFailureHandlerCallCount.get(), is(1));
assertThat(secondaryFailureHandlerCalled.get(), is(true));
}

@Test
void should_retry_primary_handler_according_to_the_primaryHandlerRetryCount() {
int primaryHandlerRetryCount = 3;
SequenceFailureHandler<String> sequenceFailureHandler =
new SequenceFailureHandler<>(
primaryFailureHandler, secondaryFailureHandler, primaryHandlerRetryCount);

// First failure
Execution execution = getExecutionWithFails(0);
when(executionComplete.getExecution()).thenReturn(execution);

sequenceFailureHandler.onFailure(executionComplete, executionOperations);

assertThat(primaryFailureHandlerCallCount.get(), is(1));
assertThat(secondaryFailureHandlerCalled.get(), is(false));

// Second failure
execution = getExecutionWithFails(1);
when(executionComplete.getExecution()).thenReturn(execution);

sequenceFailureHandler.onFailure(executionComplete, executionOperations);

assertThat(primaryFailureHandlerCallCount.get(), is(2));
assertThat(secondaryFailureHandlerCalled.get(), is(false));

// Third failure
execution = getExecutionWithFails(2);
when(executionComplete.getExecution()).thenReturn(execution);

sequenceFailureHandler.onFailure(executionComplete, executionOperations);

assertThat(primaryFailureHandlerCallCount.get(), is(3));
assertThat(secondaryFailureHandlerCalled.get(), is(false));

// Fourth failure
execution = getExecutionWithFails(3);
when(executionComplete.getExecution()).thenReturn(execution);

sequenceFailureHandler.onFailure(executionComplete, executionOperations);

assertThat(primaryFailureHandlerCallCount.get(), is(3));
assertThat(secondaryFailureHandlerCalled.get(), is(true));
}

@Test
void should_throw_exception_when_primaryFailureHandler_is_MaxRetriesFailureHandler() {
FailureHandler<String> primaryFailureHandlerForThisTest =
new MaxRetriesFailureHandler<>(3, null, null);
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
new SequenceFailureHandler<>(
primaryFailureHandlerForThisTest, secondaryFailureHandler));
assertThat(
thrown.getMessage(),
is(
"Primary failure handler cannot be a MaxRetriesFailureHandler as it stops the execution. Use the primaryHandlerRetryCount parameter instead if retries are needed."));
}

@ParameterizedTest
@ValueSource(ints = {0, -1})
void should_throw_exception_when_primaryHandlerRetryCount_is_smaller_than_1(
int primaryHandlerRetryCount) {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
new SequenceFailureHandler<>(
primaryFailureHandler, secondaryFailureHandler, primaryHandlerRetryCount));

assertThat(thrown.getMessage(), is("Primary handler retry count must be at least 1."));
}

@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void should_be_successful_when_primaryHandlerRetryCount_is_1_or_greater(
int primaryHandlerRetryCount) {
assertDoesNotThrow(
() ->
new SequenceFailureHandler<>(
primaryFailureHandler, secondaryFailureHandler, primaryHandlerRetryCount));
}

private Execution getExecutionWithFails(int consecutiveFailures) {
return new Execution(
now(), task.instance("1"), false, null, null, null, consecutiveFailures, null, 1L);
}
}
}
Loading