PoolingHttpClientConnectionManager не освобождает соединения

Я использую Spring для достижения следующего:

На сервере я получаю данные через интерфейс REST в формате XML. Я хочу преобразовать данные в JSON и POST на другой сервер. Мой код (я удалил некоторые классные имена/URL-адреса, чтобы избежать гнева моего работодателя) выглядит следующим образом:

Основной/класс конфигурации:

package stateservice;

import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class App {
    Logger log = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) {
        System.out.println("Start!");
        SpringApplication.run(StateServiceApplication.class, args);
        System.out.println("End!");
    }

    @Bean
    public RestTemplate restTemplate() {
        log.trace("restTemplate()");
        HttpHost proxy = new HttpHost("proxy_url", 8080);
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        // Increase max total connection to 200
        cm.setMaxTotal(200);
        cm.setDefaultMaxPerRoute(50);

        RequestConfig requestConfig = RequestConfig.custom().setProxy(proxy).build();

        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setDefaultRequestConfig(requestConfig);
        httpClientBuilder.setConnectionManager(cm);
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
                httpClientBuilder.build());
        return new RestTemplate(requestFactory);
    }
}

Класс, представляющий интерфейс RESTful:

package stateservice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import foo.bar.XmlData

@RestController
public class StateController {

    private static Logger log = LoggerFactory.getLogger(DataController.class);

    @Autowired
    ForwarderService forwarder;


    @RequestMapping(value = "/data", method = RequestMethod.POST)
    public String postState(@RequestBody XmlData data) {
        forwarder.forward(data);
        return "Done!";
    }
}

Наконец, Экспедитор:

package stateservice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import foo.bar.Converter;
import foo.bar.XmlData;

@Service
public class ForwarderService {
    private static Logger log = LoggerFactory.getLogger(ForwarderService.class);

    String uri = "forward_uri";

    @Autowired
    RestTemplate restTemplate;

    @Async
    public String forward(XmlData data) {
        log.trace("forward(...) - start");
        String json = Converter.convert(data);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<String> response = restTemplate.postForEntity(uri,
                new HttpEntity<String>(json, headers), String.class);
        // responseEntity.getBody();
        // log.trace(responseEntity.toString());
        log.trace("forward(...) - end");
        return response.getBody();
    }
}

Однако диспетчер подключений редко кажется, что он освобождает соединения для повторного использования, и, кроме того, система заливается соединениями в состоянии CLOSE_WAIT (что можно увидеть с помощью netstat). Все соединения в пуле становятся арендованными, но не выпущены, и как только количество подключений в состоянии CLOSE_WAIT достигает ulimit, я получаю "Слишком много открытых файлов" - исключения

Из-за многопоточного характера кода я подозреваю, что сокеты не могут быть закрыты/соединения освобождены, потому что какой-то другой поток как-то блокирует их.

Я бы очень признателен за любую помощь или любой намек, который вы можете дать мне, чтобы решить проблему.

Ответ 1

Существует трюк с Apache HttpEntity - для освобождения заблокированного соединения - ответ должен быть полностью потреблен и закрыт. Подробнее см. EntityUtils и HttpEntity docs:

EntityUtils.consume(response);

Начиная с версии 4.3 Apache HttpClient отправляет соединение обратно в пул, когда метод #close() вызывается в CloseableHttpResponse.

Однако эта функция поддерживается Spring Web только с версии 4.0.0-RELEASE, см. метод #close() в HttpComponentsClientHttpResponse.java:

@Override
public void close() {
    // Release underlying connection back to the connection manager
    try {
        try {
            // Attempt to keep connection alive by consuming its remaining content
            EntityUtils.consume(this.httpResponse.getEntity());
        } finally {
            // Paranoia
            this.httpResponse.close();
        }
    }
    catch (IOException ignore) {
    }
}

Ключом к успеху является строка, обозначенная символом "//Паранойя" - явным вызовом .close(). Он фактически освобождает подключение к пулу.