Retry with Spring - I

Spring batch provides an excellent paradigm to retry an operation in case the operation might succeed eventually. It offers couple retry strategies:

a. Retry a certain number of times. (Covered below)
b. Retry after a certain amount of time. (Will be covered in future post)

From their docs:

To make processing more robust and less prone to failure, sometimes it helps to automatically retry a failed operation in case it might succeed on a subsequent attempt. Errors that are susceptible to this kind of treatment are transient in nature. For example a remote call to a web service or RMI service that fails because of a network glitch or a DeadLockLoserException in a database update may resolve themselves after a short wait. To automate the retry of such operations Spring Batch has the RetryOperations strategy.

Problem statement: Lets say you have an api for booking cars and your api requires a token in the header for authentication and that token expires after 30 minutes. Lets say getting a token is an expensive operation and so you don't want to do it every time you want to call an api method.

Solution: a. Cache the token and get a fresh token only when you get 401 Unauthorized. b. Get fresh token once and once only for every 30 minutes.

Enter Spring Batch's Retry policy:

We used Spring's CacheManager to cache the token Caching implementation is not covered here) and RetryTemplate to retry the operation 1 more time after first failure on AuthorizationException

import com.rackspace.monitoring.BaseLoggingThing  
import com.rackspace.monitoring.errors.AuthorizationException  
import org.springframework.retry.RetryCallback  
import org.springframework.retry.RetryContext  
import org.springframework.retry.policy.SimpleRetryPolicy  
import org.springframework.retry.support.RetryTemplate

/**
 * Created by ravi on 11/17/14.
 */
class RetryExample extends BaseLoggingThing {  
    RetryTemplate retryTemplate = new RetryTemplate()
    CarRepository carRepository
    Map<Class<? extends Throwable>, Boolean> retryOnExceptions = [
        (AuthorizationException.class): true,
        (IllegalStateException.class) : true
    ]

    RetryExample(com.rackspace.monitoring.atomhopper.CarRepository carRepository, int numberOfRetries) {
        this.carRepository = carRepository
        retryTemplate.retryPolicy = new SimpleRetryPolicy(numberOfRetries, retryOnExceptions)
    }

    String getFreshTokenAndCacheIt() {
        // Expensive operation of getting fresh token
        // Cache the fresh token
        return "Token Refreshed"
    }

    String getCar(final String make) {
        return retryTemplate.execute(new RetryCallback<String>() {
            @Override
            String doWithRetry(RetryContext retryContext) throws Exception {
                try {
                    return carRepository.findCarNameByMake(make)
                } catch (AuthorizationException e) {
                    getFreshTokenAndCacheIt()
                    throw e
                }
            }
        })
    }
}

interface CarRepository {  
    public String findCarNameByMake(String make)
}

Lines of note above:
new SimpleRetryPolicy(numberOfRetries, retryOnExceptions): That's basically stating to retry numberOfRetries if any exception in retryOnExceptions collection occurs.

try {  
    return carRepository.findCarNameByMake(make)
} catch (AuthorizationException e) {
    getFreshTokenAndCacheIt()
    throw e
}

The code block above states: Get a fresh token if AuthorizationException occurs and rethrow it. If an AuthorizationException happens again, Spring Batch will not retry since it would have exceeded the maximum number of retires (2 in our case).

Here's the test code:

import com.rackspace.monitoring.errors.AuthorizationException  
import spock.lang.Specification  
import spock.lang.Unroll

/**
 * Created by ravi on 11/17/14.
 */
class RetryExampleTest extends Specification {

    CarRepository carRepository = Mock()
    // Number of retries is set to 2
    RetryExample retryExample = new RetryExample(carRepository, 2)

    @Unroll
    def 'returns valid value in second attempt if acceptable exception is thrown on first attempt'() {
        when:
        def car = retryExample.getCar('Toyota')

        then:
        car == 'Camry'
        2 * carRepository.findCarNameByMake(_) >> { throw exception.newInstance('foo') } >>> ['Camry']

        where:
        exception << [IllegalStateException, AuthorizationException]
    }

    @Unroll
    def 'retries only x times even if acceptable exception is thrown'() {
        when:
        retryExample.getCar('Toyota')

        then:
        thrown exception
        // Retries 2 times since number of retries is set to 2
        2 * carRepository.findCarNameByMake(_) >> { throw exception.newInstance('foo') }

        where:
        exception << [IllegalStateException, AuthorizationException]
    }

    def 'does not retry if an unexpected exception is thrown'() {
        when:
        retryExample.getCar('Toyota')

        then:
        thrown NumberFormatException
        // Does not retry because NumberFormatException is not acceptable
        1 * carRepository.findCarNameByMake(_) >> { throw new NumberFormatException('foo') } >>> ['Camry']
    }
}