Skip to main content
Version: 5.0.0 (Unmaintained)

Retry and CleanUp

The org.paramixel.api.support package provides two utilities for common testing patterns: Retry for operations that may fail transiently, and CleanUp for executing cleanup code while capturing throwables.

Retry​

Retry retries a ThrowingRunnable with configurable backoff and a wall-clock duration budget.

import org.paramixel.api.support.Retry;
import org.paramixel.api.support.Retry.Policy;

Creating a retry sequence​

Retry retry = Retry.of(Policy.exponential(
Duration.ofMillis(100),
Duration.ofSeconds(5)));

Configuring retry behavior​

retryOn(Predicate<Throwable>) — Controls which throwables are retryable. The default retries on any throwable that is not an Error. When the predicate returns false, the retry sequence stops immediately.

retry.retryOn(throwable ->
throwable instanceof java.net.ConnectException);

onRetry(BiConsumer<Integer, Throwable>) — Registers a callback invoked before each retry attempt. The callback receives the 1-based attempt number and the throwable from the previous failure. Multiple callbacks can be registered.

retry.onRetry((attempt, cause) ->
System.out.println("Retry attempt " + attempt + ": " + cause.getMessage()));

Running the operation​

run(ThrowingRunnable) — Runs the operation with retry behavior and returns a Result.

Retry.Result result = retry.run(() -> {
connectToService();
});

if (result.isSuccessful()) {
System.out.println("Succeeded after " + result.attemptCount() + " attempt(s)");
} else {
System.out.println("Failed after " + result.attemptCount() + " attempt(s)");
}

runAndThrow(ThrowingRunnable) — Same as run(), but rethrows the last captured exception when all retries are exhausted. Earlier exceptions are added as suppressed throwables.

retry.runAndThrow(() -> {
connectToService();
});

Retry.Policy​

The Policy interface defines the backoff strategy:

MethodDescription
waitDuration(int attempt, Throwable cause)Returns the delay before the next retry attempt
maximumDuration()Returns the total wall-clock duration budget

Built-in policies:

Factory MethodDescription
Policy.fixed(initialDelay, maximumDuration)Linear escalation: each retry waits initialDelay * attempt
Policy.exponential(initialDelay, maximumDuration)Exponential escalation: each retry waits initialDelay * 2^(attempt-1)
Policy fixed = Policy.fixed(Duration.ofMillis(200), Duration.ofSeconds(10));
Policy exponential = Policy.exponential(Duration.ofMillis(100), Duration.ofSeconds(5));

Custom policies implement the Policy interface:

Policy custom = new Policy() {
@Override
public Duration waitDuration(int attempt, Throwable cause) {
return Duration.ofMillis(500);
}

@Override
public Duration maximumDuration() {
return Duration.ofSeconds(30);
}
};

If a policy computation fails, implementations should throw PolicyException. The Retry loop catches PolicyException and treats the failed computation as zero, allowing the sequence to continue.

Retry.Result​

The Result describes the outcome of a retry run:

MethodDescription
maximumDuration()The configured wall-clock duration budget
attemptCount()The number of attempts executed
elapsedDuration()The total wall-clock duration from first attempt to termination
isSuccessful()Whether the operation succeeded within the budget
hasExceptions()Whether any attempt produced an exception
exception(int index)The exception for the attempt at the 0-based index, or empty if out of range
exceptions()Every captured exception in attempt order

Lifecycle methods​

MethodDescription
hasRun()Whether run() or runAndThrow() has been called
reset()Marks as not yet run without removing callbacks
clear()Removes all callbacks and resets execution state
caution

Retry is not thread-safe. Instances must not be shared across threads.

CleanUp​

CleanUp is a single-use wrapper that executes a ThrowingRunnable and captures any throwable thrown during execution. Inspired by JUnit 5's ThrowableCollector.

import org.paramixel.api.support.CleanUp;

Creating a CleanUp​

CleanUp cleanup = CleanUp.of(() -> database.close());
CleanUp fromCloseable = CleanUp.of(inputStream);
CleanUp noOp = CleanUp.of((AutoCloseable) null);

CleanUp.of(AutoCloseable) accepts null — calling run() on a null closeable does nothing.

Running cleanup​

run() — Executes the runnable and captures any non-unrecoverable throwable. Unrecoverable errors (non-StackOverflowError VirtualMachineError) are rethrown immediately.

runAndThrow() — Same as run(), but throws the captured throwable if one was recorded.

CleanUp cleanup = CleanUp.of(() -> connection.close());
// ... test logic ...
cleanup.runAndThrow();

Inspecting the result​

MethodDescription
throwable()The captured throwable, or null if the runnable succeeded
hasRun()Whether run() or runAndThrow() has been called
isEmpty()Whether no throwable was captured
isNotEmpty()Whether a throwable was captured

Running multiple CleanUp instances​

The static runAndThrow(CleanUp...) and runAndThrow(Collection<CleanUp>) methods run all supplied instances and throw the first captured throwable with subsequent throwables added as suppressed exceptions:

CleanUp db = CleanUp.of(() -> database.close());
CleanUp conn = CleanUp.of(() -> connection.close());

try {
// ... test logic ...
} finally {
CleanUp.runAndThrow(db, conn);
}

Each instance is run only once. If an instance has already been run, its previously captured throwable is collected without re-execution.

caution

Each CleanUp instance may only be run once. Subsequent calls to run() or runAndThrow() throw IllegalStateException.

Integration with actions​

Use Retry and CleanUp inside Step or Lifecycle actions:

@Paramixel.Factory
public static Action<?> factory() {
return Instance.of(MyTest.class)
.child(Lifecycle.of("lifecycle")
.before("setUp()", MyTest::setUp)
.child("test()", MyTest::test)
.after("tearDown()", MyTest::tearDown)
.resolve())
.resolve();
}

// Inside a test method:
public void test() {
Retry.of(Policy.exponential(Duration.ofMillis(100), Duration.ofSeconds(5)))
.retryOn(e -> e instanceof java.net.ConnectException)
.onRetry((attempt, cause) -> logger.warn("Retry " + attempt))
.runAndThrow(() -> service.call());
}