001
002package io.vrap.rmf.base.client.http;
003
004import java.time.temporal.ChronoUnit;
005import java.util.List;
006import java.util.Optional;
007import java.util.concurrent.CompletableFuture;
008import java.util.concurrent.ExecutorService;
009import java.util.concurrent.ScheduledExecutorService;
010import java.util.function.Function;
011
012import io.vrap.rmf.base.client.*;
013import io.vrap.rmf.base.client.utils.json.JsonException;
014import io.vrap.rmf.base.client.utils.json.JsonUtils;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019import dev.failsafe.Failsafe;
020import dev.failsafe.FailsafeExecutor;
021import dev.failsafe.RetryPolicy;
022import dev.failsafe.event.ExecutionAttemptedEvent;
023import dev.failsafe.spi.Scheduler;
024
025/**
026 * Implementation for a retry of a requests upon configured response status codes
027 */
028public class RetryMiddleware implements RetryRequestMiddleware, AutoCloseable {
029    static final String loggerName = ClientBuilder.COMMERCETOOLS + ".retry";
030
031    /**
032     * @deprecated use {@link RetryRequestMiddleware#DEFAULT_MAX_DELAY} instead
033     */
034    @Deprecated
035    public static final int DEFAULT_MAX_DELAY = RetryRequestMiddleware.DEFAULT_MAX_DELAY;
036    /**
037     * @deprecated use {@link RetryRequestMiddleware#DEFAULT_INITIAL_DELAY} instead
038     */
039    @Deprecated
040    public static final int DEFAULT_INITIAL_DELAY = RetryRequestMiddleware.DEFAULT_INITIAL_DELAY;
041    /**
042     * @deprecated use {@link RetryRequestMiddleware#DEFAULT_RETRY_STATUS_CODES} instead
043     */
044    @Deprecated
045    public static final List<Integer> DEFAULT_RETRY_STATUS_CODES = RetryRequestMiddleware.DEFAULT_RETRY_STATUS_CODES;
046    private static final InternalLogger logger = InternalLogger.getLogger(loggerName);
047    private static final Logger classLogger = LoggerFactory.getLogger(RetryMiddleware.class);
048
049    private final FailsafeExecutor<ApiHttpResponse<byte[]>> failsafeExecutor;
050
051    /**
052     * @deprecated use {@link RetryRequestMiddleware#of(int)} instead
053     * @param maxRetries number of retries before giving up
054     */
055    @Deprecated
056    public RetryMiddleware(final int maxRetries) {
057        this(Scheduler.DEFAULT, maxRetries, RetryRequestMiddleware.DEFAULT_INITIAL_DELAY,
058            RetryRequestMiddleware.DEFAULT_MAX_DELAY, RetryRequestMiddleware.DEFAULT_RETRY_STATUS_CODES, null);
059    }
060
061    /**
062     * @deprecated use {@link RetryRequestMiddleware#of(int, List)} instead
063     * @param maxRetries number of retries before giving up
064     * @param statusCodes response status codes to be retried
065     */
066    @Deprecated
067    public RetryMiddleware(final int maxRetries, final List<Integer> statusCodes) {
068        this(Scheduler.DEFAULT, maxRetries, RetryRequestMiddleware.DEFAULT_INITIAL_DELAY,
069            RetryRequestMiddleware.DEFAULT_MAX_DELAY, statusCodes, null);
070    }
071
072    RetryMiddleware(final int maxRetries, final List<Integer> statusCodes,
073            final List<Class<? extends Throwable>> failures) {
074        this(Scheduler.DEFAULT, maxRetries, RetryRequestMiddleware.DEFAULT_INITIAL_DELAY,
075            RetryRequestMiddleware.DEFAULT_MAX_DELAY, statusCodes, failures);
076    }
077
078    /**
079     * @deprecated use {@link RetryRequestMiddleware#of(int, long, long)} instead
080     * @param maxRetries number of retries before giving up
081     * @param delay initial delay before retry
082     * @param maxDelay maximum delay before retry
083     */
084    @Deprecated
085    public RetryMiddleware(final int maxRetries, final long delay, final long maxDelay) {
086        this(Scheduler.DEFAULT, maxRetries, delay, maxDelay, RetryRequestMiddleware.DEFAULT_RETRY_STATUS_CODES, null);
087    }
088
089    /**
090     * @deprecated use {@link RetryRequestMiddleware#of(int, long, long, List)} instead
091     * @param maxRetries number of retries before giving up
092     * @param delay initial delay before retry
093     * @param maxDelay maximum delay before retry
094     * @param statusCodes response status codes to be retried
095     */
096    @Deprecated
097    public RetryMiddleware(final int maxRetries, final long delay, final long maxDelay,
098            final List<Integer> statusCodes) {
099        this(Scheduler.DEFAULT, maxRetries, delay, maxDelay, statusCodes, null);
100    }
101
102    RetryMiddleware(final int maxRetries, final long delay, final long maxDelay, final List<Integer> statusCodes,
103            final List<Class<? extends Throwable>> failures) {
104        this(Scheduler.DEFAULT, maxRetries, delay, maxDelay, RetryRequestMiddleware.handleFailures(failures)
105                .andThen(RetryRequestMiddleware.handleStatusCodes(statusCodes)));
106    }
107
108    RetryMiddleware(final ExecutorService executorService, final int maxRetries, final long delay, final long maxDelay,
109            final List<Integer> statusCodes, final List<Class<? extends Throwable>> failures) {
110        this(executorService, maxRetries, delay, maxDelay, RetryRequestMiddleware.handleFailures(failures)
111                .andThen(RetryRequestMiddleware.handleStatusCodes(statusCodes)));
112    }
113
114    RetryMiddleware(final ScheduledExecutorService executorService, final int maxRetries, final long delay,
115            final long maxDelay, final List<Integer> statusCodes, final List<Class<? extends Throwable>> failures) {
116        this(executorService, maxRetries, delay, maxDelay, RetryRequestMiddleware.handleFailures(failures)
117                .andThen(RetryRequestMiddleware.handleStatusCodes(statusCodes)));
118    }
119
120    RetryMiddleware(final Scheduler scheduler, final int maxRetries, final long delay, final long maxDelay,
121            final List<Integer> statusCodes, final List<Class<? extends Throwable>> failures) {
122        this(scheduler, maxRetries, delay, maxDelay, RetryRequestMiddleware.handleFailures(failures)
123                .andThen(RetryRequestMiddleware.handleStatusCodes(statusCodes)));
124    }
125
126    RetryMiddleware(final int maxRetries, final long delay, final long maxDelay,
127            final FailsafeRetryPolicyBuilderOptions fn) {
128        this(Scheduler.DEFAULT, maxRetries, delay, maxDelay, fn);
129    }
130
131    RetryMiddleware(final ExecutorService executorService, final int maxRetries, final long delay, final long maxDelay,
132            final FailsafeRetryPolicyBuilderOptions fn) {
133        this(Scheduler.of(executorService), maxRetries, delay, maxDelay, fn);
134    }
135
136    RetryMiddleware(final ScheduledExecutorService executorService, final int maxRetries, final long delay,
137            final long maxDelay, final FailsafeRetryPolicyBuilderOptions fn) {
138        this(Scheduler.of(executorService), maxRetries, delay, maxDelay, fn);
139    }
140
141    RetryMiddleware(final Scheduler scheduler, final int maxRetries, final long delay, final long maxDelay,
142            final FailsafeRetryPolicyBuilderOptions fn) {
143        RetryPolicy<ApiHttpResponse<byte[]>> retryPolicy = fn
144                .apply(RetryPolicy.<ApiHttpResponse<byte[]>> builder()
145                        .withBackoff(delay, maxDelay, ChronoUnit.MILLIS)
146                        .withJitter(0.25)
147                        .withMaxRetries(maxRetries)
148                        .onRetry(this::logEventFailure))
149                .build();
150        this.failsafeExecutor = Failsafe.with(retryPolicy).with(scheduler);
151    }
152
153    private void logEventFailure(ExecutionAttemptedEvent<ApiHttpResponse<byte[]>> event) {
154        final int attempt = event.getAttemptCount();
155
156        logger.info(() -> "Retry #" + attempt);
157        logger.trace(() -> {
158            final Throwable failure = event.getLastException();
159            if (failure instanceof ApiHttpException) {
160                final ApiHttpException httpException = (ApiHttpException) failure;
161                final ApiHttpRequest request = httpException.getRequest();
162                final ApiHttpResponse<byte[]> response = httpException.getResponse();
163                if (request != null) {
164                    return requestLog(attempt, request, response);
165                }
166            }
167            return event.toString();
168        });
169    }
170
171    private String requestLog(final int attempt, ApiHttpRequest request, ApiHttpResponse<?> response) {
172        String output;
173        final String httpMethodAndUrl = request.getMethod().name() + " " + request.getUrl().toString();
174        if (request.getBody() != null) {
175            final String unformattedBody = request.getSecuredBody();
176            final boolean isJsonRequest = request.getHeaders()
177                    .getHeaders(ApiHttpHeaders.CONTENT_TYPE)
178                    .stream()
179                    .findFirst()
180                    .map(ct -> ct.getValue().toLowerCase().contains("json"))
181                    .orElse(true);
182            if (isJsonRequest) {
183                String prettyPrint;
184                try {
185                    prettyPrint = JsonUtils.prettyPrint(unformattedBody);
186                }
187                catch (final JsonException e) {
188                    classLogger.warn("pretty print failed", e);
189                    prettyPrint = unformattedBody;
190                }
191                output = "Retry #" + attempt + ": " + request + "\n" + httpMethodAndUrl + "\nformatted: " + prettyPrint;
192            }
193            else {
194                output = "Retry #" + attempt + ": " + request + "\n" + request.getMethod().name() + " "
195                        + request.getUrl() + " " + unformattedBody;
196            }
197        }
198        else {
199            output = "Retry #" + attempt + ": " + request + "\n" + httpMethodAndUrl + " <no body>";
200        }
201        if (response != null) {
202            output += "\nFailure response: " + response.getStatusCode() + "\n" + response + "\n"
203                    + Optional.ofNullable(response.getBody())
204                            .map(body -> JsonUtils.prettyPrint(response.getBodyAsString().orElse("")))
205                            .orElse("<no body>");
206        }
207        return output;
208    }
209
210    /**
211     * @deprecated max parallel requests are limited by underlying HTTP client
212     * @param maxRetries number of retries before giving up
213     * @param maxParallelRequests maximum number of parallel retry requests
214     */
215    @Deprecated
216    public RetryMiddleware(final int maxParallelRequests, final int maxRetries) {
217        this(maxRetries, RetryRequestMiddleware.DEFAULT_INITIAL_DELAY, RetryRequestMiddleware.DEFAULT_MAX_DELAY,
218            RetryRequestMiddleware.DEFAULT_RETRY_STATUS_CODES, null);
219    }
220
221    /**
222     * @deprecated max parallel requests are limited by underlying HTTP client
223     * @param maxRetries number of retries before giving up
224     * @param maxParallelRequests maximum number of parallel retry requests
225     * @param statusCodes response status codes to be retried
226     */
227    @Deprecated
228    public RetryMiddleware(final int maxParallelRequests, final int maxRetries, final List<Integer> statusCodes) {
229        this(maxRetries, RetryRequestMiddleware.DEFAULT_INITIAL_DELAY, RetryRequestMiddleware.DEFAULT_MAX_DELAY,
230            statusCodes, null);
231    }
232
233    /**
234     * @deprecated max parallel requests are limited by underlying HTTP client
235     * @param maxRetries number of retries before giving up
236     * @param maxParallelRequests maximum number of parallel retry requests
237     * @param delay initial delay before retry
238     * @param maxDelay maximum delay before retry
239     */
240    @Deprecated
241    public RetryMiddleware(final int maxParallelRequests, final int maxRetries, final long delay, final long maxDelay) {
242        this(maxRetries, delay, maxDelay, RetryRequestMiddleware.DEFAULT_RETRY_STATUS_CODES, null);
243    }
244
245    /**
246     * @deprecated max parallel requests are limited by underlying HTTP client
247     * @param maxRetries number of retries before giving up
248     * @param maxParallelRequests maximum number of parallel retry requests
249     * @param delay initial delay before retry
250     * @param maxDelay maximum delay before retry
251     * @param statusCodes response status codes to be retried
252     */
253    @Deprecated
254    public RetryMiddleware(final int maxParallelRequests, final int maxRetries, final long delay, final long maxDelay,
255            List<Integer> statusCodes) {
256        this(maxRetries, delay, maxDelay, statusCodes, null);
257    }
258
259    @Override
260    public CompletableFuture<ApiHttpResponse<byte[]>> invoke(final ApiHttpRequest request,
261            final Function<ApiHttpRequest, CompletableFuture<ApiHttpResponse<byte[]>>> next) {
262        return failsafeExecutor.getStageAsync(() -> next.apply(request));
263    }
264
265    @Override
266    public void close() {
267    }
268}