Skip to content

Commit

Permalink
Update readme.
Browse files Browse the repository at this point in the history
  • Loading branch information
lpryor committed Sep 12, 2014
1 parent 75c97b2 commit 5ce0e4f
Showing 1 changed file with 79 additions and 39 deletions.
118 changes: 79 additions & 39 deletions README.md
Expand Up @@ -31,7 +31,7 @@ def doSomethingUntilItWorks(): String = {
}
```

Retry loops like the one above bring a number of issues with them:
Retry loops like the one above have a number of problems:

- They obscure the actual work that the program is trying to do (the lone call to `doSomethingThatMightFail()` above).

Expand All @@ -53,7 +53,7 @@ val result = retry() { doSomethingThatMightFail() }

In addition to making retry behavior easy to understand, atmos provides the ability to customize the strategies that control [loop termination](#termination-policies), [backoff calculation](#backoff-policies), [error handling](#error-classifiers) and [event monitoring](#event-monitors), as well as supporting both [synchronous](#retrying-synchronously) and [asynchronous](#retrying-asynchronously) styles of programming. See the [user guide](#using-the-library) below for information about the wide array of customization options this library supports.

[code](https://github.com/zmanio/atmos) - [api](http://zman.io/atmos/api/#atmos.package) - [history](changelog/)
[code](https://github.com/zmanio/atmos) - [licence](https://github.com/zmanio/atmos/blob/master/LICENSE) - [api](http://zman.io/atmos/api/#atmos.package) - [history](changelog/)

[![Build Status](https://travis-ci.org/zmanio/atmos.png?branch=master)](https://travis-ci.org/zmanio/atmos) [![Coverage Status](https://coveralls.io/repos/zmanio/atmos/badge.png)](https://coveralls.io/r/zmanio/atmos)

Expand All @@ -68,7 +68,6 @@ In addition to making retry behavior easy to understand, atmos provides the abil
- [Retrying Synchronously](#retrying-synchronously)
- [Retrying Asynchronously](#retrying-asynchronously)
- [Retrying with Actors](#retrying-with-actors)
- [Example Retry Policies](#example-retry-policies)
- [Building and Testing](#building-and-testing)

<a name="getting-started"></a>
Expand All @@ -77,30 +76,28 @@ In addition to making retry behavior easy to understand, atmos provides the abil

Prerequisites:

- [Scala](http://scala-lang.org/) 2.10.x
- [Java](http://www.oracle.com/technetwork/java/index.html) 1.6+

<!---
- [SBT](http://www.scala-sbt.org/) or a similar build tool.
- [Scala](http://scala-lang.org/) 2.10.x

To use atmos in your project simply add one line to your SBT configuration:
To use from SBT, add the following to your build.sbt file:

```scala
libraryDependencies += "io.zman" %% "atmos" % "2.0"
```

[Instructions for other build systems](http://mvnrepository.com/artifact/io.zman/atmos_2.10/2.0).
-->
For other build systems or to download the jar see [atmos in the central repository](http://mvnrepository.com/artifact/io.zman/atmos_2.10/2.0).

<a name="using-the-library"></a>

## Using the Library

The atmos library divides the definition of a retry policy into four parts:

- [Termination policies](#termination-policies) enforce an upper bound on the number of retry attempts that are made.
- [Backoff policies](#backoff-policies) calculate the delay that is inserted before subsequent retry attempts.
- [Error classifiers](#error-classifiers) define the strategy used to determine if an error prevents further attempts.
- [Event monitors](#event-monitors) are notified of events that occur while performing a retry operation.
- [Termination policies](#termination-policies) enforce an upper bound on the number of retry attempts.
- [Backoff policies](#backoff-policies) calculate the delay that is inserted between retry attempts.
- [Error classifiers](#error-classifiers) determine if an error prevents further attempts.
- [Event monitors](#event-monitors) are notified of events that occur during a retry operation.

Using the naive retry loop from above, we can classify its behavior according to the four elements of a retry policy:

Expand Down Expand Up @@ -128,13 +125,13 @@ while (true) {
}
```

Atmos decomposes the traditional retry loop into these four, independent strategies and allows you to easily recombine them in whatever fashion you see fit. A reconstructed retry policy is encapsulated in the `atmos.RetryPolicy` class.
Atmos decomposes the traditional retry loop into these four, independent strategies and allows you to easily recombine them in whatever fashion you see fit. A reconstructed retry policy is encapsulated in the [`atmos.RetryPolicy`](http://zman.io/atmos/api/#atmos.RetryPolicy) class.

<a name="termination-policies"></a>

### Termination Policies

Termination policies determine when a retry operation will make no further attempts. Any type that implements the `atmos.TerminationPolicy` trait can be used in a retry policy, but the DSL exposes factory methods for creating the most common implementations. DSL methods that define termination policies return a `RetryPolicy` configured with that termination policy and with default values for its other properties.
Termination policies determine when a retry operation will make no further attempts. Any type that implements the [`atmos.TerminationPolicy`](http://zman.io/atmos/api/#atmos.TerminationPolicy) trait can be used in a retry policy, but the DSL exposes factory methods for creating the most common implementations. DSL methods that define termination policies return a [`RetryPolicy`](http://zman.io/atmos/api/#atmos.RetryPolicy) configured with that termination policy and with default values for its other properties.

A default retry policy that limits an operation to 3 attempts can be created with `retrying`:

Expand Down Expand Up @@ -189,7 +186,7 @@ val otherRetryPolicy = retryForever

### Backoff Policies

Backoff policies specify the delay before subsequent retry attempts and are configured by calling `using` on an existing retry policy. Any type that implements the `atmos.BackoffPolicy` trait can be used in a retry policy, but the DSL exposes factory methods for creating the most common implementations.
Backoff policies specify the delay before subsequent retry attempts and are configured by calling `using` on an existing retry policy. Any type that implements the [`atmos.BackoffPolicy`](http://zman.io/atmos/api/#atmos.BackoffPolicy) trait can be used in a retry policy, but the DSL exposes factory methods for creating the most common implementations.

There are four basic backoff policies provided by this library:

Expand Down Expand Up @@ -243,16 +240,16 @@ import atmos.dsl._
implicit val retryPolicy = retryForever using { constantBackoff { 1 second } randomized 100.millis }

// Randomizes each backoff duration by adding a random duration between -30 seconds and 30 seconds.
val otherRetryPolicy = retryForever using { linearBackoff { 5 minues } randomized -30.seconds -> 30.seconds }
val otherRetryPolicy = retryForever using { linearBackoff { 5 minutes } randomized -30.seconds -> 30.seconds }
```

<a name="error-classifiers"></a>

### Error Classifiers

Errors that occur during a retry attempt can be classified as `Fatal`, `Recoverable` or `SilentlyRecoverable`. `Fatal` errors will interrupt a retry operation and cause it to immediately fail. `Recoverable` errors will be logged and suppressed so that the retry operation can continue. `SilentlyRecoverable` errors will be suppressed without being logged so that the retry operation can continue. Error classifications are defined in `atmos.ErrorClassification`.
Errors that occur during a retry attempt can be classified as `Fatal`, `Recoverable` or `SilentlyRecoverable`. `Fatal` errors will interrupt a retry operation and cause it to immediately fail. `Recoverable` errors will be logged and suppressed so that the retry operation can continue. `SilentlyRecoverable` errors will be suppressed without being logged so that the retry operation can continue. Error classifications are defined in [`atmos.ErrorClassification`](http://zman.io/atmos/api/#atmos.ErrorClassification).

Error classifiers are simply implementations of `PartialFunction` that map instances of `Throwable` to the desired error classification. In situations where a classifier is not defined for a particular error, `scala.util.control.NonFatal` is used to classify errors as `Fatal` or `Recoverable`. The appropriate partial function type is defined as `atmos.ErrorClassifier` and includes a helper factory object.
Error classifiers are simply implementations of `PartialFunction` that map instances of `Throwable` to the desired error classification. In situations where a classifier is not defined for a particular error, `scala.util.control.NonFatal` is used to classify errors as `Fatal` or `Recoverable`. The appropriate partial function type is defined as [`atmos.ErrorClassifier`](http://zman.io/atmos/api/#atmos.ErrorClassifier) and includes a factory in the companion object.

Error classifiers are configured by calling `onError` on an existing retry policy:

Expand All @@ -273,7 +270,12 @@ val otherRetryPolicy = retryForever onError {

### Event Monitors

Event monitors are notified when retry attempts fail and are configured on a retry policy using `monitorWith`. Any type that implements the `atmos.EventMonitor` trait can be used in a retry policy, but the DSL exposes factory methods for creating the most common implementations.
Event monitors are notified when retry attempts fail and are configured on a retry policy using `monitorWith`. Any type that implements the [`atmos.EventMonitor`](http://zman.io/atmos/api/#atmos.EventMonitor) trait can be used in a retry policy, but the DSL exposes factory methods for creating the most common implementations.

Event monitors handle three distinct types of events:
- Retrying events occur when an attempt has failed but another attempt is going to be made.
- Interrupted events occur when an attempt has failed with a fatal error.
- Aborted events occur when too many attempts have been made and failed.

This library supports using instances of Java's `PrintStream` and `PrintWriter` as targets for logging retry events. The specifics of what is printed can be customized for each type of event:

Expand All @@ -300,7 +302,7 @@ implicit val retryPolicy = retryForever monitorWith Logger.getLogger("MyLoggerNa

// Submit information about failed attempts to the specified instance of `java.util.logging.Logger`, customizing
// what events get logged and and at what level.
val otheretryPolicy = retryForever monitorWith {
val otherRetryPolicy = retryForever monitorWith {
Logger.getLogger("MyLoggerName") onRetrying logNothing onInterrupted logWarning onAborted logError
}

Expand All @@ -325,7 +327,7 @@ val akkaRetryPolicy = retryForever monitorWith {

### Retrying Synchronously

When you retry synchronously you pass a block of code to the `retry()` method and that block is repeatedly executed until it completes successfully or ultimately fails in accordance with your policy. If a block completes successfully then the value the block evaluates to becomes the return value of `retry()`. If a block fails to complete successfully, meaning it was interrupted by a fatal error or had to abort after too many attempts, then the most recently thrown exception is thrown from `retry()`.
To retry synchronously you pass a block of code to the `retry()` method and that block is repeatedly executed until it completes successfully or ultimately fails in accordance with your policy. If a block completes successfully then the value the block evaluates to becomes the return value of `retry()`. If a block fails to complete successfully, meaning it was interrupted by a fatal error or had to abort after too many attempts, then the most recently thrown exception is thrown from `retry()`.

Typically, a retry policy is declared as an implicit variable and the `retry()` method from the DSL is used to execute a synchronous retry operation. However, if you have multiple policies in the same scope (or if you want to avoid using implicit parameters) you can also call `retry()` directly on the policy object:

Expand All @@ -348,11 +350,11 @@ implicit val policy = retryForever

// The following two statements will have a custom operation name in log messages:
retry("Doing something") { doSomething() }
val result = retry(Some("Getting something")) { getSomething() }
val result1 = retry(Some("Getting something")) { getSomething() }

// The following two statements will have a generic operation name in log messages:
retry() { doSomethingMysterious() }
val result = retry(None) { getSomethingMysterious() }
policy.retry() { doSomethingMysterious() }
val result2 = policy.retry(None) { getSomethingMysterious() }
```

It is important to note that synchronous retry operations will block the calling thread while waiting for a backoff duration to expire. Use synchronous retries carefully in situations where you do not control the calling thread.
Expand All @@ -361,43 +363,81 @@ It is important to note that synchronous retry operations will block the calling

### Retrying Asynchronously

Atmos also supports asynchronous retries using Scala Futures. There are, however, some additional dependencies that must be considered before retrying asynchronously:
Atmos supports asynchronous retries using Scala Futures. Asynchronous retries are much more involved than their single-threaded cousins, so care must be taken to understand the retry execution model.

To retry asynchronously you call the `retryAsync()` method and pass it a block of code that evaluates to a `scala.concurrent.Future`. A single asyncnhronous attempt consists of executing the block *and* evaluating the outcome of the resulting future. If either the block or the future fails then the attempt fails and normal retry behavior takes over. The `retryAsync()` method returns a future that tracks the entire retry operation regardless of how many attempts are made. If any attempt succeeds then the returned future succeeds with the same value, if the operation fails then the returned future fails with the last reported exception.

- There must be an implicit `scala.concurrent.ExecutionContext` available at the point the retry operation is invoked. This execution context is where subsequent attempts will be run and can typically be the same context used to execute your futures.
- You may optionally define an implicit `rummage.Timer` from the [rummage](http://zman.io/rummage) project. This is the component responsible for providing delayed callbacks when a backoff duration expires and is backed by a single daemon thread by default. It is unlikely that you will need to provide a custom timer unless you are working with [actors](#retrying-with-actors).
When retrying asynchronously, certain additional dependencies must be specified:

Asynchronous retriy operations are executed with the `retryAsync` method. In the same fashion as the synchronous form, you may optionally provide an operation name and you can either call this method via the DSL with an implicit retry policy or directly on the retry policy itself.
- There must be an implicit `scala.concurrent.ExecutionContext` available at the point the retry operation is invoked. This execution context is where the block provided to `retryAsync()` will be executed during subsequent retries. This can typically be the same context used to execute your futures (if applicable).
- You may optionally define an implicit `rummage.Timer`, from the [rummage](http://zman.io/rummage) project, at the point the retry operation is invoked. This is the component responsible for providing non-blocking, asynchronous callbacks based on when a backoff duration expires. By default, timing is controlled by a singular, global daemon thread. It is unlikely that you will need to provide a custom timer unless you are working with [actors](#retrying-with-actors).

Asynchronous retries support the same operations as the synchronous form: you may optionally provide an operation name and you can either call this method via the DSL with an implicit retry policy or directly on the retry policy itself.

```scala
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._
import scala.concurrent.ExecutionContext.Implicits.global
import atmos.dsl._

implicit val policy = retryForever

// The following two statements will have a custom operation name in log messages:
retryAsync("Doing something") { Future { doSomething() } }
retryAsync(Some("Doing something else")) { doSomethingElse() }
retryAsync("Doing something in the future") { Future { doSomething() } }
val futureResult = retryAsync(Some("Getting something in the future")) { Future { getSomething() } }

// The following two statements will have a generic operation name in log messages:
retryAsync() { doSomethingMysterious() }
retryAsync(None) { doSomethingElseMysterious() }
policy.retryAsync() { Future { doSomethingMysterious() } }
val futureResult = policy.retryAsync(None) { Future { getSomethingInTheFuture() } }
```

<a name="retrying-with-actors"></a>

### Retrying with Actors

(work-in-progress)
The atmos library has built-in support for [Akka](http://akka.io/), specifically for retrying asynchronously when using the ask pattern. To use this library with actors there are only a couple extra steps involved beyond what is described in [Retrying Asynchronously](#retrying-asynchronously) above.

First, you will want to make sure you have an implicit instance of `rummage.AkkaTimer` from the [rummage](http://zman.io/rummage) project in scope, this will make sure that your actor system is the one responsible for scheduling asynchronous backoff timers. Second, you'll want to make sure and use Akka logging support to keep your entire retry operation non-blocking.

```scala
import scala.concurrent.duration._
import akka.actor.{ ActorRef, ActorSystem }
import akka.event.Logging
import akka.pattern.{ ask, pipe }
import akka.util.Timeout
import rummage.AkkaTimer
import atmos.dsl._
import AkkaSupport._

<a name="example-retry-policies"></a>
val system: ActorSystem = ???
val actor: ActorRef = ???
val otherActor: ActorRef = ???

### Example Retry Policies
implicit val context = system.dispatcher
implicit val timer = AkkaTimer(system)
implicit val timeout = Timeout(2 seconds)
implicit val policy = retryForever monitorWith Logging(system, this.getClass)

(work-in-progress)
retryAsync("Ask an actor over and over") { actor ? "Hello!" } pipeTo otherActor
```

<a name="building-and-testing"></a>

## Building and Testing

(work-in-progress)
Atmos uses [SBT](http://www.scala-sbt.org/), so kicking off a build or running test cases is nice and straightforward.

```sh
# Clone the repository:
git clone git@github.com:zmanio/atmos.git
cd atmos

# Build and package a jar file:
sbt package

# Run the test suite
sbt test

# Generate test coverage reports:
sbt scoverage:test
```

0 comments on commit 5ce0e4f

Please sign in to comment.