Trying to exclude an exception using @Retryable - causes ExhaustedRetryException to be thrown

Les picture Les · Jul 27, 2016 · Viewed 12.9k times · Source

I'm trying to use @Retryable on a method that calls the REST template. If an error is returned due to a communication error, I want to retry otherwise I want to just thrown an exception on the call.

When the ApiException occurs, instead of it being thrown and ignored by @Retryable, I get an ExhaustedRetryException and a complaint about not finding enough 'recoverables', i.e, @Recover methods.

I thought I'd see if just having the recoverable method present might make it happy and still perform as hoped for. Not so much. Instead of throwing the exception, it called the recoverable method.

@Retryable(exclude = ApiException include = ConnectionException, maxAttempts = 5, backoff = @Backoff(multiplier = 2.5d, maxDelay = 1000000L, delay = 150000L))
Object call(String domainUri, ParameterizedTypeReference type, Optional<?> domain = Optional.empty(), HttpMethod httpMethod = HttpMethod.POST) throws RestClientException {

    RequestEntity request = apiRequestFactory.createRequest(domainUri, domain, httpMethod)
    log.info "************************** Request Entity **************************"
    log.info "${request.toString()}"
    ResponseEntity response

    try {

        response = restTemplate.exchange(request, type)
        log.info "************************** Response Entity **************************"
        log.info "${response.toString()}"

    } catch (HttpStatusCodeException | HttpMessageNotWritableException httpException) {

        String errorMessage
        String exceptionClass = httpException.class.name.concat("-")
        if(httpException instanceof HttpStatusCodeException) {

            log.info "************************** API Error **************************"
            log.error("API responded with errors: ${httpException.responseBodyAsString}")
            ApiError apiError = buildErrorResponse(httpException.responseBodyAsString)
            errorMessage = extractErrorMessage(apiError)

            if(isHttpCommunicationError(httpException.getStatusCode().value())) {
                throw new ConnectionException(exceptionClass.concat(errorMessage))
            }
        }

        errorMessage = StringUtils.isBlank(errorMessage) ? exceptionClass.concat(httpException.message) : exceptionClass.concat(errorMessage)
        throw new ApiException(httpMethod, domainUri, errorMessage)

    }

    if (type.type == ResponseEntity) {
        response
    }
    else response.body

}

@Recover
Object connectionException(ConnectionException connEx) {
    log.error("Retry failure - communicaiton error")
    throw new ConnectionException(connEx.class.name + " - " + connEx.message)
}

Any insights would be appreciated. Is it a bug or operator error? This is using Spring Boot 1.3.6 and Spring-Retry 1.1.3.

Answer

Gary Russell picture Gary Russell · Jul 27, 2016

Your include/exclude syntax looks bad - that won't even compile.

I just wrote a quick test and it works exactly as expected if you have zero @Recover methods...

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;

@SpringBootApplication
@EnableRetry
public class So38601998Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(So38601998Application.class, args);
        Foo bean = context.getBean(Foo.class);
        try {
            bean.out("foo");
        }
        catch (Exception e) {
            System.out.println(e);
        }
        try {
            bean.out("bar");
        }
        catch (Exception e) {
            System.out.println(e);
        }
    }


    @Bean
    public Foo foo() {
        return new Foo();
    }

    public static class Foo {

        @Retryable(include = IllegalArgumentException.class, exclude = IllegalStateException.class,
                maxAttempts = 5)
        public void out(String foo) {
            System.out.println(foo);
            if (foo.equals("foo")) {
                throw new IllegalArgumentException();
            }
            else {
                throw new IllegalStateException();
            }
        }

    }

}

Result:

foo
foo
foo
foo
foo
java.lang.IllegalArgumentException
bar
java.lang.IllegalStateException

If you just add

@Recover
public void connectionException(IllegalArgumentException e) {
    System.out.println("Retry failure");
}

You get

foo
foo
foo
foo
foo
Retry failure
bar
org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.IllegalStateException

So you need a catch-all @Recover method...

@Recover
public void connectionException(Exception e) throws Exception {
    System.out.println("Retry failure");
    throw e;
}

Result:

foo
foo
foo
foo
foo
Retry failure
bar
Retry failure
java.lang.IllegalStateException