Retry with Feign (OpenFeign)

Previous article introduced how to create an HTTP client with only annotations, but in the actual HTTP client it is necessary to retry. there is. In fact, there may be many projects that create an HTTP client class that has RestTemplate and perform retry processing there.

Let's realize this retry process with ʻOpen Feign`.

How it works

ʻOpenFeign` retry processing can be realized by implementing two interfaces.

-ErrorDecoder interface -Retryer interface

ErrorDecoder

ʻErrorDecoder is an interface for deciding what kind of ʻException is thrown when a response other than the 200s is returned in an HTTP request.

As an example, let's implement a class that returns ʻIllegalArgumentExceptionwhen400`.

class IllegalArgumentExceptionOn404Decoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() == 400) {
            throw new IllegalArgumentException("It's 400.");
        }
        return new ErrorDecoder.Default().decode(methodKey, response);
    }
}

Basically, I think that you can change the behavior by looking at response.status (). If you throw RetryableException inside decode, the followingRetryer.continueOrPropagateIs called.

Retryer

Retryer is called when RetryableException is thrown, and is an interface for managing the retry interval and the number of times.

The default Retryer is Retryer.Default tries 5 times in 100ms It is supposed to be done.

If you only want to change the retry interval and the number of times, I think Retryer.Default is enough.

How to use

Specify the execution class of the above two interfaces in ʻapplication.yml`.

feign:
  client:
    config:
      {{yourFeignName}}:
        errorDecode: com.example.feign.MyErrorDecoder
        retryer: com.example.feign.MyRetryer

Set yourFeignName to the one specified in FeignClient.name. If you separate FeignClient.name, you can separate the retry process for each name.

sample

Let's add a retry process to the previous Weather sample.

MyErrorDecoder.java In this sample, it is tried to retry when 503 and 504.

package com.example.ofc.feign;

import org.springframework.web.client.RestClientException;

import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;

public class MyErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        RestClientException cause = new RestClientException(response.toString());

        final int status = response.status();
        if (status == 503 || status == 504) {
            return new RetryableException(methodKey, cause, null);
        }

        return cause;
    }

}

MyRetryer.java I just modified Retryer.Default a little.

package com.example.ofc.feign;

import static java.util.concurrent.TimeUnit.SECONDS;

import feign.RetryableException;
import feign.Retryer;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyRetryer implements Retryer {

    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
    int attempt;
    long sleptForMillis;

    public MyRetryer() {
        this(100, SECONDS.toMillis(1), 3);
    }

    public MyRetryer(long period, long maxPeriod, int maxAttempts) {
        this.period = period;
        this.maxPeriod = maxPeriod;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
        return System.currentTimeMillis();
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        log.info("Retry processing");
        if (attempt++ >= maxAttempts) {
            throw e;
        }

        long interval;
        if (e.retryAfter() != null) {
            interval = e.retryAfter().getTime() - currentTimeMillis();
            if (interval > maxPeriod) {
                interval = maxPeriod;
            }
            if (interval < 0) {
                return;
            }
        } else {
            interval = nextMaxInterval();
        }
        try {
            Thread.sleep(interval);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
        }
        sleptForMillis += interval;
    }

    /**
     * Calculates the time interval to a retry attempt. <br>
     * The interval increases exponentially
     * with each attempt, at a rate of nextInterval *= 1.5 (where 1.5 is the
     * backoff factor), to the
     * maximum interval.
     *
     * @return time in nanoseconds from now until the next attempt.
     */
    long nextMaxInterval() {
        long interval = (long) (period * Math.pow(1.5, attempt - 1));
        return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
        return new MyRetryer(period, maxPeriod, maxAttempts);
    }
}

application.yml Specify the above two implementation classes.

server:
  port: 8088
  application:
    name: open-feign-client

feign:
  client:
    config:
      weather:
        errorDecoder: com.example.ofc.feign.MyErrorDecoder
        retryer: com.example.ofc.feign.MyRetryer

Summary

I tried to realize retry processing with ʻOpenFeign`. It can be realized without touching the API client itself, so it seems easy to repair.

Click here for this sample https://github.com/totto357/open-feign-client-example

Recommended Posts

Retry with Feign (OpenFeign)
Implement API client with only annotations using Feign (OpenFeign)
How to achieve file download with Feign
How to achieve file upload with Feign