From 2cca12ea2bcf3cde64218c56eed2bf738dec0bb5 Mon Sep 17 00:00:00 2001 From: dominictootell Date: Sun, 1 Dec 2013 13:43:48 +0000 Subject: [PATCH] Consume Entity in AsynchronousValidationRequest or a connection is lost --- httpclient-cache/pom.xml | 6 + .../cache/AsynchronousValidationRequest.java | 8 +- ...stStaleWhileRevalidationReleasesConnection.java | 229 +++++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestStaleWhileRevalidationReleasesConnection.java diff --git a/httpclient-cache/pom.xml b/httpclient-cache/pom.xml index 199e8dc..0e352bc 100644 --- a/httpclient-cache/pom.xml +++ b/httpclient-cache/pom.xml @@ -84,6 +84,12 @@ easymockclassextension test + + com.xebialabs.restito + restito + 0.4-beta-1 + test + diff --git a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/AsynchronousValidationRequest.java b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/AsynchronousValidationRequest.java index 93b7a1d..e55f91d 100644 --- a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/AsynchronousValidationRequest.java +++ b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/AsynchronousValidationRequest.java @@ -39,6 +39,7 @@ import org.apache.http.client.methods.HttpExecutionAware; import org.apache.http.client.methods.HttpRequestWrapper; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.util.EntityUtils; /** * Class used to represent an asynchronous revalidation event, such as with @@ -107,8 +108,9 @@ class AsynchronousValidationRequest implements Runnable { * otherwise false */ protected boolean revalidateCacheEntry() { + HttpResponse httpResponse = null; try { - final HttpResponse httpResponse = cachingExec.revalidateCacheEntry(route, request, context, execAware, cacheEntry); + httpResponse = cachingExec.revalidateCacheEntry(route, request, context, execAware, cacheEntry); final int statusCode = httpResponse.getStatusLine().getStatusCode(); return isNotServerError(statusCode) && isNotStale(httpResponse); } catch (final IOException ioe) { @@ -120,6 +122,10 @@ class AsynchronousValidationRequest implements Runnable { } catch (final RuntimeException re) { log.error("RuntimeException thrown during asynchronous revalidation: " + re); return false; + } finally { + if(httpResponse!=null) { + EntityUtils.consumeQuietly(httpResponse.getEntity()); + } } } diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestStaleWhileRevalidationReleasesConnection.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestStaleWhileRevalidationReleasesConnection.java new file mode 100644 index 0000000..8de4166 --- /dev/null +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestStaleWhileRevalidationReleasesConnection.java @@ -0,0 +1,229 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.http.impl.client.cache; + +import com.xebialabs.restito.server.StubServer; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.cache.CacheResponseStatus; +import org.apache.http.client.cache.HttpCacheContext; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.glassfish.grizzly.http.util.HttpStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.ServerSocket; + +import static com.xebialabs.restito.builder.stub.StubHttp.whenHttp; +import static com.xebialabs.restito.semantics.Action.header; +import static com.xebialabs.restito.semantics.Action.status; +import static com.xebialabs.restito.semantics.Action.stringContent; +import static com.xebialabs.restito.semantics.Condition.get; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Test that after background validation that a subsequent request for non cached + * conent can be made. This verifies that the connection has been release back to + * the pool by the AsynchronousValidationRequest. + */ +public class TestStaleWhileRevalidationReleasesConnection { + + private StubServer server; + private int port; + private CloseableHttpClient client; + private final String url = "/static/dom"; + private final String url2 = "2"; + + @Before + public void start() { + + + server = new StubServer(findFreePort()).run(); + port = server.getPort(); + + final CacheConfig cacheConfig = CacheConfig.custom() + .setMaxCacheEntries(100) + .setMaxObjectSize(15) //1574 + .setAsynchronousWorkerIdleLifetimeSecs(60) + .setAsynchronousWorkersMax(1) + .setAsynchronousWorkersCore(1) + .setRevalidationQueueSize(100) + .setSharedCache(true) + .build(); + + final HttpClientBuilder clientBuilder = CachingHttpClientBuilder.create().setCacheConfig(cacheConfig); + clientBuilder.setMaxConnTotal(1); + clientBuilder.setMaxConnPerRoute(1); + + final RequestConfig config = RequestConfig.custom() + .setSocketTimeout(1000) + .setConnectTimeout(1000) + .setConnectionRequestTimeout(1000) + .build(); + + clientBuilder.setDefaultRequestConfig(config); + + + client = clientBuilder.build(); + } + + @After + public void stop() { + server.stop(); + try { + client.close(); + } catch(IOException e) { + e.printStackTrace(); + } + } + + public static int findFreePort() { + try { + final ServerSocket server = new ServerSocket(0); + final int port = server.getLocalPort(); + server.close(); + return port; + } catch (IOException e) { + return 6789; + } + } + + @Test + public void testStaleWhileRevalidate() { + // Restito + whenHttp(server). + match(get(url)). + then(status(HttpStatus.OK_200), header("Cache-Control", "public, max-age=3, stale-while-revalidate=5"), + stringContent("sdgsdgffgfgdom")); + + whenHttp(server). + match(get(url + url2)). + then(status(HttpStatus.OK_200), header("Cache-Control", "public, max-age=3, stale-while-revalidate=5"), + stringContent("sdgsdgffgfgdom")); + + final HttpContext localContext = new BasicHttpContext(); + Exception requestException = null; + + // This will fetch from backend. + requestException = sendRequest(client, localContext,port); + assertNull(requestException); + + CacheResponseStatus responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS); + assertEquals(CacheResponseStatus.CACHE_MISS,responseStatus); + + try { + Thread.sleep(1000); + } catch (Exception e) { + + } + // These will be cached + requestException = sendRequest(client, localContext,port); + assertNull(requestException); + + responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS); + assertEquals(CacheResponseStatus.CACHE_HIT,responseStatus); + + requestException = sendRequest(client, localContext,port); + assertNull(requestException); + + responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS); + assertEquals(CacheResponseStatus.CACHE_HIT,responseStatus); + + // wait, so that max-age is expired + try { + Thread.sleep(4000); + } catch (Exception e) { + + } + + whenHttp(server). + match(get(url)). + then(status(HttpStatus.OK_200), header("Cache-Control", "public, max-age=3, stale-while-revalidate=5"), + stringContent("This is new content that is bigger than cache limit")); + + // This will cause a revalidation to occur + requestException = sendRequest(client, localContext,port); + assertNull(requestException); + + responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS); + assertEquals(CacheResponseStatus.CACHE_HIT,responseStatus); + + try { + Thread.sleep(1000); + } catch (Exception e) { + + } + + // fetch a different content This will hang due to connection leak in revalidation + requestException = sendRequest(client, localContext,port, url2); + if(requestException!=null) { + requestException.printStackTrace(); + } + assertNull(requestException); + + + } + + static Exception sendRequest(final HttpClient cachingClient, final HttpContext localContext, final int port) { + return sendRequest(cachingClient, localContext,port,""); + } + + static Exception sendRequest(final HttpClient cachingClient, final HttpContext localContext , final int port, final String extra) { + final HttpGet httpget = new HttpGet("http://localhost:"+port+"/static/dom"+extra); + HttpResponse response = null; + try { + response = cachingClient.execute(httpget, localContext); + return null; + } catch (ClientProtocolException e1) { + return e1; + } catch (IOException e1) { + return e1; + } finally { + if(response!=null) { + final HttpEntity entity = response.getEntity(); + try { + EntityUtils.consume(entity); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + } + + } \ No newline at end of file -- 1.7.12.4 (Apple Git-37)