/recurrent

Simple, sophisticated retries

Primary LanguageJavaApache License 2.0Apache-2.0

Recurrent

Build Status Maven Central License JavaDoc

Simple, sophisticated retries.

Introduction

Recurrent is a simple, zero-dependency library for performing retries. It was designed to be as easy to use as possible, with a concise API for handling everday use cases and the flexibility to handle everything else. Recurrent features:

Supports Java 6+ though the documentation uses lambdas for simplicity.

Usage

To start, define a RetryPolicy that expresses when retries should be performed:

RetryPolicy retryPolicy = new RetryPolicy()
  .retryOn(ConnectException.class)
  .withDelay(1, TimeUnit.SECONDS)
  .withMaxRetries(3);

Then use your RetryPolicy to execute a Runnable or Callable with retries:

// Run with retries
Recurrent.with(retryPolicy).run(() -> connect());

// Get with retries
Connection connection = Recurrent.with(retryPolicy).get(() -> connect());

Retry Policies

Recurrent's retry policies provide flexibility in allowing you to express when retries should be performed.

A policy can allow retries on particular failures:

RetryPolicy retryPolicy = new RetryPolicy()
  .retryOn(ConnectException.class, SocketException.class);
  .retryOn(failure -> failure instanceof ConnectException);

And for particular results or conditions:

retryPolicy
  .retryWhen(null);
  .retryIf(result -> result == null);  

We can add a fixed delay between retries:

retryPolicy.withDelay(1, TimeUnit.SECONDS);

Or a delay that backs off exponentially:

retryPolicy.withBackoff(1, 30, TimeUnit.SECONDS);

We can add a max number of retries and a max retry duration:

retryPolicy
  .withMaxRetries(100)
  .withMaxDuration(5, TimeUnit.MINUTES);

We can also specify which results, failures or conditions to abort retries on:

retryPolicy
  .abortWhen(true)
  .abortOn(NoRouteToHostException.class)
  .abortIf(result -> result == true)

And of course we can combine these things into a single policy.

Synchronous Retries

With a retry policy defined, we can perform a retryable synchronous execution:

// Run with retries
Recurrent.with(retryPolicy).run(() -> connect());

// Get with retries
Connection connection = Recurrent.with(retryPolicy).get(() -> connect());

Asynchronous Retries

Asynchronous executions can be performed and retried on a ScheduledExecutorService and return a RecurrentFuture. When the execution succeeds or the retry policy is exceeded, the future is completed and any listeners registered against it are called:

Recurrent.with(retryPolicy, executor)
  .get(() -> connect())
  .whenSuccess(connection -> log.info("Connected to {}", connection))
  .whenFailure((result, failure) -> log.error("Connection attempts failed", failure));

Execution Statistics

Recurrent exposes ExecutionStats that provide the number of execution attempts as well as start and elapsed times:

Recurrent.with(retryPolicy).get(stats -> {
  log.debug("Connection attempt #{}", stats.getExecutions());
  return connect();
});

CompletableFuture Integration

Java 8 users can use Recurrent to retry CompletableFuture calls:

Recurrent.with(retryPolicy, executor)
  .future(() -> connectAsync())
    .thenApplyAsync(value -> value + "bar")
    .thenAccept(System.out::println));

Java 8 Functional Interfaces

Recurrent can be used to create retryable Java 8 functional interfaces:

Function<String, Connection> connect = address -> Recurrent.with(retryPolicy).get(() -> connect(address));

We can retry streams:

Recurrent.with(retryPolicy).run(() -> Stream.of("foo").map(value -> value + "bar"));

Individual Stream operations:

Stream.of("foo").map(value -> Recurrent.with(retryPolicy).get(() -> value + "bar"));

Or individual CompletableFuture stages:

CompletableFuture.supplyAsync(() -> Recurrent.with(retryPolicy).get(() -> "foo"))
  .thenApplyAsync(value -> Recurrent.with(retryPolicy).get(() -> value + "bar"));

Event Listeners

Recurrent supports event listeners that can be notified of various events such as when retries are performed and when executions complete:

Recurrent.with(retryPolicy)
  .with(new Listeners<Connection>()
    .whenRetry((c, f, stats) -> log.warn("Failure #{}. Retrying.", stats.getExecutions()))
    .whenFailure((cxn, failure) -> log.error("Connection attempts failed", failure))
    .whenSuccess(cxn -> log.info("Connected to {}", cxn)))
  .get(() -> connect());

You can also implement listeners by extending the Listeners class and overriding individual event handlers:

Recurrent.with(retryPolicy)
  .with(new Listeners<Connection>() {
    public void onRetry(Connection cxn, Throwable failure, ExecutionStats stats) {
      log.warn("Failure #{}. Retrying.", stats.getExecutions());
    }
  
    public void onComplete(Connection cxn, Throwable failure) {
      if (failure != null)
        log.error("Connection attempts failed", failure);
      else
        log.info("Connected to {}", cxn);
    }
  }).get(() -> connect());

For asynchronous Recurrent executions, AsyncListeners can also be used to receive asynchronous callbacks for failed attempt and retry events. Asynchronous completion and failure listeners can be registered via RecurrentFuture.

Asynchronous API Integration

Recurrent can be integrated with asynchronous code that reports completion via callbacks. The runAsync, getAsync and futureAsync methods provide an AsyncExecution reference that can be used to manually perform retries or completion inside asynchronous callbacks:

Recurrent.with(retryPolicy, executor)
  .getAsync(execution -> service.connect().whenComplete((result, failure) -> {
    if (execution.complete(result, failure))
      log.info("Connected");
    else if (!execution.retry())
      log.error("Connection attempts failed", failure);
  }));

Recurrent can also perform asynchronous executions and retries on 3rd party schedulers via the Scheduler interface. See the Vert.x example for a more detailed implementation.

Execution Tracking

In addition to automatically performing retries, Recurrent can be used to track executions for you, allowing you to manually retry as needed:

Execution execution = new Execution(retryPolicy);
while (!execution.isComplete()) {
  try {
	doSomething();
    execution.complete();
  } catch (ConnectException e) {
    execution.fail(e);
  }
}

Execution tracking is also useful for integrating with APIs that have their own retry mechanism:

Execution execution = new Execution(retryPolicy);

// On failure
if (execution.canRetryOn(someFailure))
  service.scheduleRetry(execution.getWaitMillis(), TimeUnit.MILLISECONDS);

See the RxJava example for a more detailed implementation.

Example Integrations

Recurrent was designed to integrate nicely with existing libraries. Here are some example integrations:

Public API Integration

For library developers, Recurrent integrates nicely into public APIs, allowing your users to configure retry policies for different opererations. One integration approach is to subclass the RetryPolicy class, then expose that as part of your API while the rest of Recurrent remains internal. Another approach is to use something like the Maven shade plugin to relocate Recurrent into your project's package structure as desired.

Docs

JavaDocs are available here.

Contribute

Recurrent is a volunteer effort. If you use it and you like it, you can help by spreading the word!

License

Copyright 2015-2016 Jonathan Halterman - Released under the Apache 2.0 license.