Feign client and Spring retry

Viewed 34320

I have a restful service calling an external service using Spring Cloud Feign client

@FeignClient(name = "external-service", configuration = FeignClientConfig.class)
public interface ServiceClient {

    @RequestMapping(value = "/test/payments", method = RequestMethod.POST)
    public void addPayment(@Valid @RequestBody AddPaymentRequest addPaymentRequest);

    @RequestMapping(value = "/test/payments/{paymentId}", method = RequestMethod.PUT)
    public ChangePaymentStatusResponse updatePaymentStatus(@PathVariable("paymentId") String paymentId,
            @Valid @RequestBody PaymentStatusUpdateRequest paymentStatusUpdateRequest);

}

I noticed the following failure 3-4 times in the last 3 months in my log file:

json.ERROR_RESPONSE_BODY:Connection refused executing POST http://external-service/external/payments json.message:Send Payment Add Payment Failure For other reason: {ERROR_RESPONSE_BODY=Connection refused executing POST http://external-service/external/payments, EVENT=ADD_PAYMENT_FAILURE, TRANSACTION_ID=XXXXXXX} {} json.EVENT:ADD_PAYMENT_FAILURE json.stack_trace:feign.RetryableException: Connection refused executing POST http://external-service/external/payments at feign.FeignException.errorExecuting(FeignException.java:67) at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:104) at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76) at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)

Is it possible to add Spring Retry on a Feign client. What I wanted to annotate the addPayment operation with

@Retryable(value = {feign.RetryableException.class }, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier=2))

But this is not possible, what other options do I have?

7 Answers

You can add a Retryer in the FeignClientConfig

@Configuration
public class FeignClientConfig {

    @Bean
    public Retryer retryer() {
        return new Custom();
    }

}

class Custom implements Retryer {

    private final int maxAttempts;
    private final long backoff;
    int attempt;

    public Custom() {
        this(2000, 3);
    }

    public Custom(long backoff, int maxAttempts) {
        this.backoff = backoff;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }

    public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e;
        }

        try {
            Thread.sleep(backoff);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public Retryer clone() {
        return new Custom(backoff, maxAttempts);
    }
}

Updated with sample Retryer example config based on the Retryer.Default.

If you are using ribbon you can set properties, you can use below properties for retry:

myapp.ribbon.MaxAutoRetries=5
myapp.ribbon.MaxAutoRetriesNextServer=5
myapp.ribbon.OkToRetryOnAllOperations=true

Note: "myapp" is your service id.

Checkout this Github implementation for working example

Just new a contructor Default

@Configuration
public class FeignClientConfig {
    @Bean
    public Retryer retryer() {
        return new Retryer.Default(100, 2000, 3);
    }
}

I prepared a blog post about using Spring Retry with Feign Client methods. You may consider checking the Post. All steps have been explained in the post.

This is my config. Test OK in spring boot 2.2.0.RELEASE spring cloud Hoxton.M3.

feign.hystrix.enabled=true
MY-SPRING-API.ribbon.MaxAutoRetries=2
MY-SPRING-API.ribbon.MaxAutoRetriesNextServer=2
MY-SPRING-API.ribbon.OkToRetryOnAllOperations=true
MY-SPRING-API.ribbon.retryableStatusCodes=404,500

feign.client.config.PythonPatentClient.connectTimeout=500
feign.client.config.PythonPatentClient.readTimeout=500

hystrix.command.PythonPatentClient#timeTest(String).execution.isolation.thread.timeoutInMilliseconds=5000

java code is :

@FeignClient(name = "MY-SPRING-API",configuration = {PythonPatentConfig.class},fallbackFactory = FallBack.class)
public interface PythonPatentClient 
@RequestLine("GET /test?q={q}")
    void timeTest(@Param("appNo") String q);

Controller is :

  @RequestMapping(value = "/test",method = {RequestMethod.POST,RequestMethod.GET})
    public Object test() throws InterruptedException {
        log.info("========important print enter test========");
         TimeUnit.SECONDS.sleep(10L);

pom.xml additon add:

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

optional:

<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
    </dependency>

@EnableRetry
@SpringBootApplication
public class ApiApplication

this is document :

https://docs.spring.io/spring-cloud-netflix/docs/2.2.10.RELEASE/reference/html/#retrying-failed-requests

https://github.com/spring-projects/spring-retry

https://github.com/spring-cloud/spring-cloud-netflix/

I resolved that by creating a wrapper on top of ServiceClient

@Configuration
public class ServiceClient {

@Autowired
ServiceFeignClient serviceFeignClient;

@Retryable(value = { ClientReprocessException.class }, maxAttemptsExpression = "#{${retryMaxAttempts}}", backoff = @Backoff(delayExpression = "#{${retryDelayTime}}"))

public void addPayment( AddPaymentRequest addPaymentRequest){
    return serviceFeignClient.addPayment(addPaymentRequest);
 }    
}
Related