Search code examples
spring-retrycircuit-breaker

Spring-retry - @Circuitbreaker is not retrying


I'm running into an issue where @CircuitBreaker is not retrying.

I have a service class (ex. Class UserService and method name getUser), this method calls another Spring bean (ex. AppClient and execute) which in turn makes call to remote service(REST call). the execute method is annoted with @CircuitBreaker of Spring-Retry.

I have exposed the call to service method (Class UserService and method name getUser) in rest controller and I test it using Postman. here is what happens - In case of timeout error, it does call @Recover method. But it wouldn't retry calling the remote service three times (default value).

If I manually run it 3 times through Postman, the circuit breaker state changes to OPEN and redirects calls to @Recover method and after reset time out, it resumes making calls to remote service.

Also, I replaced @CircuitBreaker with @Retryable and that makes call three times (default value). I'm using spring-retry version 1.2.1.RELEASE and aspectjtools version 1.6.2.

Why would it not retry with @CircuitBreaker?? I would like to have both features Circuit breaker and Retry. As per Documentation @CircuitBreaker is suppose to do both. Any help would be appreciated.

@Configuration
@EnableRetry
public class cfgUserApp
{

}

@RestController
public class UserController
{
@Autowired
UserService userService;

@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<User> getUser(@PathVariable String userId) {
    return ok(userService.getUser(userId));
}
}

/* Spring Bean -- userService */

public class UserServiceImpl
implements userService
{

@Override
public User getUser( final String userId )
{
checkArgument( User != null && !User.isEmpty(), "User Id can not be null or empty." );
try
{
    final HttpGet request = buildGetUserRequest( userId );
    final User userResult = appClient.execute( request,
        response -> createGetReservationResult( response ) );
    return userResult;
}
catch ( final IOException e )
{
    LOG.error( "getUser failed.", e );
    throw new AppException(e.getMessage(), e);
}
}
}

public Class Appclient {

@Recover
public <T> T recover(AppException appException, final HttpUriRequest request,
                            final ResponseHandler<T> responseFunction )
{
    System.out.println("In Recovery");
    return <T>new User();
}

@CircuitBreaker( include = AppException.class, openTimeout = 5000l, resetTimeout = 10000l )
    public <T> T execute( final HttpUriRequest request,
                          final ResponseHandler<T> responseFunction )
                          {
                          // HTTP call
                          }
}

Solution

  • It's probably a similar problem to this - annotation not found when a JDK proxy is used, since you have an interface.

    Move the annotation to the interface, or use

    @EnableRetry(proxyTargetClass = true)
    

    I added another commit to that PR to fix this.

    EDIT

    You seem to have mis-understood @CircuitBreaker; it does not retry internally; instead it is a stateful retry interceptor that fails over after the circuit breaker properties are exceeded.

    I changed your app to do this...

    @GetMapping("/getnumber")
    public int getNumber(){
        return this.userService.getNumber() + this.userService.getNumber() +
                this.userService.getNumber() + this.userService.getNumber();
    }
    

    and then I see

    getNumber
    fallback
    getNumber
    fallback
    getNumber
    fallback
    fallback
    

    To achieve what (I think) you want, you need to wrap the service with a retrying service, and put the recovery there:

    @SpringBootApplication
    @EnableRetry(proxyTargetClass = true)
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
    @RestController
    class UserRestController {
    
        private final RetryingUserService userService;
    
        @Autowired
        public UserRestController(RetryingUserService userService) {
            this.userService = userService;
        }
    
        @GetMapping("/getnumber")
        public int getNumber() {
            return this.userService.getNumber();
        }
    
    }
    
    @Service
    class RetryingUserService {
    
        private final UserService userService;
    
        public RetryingUserService(UserService userService) {
            this.userService = userService;
        }
    
        @Retryable
        public int getNumber() {
            return this.userService.getNumber();
        }
    
        @Recover
        public int fallback(RuntimeException re) {
            System.out.println("fallback");
            return 2;
        }
    
    }
    
    @Service
    class UserService {
    
        @CircuitBreaker(include = RuntimeException.class)
        public int getNumber() {
            System.out.println("getNumber");
            throw new RuntimeException();
        }
    
    }
    

    and

    getNumber
    getNumber
    getNumber
    fallback
    

    Or, you might want to put the retry within the circuit breaker, depending on what behavior you want.