Regenschein
Regenschein

Reputation: 1502

AsyncRestTemplate ListenableFuture<T> Callback + Timeout

I have configured an AsyncRestTemplate like this, here just an illustration to show that I am using an HttpComponentsAsyncClientHttpRequestFactory with connectTimeout and readTimeout being initialized with values - using Spring 4.0.8 RELEASE:

    <bean id="myAsynchRequestFactory" class="org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory">
        <property name="httpAsyncClient" ref="myCloseableHttpAsynchClient"></property>
        <property name="connectTimeout" value="444"></property>
        <property name="readTimeout" value="555"></property>
    </bean>

    <bean id="myAsynchRestTemplate" class="org.springframework.web.client.AsyncRestTemplate">
        <constructor-arg>
            <ref bean="myAsynchRequestFactory"/>
        </constructor-arg>
    </bean>

I also have configured a RequestConfig as the spring code suggests that the former method is deprecated, so I added it like this:

<bean id="myRequestConfigBuilder" class="org.apache.http.client.config.RequestConfig" factory-method="custom">
    <property name="connectionRequestTimeout" value="555"></property>
    <property name="connectTimeout" value="400"></property>
    <property name="socketTimeout" value="555"></property>
</bean>

<bean id="myRequestConfig" factory-bean="myRequestConfigBuilder" factory-method="build">
</bean>

<bean id="myHttpAsyncClientBuilder" class="org.apache.http.impl.nio.client.HttpAsyncClientBuilder">
    <property name="connectionManager" ref="myHttpAsyncConnectionManager"></property>
    <property name="defaultRequestConfig" ref="myRequestConfig"></property>
</bean>

<bean id="myHttpAsyncClientBuilder" class="org.apache.http.impl.nio.client.HttpAsyncClientBuilder">
    <property name="connectionManager" ref="myHttpAsyncConnectionManager"></property>
    <property name="defaultRequestConfig" ref="myRequestConfig"></property>
</bean>
<bean id="myCloseableHttpAsynchClient" factory-bean="myHttpAsyncClientBuilder" factory-method="build">
</bean>

Now I am using the AsyncRestTemplate together with its addCallback() method like this (as illustration):

  response = myAsynchRestTemplate.getForEntity( ... );
  response.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback<ResponseEntity<?>>() {

                    @Override
                    public void onSuccess(ResponseEntity<?> result) {
                        System.out.println("SUCCESS");
                    }

                    @Override
                    public void onFailure(Throwable ex) {
                        System.out.println("FAILURE")
                    }
                }

I would expect that onSuccess(...) is never called when the Service takes more than 555ms to reply. But that does not happen. Even if my service fakes a delay of let's say 5000ms, the onSuccess method will be called no matter what.

I have searched the web for possible solutions to add some kind of timeout to a callback, but didn't find anything appropriate. I tried to read cross the org.springframework.util.concurrent.* code in any versions since 4.0 but didn't yet find anything that would prevent the registered callback from execution.

I already have seen the following Stackoverflow Questions:

How to cancel AsyncRestTemplate HTTP request if they are taking too much time? This one suggests that the timeout should work

ListenableFuture, FutureCallback and timeouts This one is using some Google Guava example, but I am using org.springframework.util.concurrent.ListenableFuture

Does anybody have some solution for this problem?

UPDATE

I have found the root cause why this does not work...

The HttpComponentsAsyncClientHttpRequestFactory does not take the RequestConfig I have provided. Instead, it is always null and such beeing recreated as RequestConfig.DEFAULT. This happens here:

From HttpComponentesAsyncClientHttpRequestFactory:

@Override
public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpAsyncClient asyncClient = getHttpAsyncClient();
    startAsyncClient();
    HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
    postProcessHttpRequest(httpRequest);



    /** HERE: It tries to create a HttpContext **/
    HttpContext context = createHttpContext(httpMethod, uri);




    if (context == null) {
        context = HttpClientContext.create();
    }
    // Request configuration not set in the context
    if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
        // Use request configuration given by the user, when available
        RequestConfig config = null;
        if (httpRequest instanceof Configurable) {
            config = ((Configurable) httpRequest).getConfig();
        }
        if (config == null) {
            config = RequestConfig.DEFAULT;
        }
        context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
    }
    return new HttpComponentsAsyncClientHttpRequest(asyncClient, httpRequest, context);
}

But the method createHttpContext(httpMethod, uri) always returns null:

/**
 * Template methods that creates a {@link HttpContext} for the given HTTP method and URI.
 * <p>The default implementation returns {@code null}.
 * @param httpMethod the HTTP method
 * @param uri the URI
 * @return the http context
 */
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
    return null;
}

Thus, the HttpContext is recreated and gets a RequestConfig.DEFAULT attached.

Now compare this with the non-Asynch Version, HttpComponentsClientHttpRequestFactory. This one recreates a RequestConfig with Timeouts:

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    CloseableHttpClient client = (CloseableHttpClient) getHttpClient();
    Assert.state(client != null, "Synchronous execution requires an HttpClient to be set");
    HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
    postProcessHttpRequest(httpRequest);
    HttpContext context = createHttpContext(httpMethod, uri);
    if (context == null) {
        context = HttpClientContext.create();
    }
    // Request configuration not set in the context
    if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
        // Use request configuration given by the user, when available
        RequestConfig config = null;
        if (httpRequest instanceof Configurable) {
            config = ((Configurable) httpRequest).getConfig();
        }
        if (config == null) {




            /** LOOK HERE - THE SYNC WORLD HAS THIS WORKAROUND */
            if (this.socketTimeout > 0 || this.connectTimeout > 0) {
                config = RequestConfig.custom()
                        .setConnectTimeout(this.connectTimeout)
                        .setSocketTimeout(this.socketTimeout)
                        .build();
            }



            else {
                config = RequestConfig.DEFAULT;
            }
        }
        context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
    }
    if (this.bufferRequestBody) {
        return new HttpComponentsClientHttpRequest(client, httpRequest, context);
    }
    else {
        return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, context);
    }
}

When I am using a SimpleClientHttpRequestFactory instead the timeouts I configure are taken and rather than onSuccess(...), the onFailure(...) method is called with a SocketTimeoutException

This is totally puzzling - maybe anyone can give a hint why the HttpComponentsAsyncClientHttpRequestFactory is implemented as is?

Is there any example of the proper use of the RequestConfig Object?

Upvotes: 2

Views: 3530

Answers (1)

Regenschein
Regenschein

Reputation: 1502

This issue is discussed in

https://jira.spring.io/browse/SPR-12540

There is a solution ahead in Spring 4.3 (current Spring is 4.2), and the solution for the time beeing is to subclass HttpComponentsAsyncClientHttpRequestFactory and @Override the getHttpContext method.

Upvotes: 1

Related Questions