Как отменить HTTP-запрос AsyncRestTemplate, если они занимают слишком много времени?

С самого начала я всегда путался с тем, как бороться с InterruptedException и как правильно отменить HTTP-запрос, если они занимают слишком много времени. У меня есть библиотека, в которой я предоставил два метода, синхронизацию и асинхронность для нашего клиента. Они могут называть тот метод, который, по их мнению, подходит для их целей.

  • executeSync() - ждет, пока у меня не будет результата, вернет результат.
  • executeAsync() - немедленно возвращает Будущее, которое может быть обработано после выполнения других действий, если это необходимо.

Они передадут объект DataKey, в котором есть идентификатор пользователя и время ожидания. Мы выясним, какую машину вызывать на основе идентификатора пользователя, а затем создать URL-адрес с этой машиной, и мы сделаем http-обращение к URL-адресу с помощью AsyncRestTemplate, а затем отправьте ответ им на вопрос, успешно ли это или нет.

Я использую exchange метод AsyncRestTemplate, который возвращает обратно ListenableFuture, и я хотел иметь асинхронную неблокирующую архитектуру с подключением к NIO-подключению, поэтому этот запрос использует неблокирующий IO, поэтому я пошел с AsyncRestTemplate. Подходит ли этот подход к определению моей проблемы? Эта библиотека будет использоваться в производстве с очень большой нагрузкой.

Ниже мой интерфейс:

public interface Client {
    // for synchronous
    public DataResponse executeSync(DataKey key);

    // for asynchronous
    public ListenableFuture<DataResponse> executeAsync(DataKey key);
}

И ниже моя реализация интерфейса:

public class DataClient implements Client {

    // using spring 4 AsyncRestTemplate
    private final AsyncRestTemplate restTemplate = new AsyncRestTemplate();

    // for synchronous
    @Override
    public DataResponse executeSync(DataKey keys) {
        Future<DataResponse> responseFuture = executeAsync(keys);
        DataResponse response = null;

        try {
            response = responseFuture.get(keys.getTimeout(), TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {
            // do we need to catch InterruptedException here and interrupt the thread?
            Thread.currentThread().interrupt();
            // also do I need throw this RuntimeException at all?
            throw new RuntimeException("Interrupted", ex);
        } catch (TimeoutException ex) {
            DataLogging.logEvents(ex, DataErrorEnum.CLIENT_TIMEOUT, keys);
            response = new DataResponse(null, DataErrorEnum.CLIENT_TIMEOUT, DataStatusEnum.ERROR);
            responseFuture.cancel(true); // terminating the tasks that got timed out so that they don't take up the resources?
        } catch (Exception ex) {
            DataLogging.logEvents(ex, DataErrorEnum.ERROR_CLIENT, keys);
            response = new DataResponse(null, DataErrorEnum.ERROR_CLIENT, DataStatusEnum.ERROR);
        }

        return response;
    }

    // for asynchronous     
    @Override
    public ListenableFuture<DataResponse> executeAsync(final DataKey keys) {

        final SettableFuture<DataResponse> responseFuture = SettableFuture.create();
        final org.springframework.util.concurrent.ListenableFuture orig = 
            restTemplate.exchange(createURL(keys), HttpMethod.GET, keys.getEntity(), String.class);

        orig.addCallback(
                new ListenableFutureCallback<ResponseEntity<String>>() {
                    @Override
                    public void onSuccess(ResponseEntity<String> result) {
                        responseFuture.set(new DataResponse(result.getBody(), DataErrorEnum.OK,
                                DataStatusEnum.SUCCESS));
                    }

                    @Override
                    public void onFailure(Throwable ex) {
                        DataLogging.logErrors(ex, DataErrorEnum.ERROR_SERVER, keys);
                        responseFuture.set(new DataResponse(null, DataErrorEnum.ERROR_SERVER,
                                DataStatusEnum.ERROR));
                    }
                });

        // propagate cancellation back to the original request
        responseFuture.addListener(new Runnable() {
          @Override public void run() {
             if (responseFuture.isCancelled()) {
               orig.cancel(false); // I am keeping this false for now
             }
          }
        }, MoreExecutors.directExecutor());
        return responseFuture;
    }
}

И клиент будет так звонить по своему коду -

// if they are calling executeSync() method
DataResponse response = DataClientFactory.getInstance().executeSync(dataKey);

// and if they want to call executeAsync() method
Future<DataResponse> response = DataClientFactory.getInstance().executeAsync(dataKey);

Теперь вопрос -

  • Можем ли мы прерывать вызов AsyncRestTemplate, если HTTP-запрос занимает слишком много времени? Я на самом деле вызываю cancel на моем future в моем выше коде в методе executeSync, но я не уверен, как проверить его, чтобы убедиться, что он делает то, что должен? Я хочу отменить отмену в исходное будущее, чтобы отменить соответствующий HTTP-запрос (который я, вероятно, хочу сделать для сохранения ресурсов), поэтому я добавил слушателя в свой метод executeAsync. Я считаю, мы не можем прерывать вызовы RestTemplate, но не уверены в AsyncRestTemplate, можем ли мы это сделать или нет. Если допустим, что мы можем прерывать вызовы AsyncRestTemplate, тогда я делаю все правильно, чтобы прервать http-вызовы? Или есть лучший/более чистый способ сделать это? Или мне даже нужно беспокоиться об отмене запроса Http с помощью AsyncRestTemplate с моим текущим дизайном?

        // propagate cancellation back to the original request
        responseFuture.addListener(new Runnable() {
          @Override public void run() {
             if (responseFuture.isCancelled()) {
               orig.cancel(false); // I am keeping this false for now
             }
          }
        }, MoreExecutors.directExecutor()); 
    

    С текущей настройкой, я вижу, что это иногда исключает CancellationException (не каждый раз). Означает ли это, что мой HTTP-запрос был отменен?

  • Также я делаю правильную вещь в блоке catch InterruptedException в executeSync методе? Если нет, то какой правильный способ справиться с этим. И мне вообще нужно иметь дело с InterruptedException в моем случае?
  • Правда ли, что по умолчанию AsyncRestTamplete использует блокирующие вызовы и запросы на поток? Если да, то есть ли способ подключения клиентских подключений на основе NIO в моей текущей настройке?

Любые объяснения/предложения кода будут очень полезны.

Ответ 1

Прежде всего, почему вы используете SettableFuture? Почему нельзя просто вернуть ListenableFuture, возвращенный AsyncRestTemplate?

1. Can we interrupt AsyncRestTemplate call if http request is taking too long?

Конечно, да! Вам нужно только вызвать метод Future.cancel. Этот метод прервет выполнение внутреннего RestTemplate, который фактически использует AsyncRestTemplate.

2. Also am I doing the right thing in catch block of InterruptedException in executeSync method?

Как сказал Фил и Данило, вам не нужно прерывать текущий поток в блоке catch InterruptedException. Просто выполняйте все, что вам нужно, когда выполнение запроса должно быть отменено.

На самом деле, я рекомендую вам создать метод, который обрабатывает это поведение, например handleInterruption, и использовать этот метод для TimeoutException и InterruptedException.

3. Is it true that by default AsyncRestTamplete uses blocking calls and request per thread?

Да. Конструктор по умолчанию AsyncRestTamplete внутренне использует SimpleClientHttpRequestFactory и SimpleAsyncTaskExecutor.

Этот TaskExecutor всегда запускает угрозу для каждой задачи и никогда не повторно использует Threads, поэтому он очень неэффективен:

 * TaskExecutor implementation that fires up a new Thread for each task,
 * executing it asynchronously.
 *
 * Supports limiting concurrent threads through the "concurrencyLimit"
 * bean property. By default, the number of concurrent threads is unlimited.
 *
 * NOTE: This implementation does not reuse threads! Consider a
 * thread-pooling TaskExecutor implementation instead, in particular for
 * executing a large number of short-lived tasks.
 *

Я рекомендую вам использовать другую конфигурацию AsyncRestTemplate.

Вы должны использовать конструктор AsyncRestTemplate, который использует другой TaskExecutor:

public AsyncRestTemplate(AsyncListenableTaskExecutor taskExecutor)

Например:

AsyncRestTemplate template = new AsyncRestTemplate(new ConcurrentTaskExecutor(Executors.newCachedThreadPool()));

Этот ExecutorService (Executors.newCachedThreadPool()) создает новые потоки по мере необходимости, но будет использовать ранее созданные потоки, когда они будут доступны.

Или даже лучше, вы можете использовать другой RequestFactory. Например, вы можете использовать HttpComponentsAsyncClientHttpRequestFactory, который внутренне использует NIO, просто вызывая соответствующий конструктор AsyncRestTemplate:

new AsyncRestTemplate(new HttpComponentsAsyncClientHttpRequestFactory())

Не забывайте, что внутреннее поведение AsyncRestTemplate будет зависеть от того, как вы создаете объект.