10

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.

Les
  • 487
  • 1
  • 10
  • 22

1 Answers1

10

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
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Can you clarify what is wrong with the syntax? I'm not getting a compilation error on anything. I will try adding a blanket @Recover method. – Les Jul 27 '16 at 17:01
  • So I put in a catch-all @Recover method and the exception it is getting is the one that I said to exclude. – Les Jul 27 '16 at 17:16
  • 4
    Right - if you have at least one `@Recover` methods, you need a catch-all one even for the excluded (not retried) exceptions; it can rethrow like mine does). Re syntax: compare yours: `exclude = ApiException include = ConnectionException,` to mine: `include = IllegalArgumentException.class, exclude = IllegalStateException.class,` - needs a comma and `.class`. – Gary Russell Jul 27 '16 at 17:41
  • 2
    Is that documented some place? Maybe I saw it and glossed over it. My code is Groovy - Groovy doesn't require `.class` (nor semicolons!). – Les Jul 27 '16 at 17:48
  • I don't know groovy; you have commas everywhere else so it looked odd to me. Recovery is orthogonal to retries - the `RetryTemplate` gets a policy and a `RecoveryCallback` - the policy decides whether a particular exception is retried; the recovery callback (if supplied) is invoked for all failures - even those that aren't retried - see the Javadocs for `RetryTemplate` ... `Keep executing the callback until it either succeeds or the policy dictates that we stop, in which case the recovery callback will be executed.` If you have no `@Recover` there is no `RecoveryCallback`. – Gary Russell Jul 27 '16 at 18:01
  • 1
    @GaryRussell `@Recover method then throws java.lang.reflect.UndeclaredThrowableException in all cases.` And can't customize it. Is there a way to achieve it? Thanks. – Interstellar Mar 23 '21 at 01:07
  • I got the answer from [here](https://stackoverflow.com/questions/5490139/getting-undeclaredthrowableexception-instead-of-my-own-exception) as well as I had to use explicit RuntimeException to avoid tackle issue. – Interstellar Mar 23 '21 at 01:36
  • Don’t ask new questions on 5 year old answers. Ask a new question showing code and configuration and much more details of the problem. – Gary Russell Mar 23 '21 at 01:39