34

The following code is not retrying. What am I missing?

@EnableRetry
@SpringBootApplication
public class App implements CommandLineRunner
{
    .........
    .........


    @Retryable()
    ResponseEntity<String> authenticate(RestTemplate restTemplate, HttpEntity<MultiValueMap<String, String>> entity) throws Exception
    {
        System.out.println("try!");
        throw new Exception();
        //return restTemplate.exchange(auth_endpoint, HttpMethod.POST, entity, String.class);
    }

I have added the following to the pom.xml.

    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
        <version>1.1.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

I also tried providing different combinations of arguments to @Retryable.

@Retryable(maxAttempts=10,value=Exception.class,backoff=@Backoff(delay = 2000,multiplier=2))

Thanks.

engg
  • 481
  • 4
  • 7
  • 6

11 Answers11

56

In spring boot 2.0.2 Release, I have observed that the @Retryable is not working if you have retryable and called method in same class. On debugging found that the pointcut is not getting built properly. For now, the workaround for this problem is that we need to write the method in a different class and call it.

Working Example could be found here.

Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
nkharche
  • 917
  • 9
  • 10
  • I had the same experience with spring boot 1.5.x. – Cornelia Davis Jul 03 '18 at 03:30
  • 2
    observed the same with spring boot 2.0.0 to 2.0.5. Checked mvn repo: no version of spring-retry after 2017, while spring boot 2 arrived in 2018. is there a savior who managed to make spring-retry work with any version of spring-boot 2.x? In the meantime rollbacked to spring boot 1.5.15 and it works fine – Jeremie Sep 06 '18 at 14:17
  • thats a generic spring thing, you can use @Autowired self reference? – Kalpesh Soni Aug 27 '19 at 18:21
  • 10
    It's just how all Spring annotations work. You need a separate proxy object for Soring to be able to apply additional logic. – iozee Mar 30 '20 at 10:48
  • 4
    This is because of Spring creates a Proxy object to handle the retrying and it cannot create a proxy object of itself. Same problem happens when using other annotations as `@Transactional` – Federico Piazza Apr 29 '20 at 21:28
  • Great catch, it started working as soon as I put it in a different class, I annotated it as Configuration instead of Service though, but it serves as a Service to call the retryable method. – Yashpal Oct 11 '22 at 19:44
17

Spring's @Retryable, @Cacheable, @Transaction, etc. are ALL implemented using Aspect Oriented Programming. Spring implements AOP via proxy-based weaving. Proxies intercept calls from one bean to another. Proxies cannot intercept calls from one object's methods to another. This is a general limitation of proxy based weaving.

The following solutions address this limitation: 1) as mentioned above, use @Autowired (or @Resource) to inject a bean with a self reference; calls to this reference transit the proxy. 2) Use AspectJ's ClassLoader instead of Spring's default proxy-based weaving. 3) As mentioned above, place the methods on separate beans. I've done each in various situations, each has pros and cons.

Ken Krueger
  • 1,005
  • 14
  • 26
16

For the @Retryable annotation on the method to be discovered it needs to be called correctly from an initialised context. Is the method invoked from a bean from the spring context or called by other means?

If testing this is your runner using the SpringJunit4ClassRunner?

UserF40
  • 3,533
  • 2
  • 23
  • 34
  • Thanks, I haven't got a chance to look at this, I will get back. – engg Jul 17 '16 at 01:31
  • 3
    This should be the accepted answer, when you say "I figured out that if return something from the method that you trying to retry, then @Retryable() is not working." is wrong, you can actually return a object in the retry method, the point is that the method needs to be called from a bean. – fsimon Aug 20 '17 at 20:33
9

I solved it. I figured out that if return something from the method that you trying to retry, then @Retryable() is not working.

maven dependency in pom.xml

    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
        <version>1.1.5.RELEASE</version>
    </dependency>

Spring boot Application.java

@SpringBootApplication
@EnableTransactionManagement
@EnableRetry
public class Application {

     public static void main(String[] args) throws Exception {
       SpringApplication.run(Application.class, args);
     }

}

in controller.java

@RestController
public class JavaAllDataTypeController {

@Autowired
JavaAllDataTypeService JavaAllDataTypeService;


@RequestMapping(
        value = "/springReTryTest",
        method = RequestMethod.GET
)
public ResponseEntity<String> springReTryTest() {

    System.out.println("springReTryTest controller");

    try {
         JavaAllDataTypeService.springReTryTest();
    } catch (Exception e) {
        e.printStackTrace();
    }

    return new  ResponseEntity<String>("abcd", HttpStatus.OK);
  }

}

in service.java

@Service
@Transactional
public class JavaAllDataTypeService {

 // try the method 9 times with 2 seconds delay.
 @Retryable(maxAttempts=9,value=Exception.class,backoff=@Backoff(delay = 2000))
 public void springReTryTest() throws Exception {

    System.out.println("try!");
    throw new Exception();
  }

}

output: It' trying 9 times then throwing exception.

enter image description here

Community
  • 1
  • 1
Ferdous Wahid
  • 3,227
  • 5
  • 27
  • 28
6

For those who want to call @Retryable block in same class can to this way.

The key here is not to call the method directly and through self-injected bean

@Slf4j
@Service
public class RetryService {

    @Resource(name = "retryService")
    private RetryService self;

    public String getValue(String appender) {
        return self.getData(appender);
    }

    @Retryable(value = NumberFormatException.class, maxAttempts = 4, backoff = @Backoff(500))
    public String getData(String appender) {
        log.info("Calling getData");
        Integer value = Integer.parseInt(appender);
        value++;
        return value.toString();
    }

    @Recover
    public String recoverData(String appender) {
        log.info("Calling recoverData");
        return "DEFAULT";
    }

}

Can read more about using Retry in detail here

MyTwoCents
  • 7,284
  • 3
  • 24
  • 52
  • I'm getting this error after I try something like this: "The dependencies of some of the beans in the application context form a cycle". – ArtOfWarfare Aug 09 '22 at 15:23
5

I had exactly the same issue as described in the original question.

In my case it turned out that the spring-boot-starter-aop dependency was accidentally not included. After adding it to my pom.xml, my @Retryable methods worked as expected.

Returning values from @Retryable methods works fine for me.

Bas
  • 123
  • 2
  • 9
4

It work for return type as well

@Service
public class RetryService {

private int count = 0;

// try the method 9 times with 2 seconds delay.
@Retryable(maxAttempts = 9, value = Exception.class, backoff = @Backoff(delay = 2000))
public String springReTryTest() throws Exception {
    count++;
    System.out.println("try!");

    if (count < 4)
        throw new Exception();
    else
        return "bla";
  }

}
Chinmay Samant
  • 258
  • 1
  • 3
  • 13
3

Even I faced the same issue, Later after some investigation and research came to know that along with @Retryable annotation above the method we also need to provide @EnableRetry above the class. This @EnableRetry annotation either can be provided above same class in to which you have provided method you want to retry or above your main spring boot application class. For example like this:

@RequiredArgsConstructor
**@EnableRetry**
@Service
public class SomeService {

  **@Retryable(value = { HttpServerErrorException.class, BadRequestException.class},
      maxAttempts = maxRetry, backoff = @Backoff(random = true, delay = 1000,
                         maxDelay = 8000, multiplier = 2))**
  public <T> T get( ) throws  HttpServerErrorException, BadRequestException {
    
     //write code here which you want to retry
  }

}

I hope this will help and resolve your issue.

3

Pretty old thread, but I wanted to share that after changing my method visibility from private to public, Retryable was successfully retrying.

This is in addition to using the self resource mentioned above.

louis1204
  • 401
  • 1
  • 7
  • 21
2

An alternative could be RetryTemplate

@Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2);
        retryTemplate.setRetryPolicy(retryPolicy);

        return retryTemplate;
    }

and

retryTemplate.execute(new RetryCallback<Void, RuntimeException>() {
    @Override
    public Void doWithRetry(RetryContext arg0) {
        myService.templateRetryService();
        ...
    }
});

worked out for me

source

BiScOtTiNo
  • 175
  • 11
0

I got this one solved by moving @Retryable directly in front of the method I wanted to retry.

From this:

public class MyClass {

  public String toBeRetried() {
    return delegateTo();
  }

  @Retryable
  public String delegateTo() {
    throw new Exception();
  }
}

To this:

public class MyClass {

  @Retryable
  public String toBeRetried() {
    throw new Exception();
  }
}
user9869932
  • 6,571
  • 3
  • 55
  • 49