// Copyright 2016 Yahoo Inc. // Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms. package com.yahoo.parsec.clients; import com.ning.http.client.AsyncHandler; import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.Request; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * {@link Callable} implementation that handles HTTP request retry based on response status code. */ class ParsecHttpRequestRetryCallable<T> implements Callable<T> { /** * By default, do not delay the retries. */ private static final long DEFAULT_RETRY_INTERVAL = 0; /** * Logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(ParsecHttpRequestRetryCallable.class); /** * Async handler. */ private final AsyncHandler<T> asyncHandler; /** * Ning client. */ private final AsyncHttpClient client; /** * Request. */ private final ParsecAsyncHttpRequest request; /** * For delayed retries */ private RetryDelayer retryDelayer; /** * Response list. */ private List<T> responses; /** * Constructor. * * @param client client * @param request request */ public ParsecHttpRequestRetryCallable(final AsyncHttpClient client, final ParsecAsyncHttpRequest request) { this(client, request, null); } /** * Constructor. * * @param client client * @param request request * @param asyncHandler async handler */ public ParsecHttpRequestRetryCallable( final AsyncHttpClient client, final ParsecAsyncHttpRequest request, final AsyncHandler<T> asyncHandler) { this(client, request, asyncHandler, DEFAULT_RETRY_INTERVAL); } /** * Constructor. * * @param client client * @param request request * @param asyncHandler async handler * @param retryIntervalMillis retry interval in milliseconds */ public ParsecHttpRequestRetryCallable( final AsyncHttpClient client, final ParsecAsyncHttpRequest request, final AsyncHandler<T> asyncHandler, long retryIntervalMillis ) { this.client = client; this.request = request; this.asyncHandler = asyncHandler; this.retryDelayer = new RetryDelayer(retryIntervalMillis); responses = new ArrayList<>(); } /** * Package-private Constructor. Only for unit-test usage. * * @param client client * @param request request * @param asyncHandler async handler * @param retryDelayer custom retry Delayer */ ParsecHttpRequestRetryCallable( final AsyncHttpClient client, final ParsecAsyncHttpRequest request, final AsyncHandler<T> asyncHandler, RetryDelayer retryDelayer ) { this.client = client; this.request = request; this.asyncHandler = asyncHandler; this.retryDelayer = retryDelayer; responses = new ArrayList<>(); } /** * Check status code and handles retry if T is type {@link Response} or Ning {@link com.ning.http.client.Response}. * * @return T * @throws Exception exception */ @Override public T call() throws Exception { responses.clear(); final Request ningRequest = request.getNingRequest(); final List<Integer> retryStatusCodes = request.getRetryStatusCodes(); final List<Class<? extends Throwable>> retryExceptions = request.getRetryExceptions(); final int maxRetries = request.getMaxRetries(); T response; Exception exception; int retries = 0; for (; ; ) { response = null; exception = null; try { // when this is not the first try (i.e. is re-trying), delay an interval if (retries > 0) { retryDelayer.delay(); } response = executeRequest(ningRequest); responses.add(response); int statusCode = getStatusCode(response); if (statusCode == -1 || !retryStatusCodes.contains(statusCode)) { break; } } catch (Exception e) { Throwable root = ExceptionUtils.getRootCause(e); if (!retryExceptions.contains(root.getClass())) { throw e; } exception = e; } if (retries == maxRetries) { LOGGER.debug("Max retries reached: " + retries + " (max: " + maxRetries + ")"); break; } retries++; LOGGER.debug("Retry number: " + retries + " (max: " + maxRetries + ")"); } if (exception != null) { throw exception; } return response; } /** * Execute Request. * * @param ningRequest Ning request * @return T * @throws InterruptedException Interrupted exception * @throws ExecutionException Execution exception */ @SuppressWarnings("unchecked") private T executeRequest(Request ningRequest) throws InterruptedException, ExecutionException { if (asyncHandler != null) { return client.executeRequest(ningRequest, asyncHandler).get(); } else { return (T) client.executeRequest(ningRequest).get(); } } /** * Get Responses. * * @return {@literal List<T>} */ public List<T> getResponses() { return responses; } /** * Gets Status Code from {@link Response} or Ning {@link com.ning.http.client.Response}. * * @param response T * @return status code or -1 if T is not type of {@link Response} or Ning {@link com.ning.http.client.Response} */ private int getStatusCode(T response) { if (response instanceof Response) { return ((Response) response).getStatus(); } else if (response instanceof com.ning.http.client.Response) { return ((com.ning.http.client.Response) response).getStatusCode(); } else { return -1; } } /** * Package-private, this is only for unit-testing. * @return retryDelayer */ RetryDelayer getRetryDelayer() { return retryDelayer; } /** * Extract delay mechanism as a class to let it be testable */ public class RetryDelayer { private final long retryIntervalMillis; RetryDelayer(long retryIntervalMillis) { this.retryIntervalMillis = retryIntervalMillis; } public void delay() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(retryIntervalMillis); } /** * Package-private, this is only for unit-testing. * @return retryIntervalMillis */ long getRetryIntervalMillis() { return retryIntervalMillis; } } }