View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.http.impl.client.cache;
28  
29  import static org.junit.Assert.assertEquals;
30  import static org.junit.Assert.assertNull;
31  
32  import java.io.IOException;
33  import java.util.Locale;
34  
35  import org.apache.http.Consts;
36  import org.apache.http.Header;
37  import org.apache.http.HttpEntity;
38  import org.apache.http.HttpException;
39  import org.apache.http.HttpRequest;
40  import org.apache.http.HttpResponse;
41  import org.apache.http.MethodNotSupportedException;
42  import org.apache.http.client.ClientProtocolException;
43  import org.apache.http.client.HttpClient;
44  import org.apache.http.client.cache.CacheResponseStatus;
45  import org.apache.http.client.cache.HttpCacheContext;
46  import org.apache.http.client.config.RequestConfig;
47  import org.apache.http.client.methods.HttpGet;
48  import org.apache.http.config.SocketConfig;
49  import org.apache.http.entity.ByteArrayEntity;
50  import org.apache.http.impl.bootstrap.HttpServer;
51  import org.apache.http.impl.bootstrap.ServerBootstrap;
52  import org.apache.http.impl.client.CloseableHttpClient;
53  import org.apache.http.impl.client.HttpClientBuilder;
54  import org.apache.http.protocol.BasicHttpContext;
55  import org.apache.http.protocol.HttpContext;
56  import org.apache.http.protocol.HttpRequestHandler;
57  import org.junit.After;
58  import org.junit.Before;
59  import org.junit.Test;
60  
61  /**
62   * Test that after background validation that a subsequent request for non cached
63   * conent can be made.  This verifies that the connection has been release back to
64   * the pool by the AsynchronousValidationRequest.
65   */
66  public class TestStaleWhileRevalidationReleasesConnection {
67  
68      private static final EchoViaHeaderHandler cacheHandler = new EchoViaHeaderHandler();
69  
70      protected HttpServer localServer;
71      private int port;
72      private CloseableHttpClient client;
73      private final String url = "/static/dom";
74      private final String url2 = "2";
75  
76  
77      @Before
78      public void start() throws Exception  {
79          this.localServer = ServerBootstrap.bootstrap()
80                  .setSocketConfig(SocketConfig.custom()
81                          .setSoTimeout(5000)
82                          .build())
83                  .registerHandler(url + "*", new EchoViaHeaderHandler())
84                  .create();
85          this.localServer.start();
86  
87          port = this.localServer.getLocalPort();
88  
89          final CacheConfig cacheConfig = CacheConfig.custom()
90                  .setMaxCacheEntries(100)
91                  .setMaxObjectSize(15) //1574
92                  .setAsynchronousWorkerIdleLifetimeSecs(60)
93                  .setAsynchronousWorkersMax(1)
94                  .setAsynchronousWorkersCore(1)
95                  .setRevalidationQueueSize(100)
96                  .setSharedCache(true)
97                  .build();
98  
99          final HttpClientBuilder clientBuilder = CachingHttpClientBuilder.create().setCacheConfig(cacheConfig);
100         clientBuilder.setMaxConnTotal(1);
101         clientBuilder.setMaxConnPerRoute(1);
102 
103         final RequestConfig config = RequestConfig.custom()
104                 .setSocketTimeout(10000)
105                 .setConnectTimeout(10000)
106                 .setConnectionRequestTimeout(1000)
107                 .build();
108 
109         clientBuilder.setDefaultRequestConfig(config);
110 
111 
112         client = clientBuilder.build();
113     }
114 
115     @After
116     public void stop() {
117         if (this.localServer != null) {
118             try {
119                 this.localServer.stop();
120             } catch(final Exception e) {
121                 e.printStackTrace();
122             }
123         }
124 
125         try {
126             client.close();
127         } catch(final IOException e) {
128             e.printStackTrace();
129         }
130     }
131 
132     @Test
133     public void testStaleWhileRevalidate() {
134         final String urlToCall = "http://localhost:"+port + url;
135         final HttpContext localContext = new BasicHttpContext();
136         Exception requestException = null;
137 
138         // This will fetch from backend.
139         requestException = sendRequest(client, localContext,urlToCall,null);
140         assertNull(requestException);
141 
142         CacheResponseStatus responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS);
143         assertEquals(CacheResponseStatus.CACHE_MISS,responseStatus);
144 
145         try {
146             Thread.sleep(1000);
147         } catch (final Exception e) {
148 
149         }
150         // These will be cached
151         requestException = sendRequest(client, localContext,urlToCall,null);
152         assertNull(requestException);
153 
154         responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS);
155         assertEquals(CacheResponseStatus.CACHE_HIT,responseStatus);
156 
157         requestException = sendRequest(client, localContext,urlToCall,null);
158         assertNull(requestException);
159 
160         responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS);
161         assertEquals(CacheResponseStatus.CACHE_HIT,responseStatus);
162 
163         // wait, so that max-age is expired
164         try {
165             Thread.sleep(4000);
166         } catch (final Exception e) {
167 
168         }
169 
170         // This will cause a revalidation to occur
171         requestException = sendRequest(client, localContext,urlToCall,"This is new content that is bigger than cache limit");
172         assertNull(requestException);
173 
174         responseStatus = (CacheResponseStatus) localContext.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS);
175         assertEquals(CacheResponseStatus.CACHE_HIT,responseStatus);
176 
177         try {
178             Thread.sleep(1000);
179         } catch (final Exception e) {
180 
181         }
182 
183         // fetch a different content This will hang due to connection leak in revalidation
184         requestException = sendRequest(client, localContext,urlToCall+url2,null);
185         if(requestException!=null) {
186             requestException.printStackTrace();
187         }
188         assertNull(requestException);
189 
190 
191     }
192 
193     static Exception sendRequest(final HttpClient cachingClient, final HttpContext localContext , final String url, final String content) {
194         final HttpGet httpget = new HttpGet(url);
195         if(content!=null) {
196             httpget.setHeader(cacheHandler.getUserContentHeader(),content);
197         }
198 
199         HttpResponse response = null;
200         try {
201             response = cachingClient.execute(httpget, localContext);
202             return null;
203         } catch (final ClientProtocolException e1) {
204             return e1;
205         } catch (final IOException e1) {
206             return e1;
207         } finally {
208             if(response!=null) {
209                 final HttpEntity entity = response.getEntity();
210                 try {
211                     IOUtils.consume(entity);
212                 } catch (final IOException e) {
213                     e.printStackTrace();
214                 }
215             }
216         }
217     }
218 
219     public static class EchoViaHeaderHandler
220             implements HttpRequestHandler {
221 
222         private final String CACHE_CONTROL_HEADER = "Cache-Control";
223 
224         private final byte[] DEFAULT_CONTENT;
225         private final String DEFAULT_CLIENT_CONTROLLED_CONTENT_HEADER;
226         private final String DEFAULT_RESPONSE_CACHE_HEADER;
227 
228         // public default constructor
229         public EchoViaHeaderHandler() {
230             this("ECHO-CONTENT","abc".getBytes(), "public, max-age=3, stale-while-revalidate=5");
231         }
232 
233         public EchoViaHeaderHandler(final String contentHeader,final byte[] content,
234                                     final String defaultResponseCacheHeader) {
235             DEFAULT_CLIENT_CONTROLLED_CONTENT_HEADER = contentHeader;
236             DEFAULT_CONTENT = content;
237             DEFAULT_RESPONSE_CACHE_HEADER = defaultResponseCacheHeader;
238         }
239 
240 
241         /**
242          * Return the header the user can set the content that will be returned by the server
243          */
244         public String getUserContentHeader() {
245             return DEFAULT_CLIENT_CONTROLLED_CONTENT_HEADER;
246         }
247 
248         /**
249          * Handles a request by echoing the incoming request entity.
250          * If there is no request entity, an empty document is returned.
251          *
252          * @param request   the request
253          * @param response  the response
254          * @param context   the context
255          *
256          * @throws org.apache.http.HttpException    in case of a problem
257          * @throws java.io.IOException      in case of an IO problem
258          */
259         @Override
260         public void handle(final HttpRequest request,
261                            final HttpResponse response,
262                            final HttpContext context)
263                 throws HttpException, IOException {
264 
265             final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
266             if (!"GET".equals(method) &&
267                     !"POST".equals(method) &&
268                     !"PUT".equals(method)
269                     ) {
270                 throw new MethodNotSupportedException
271                         (method + " not supported by " + getClass().getName());
272             }
273 
274             response.setStatusCode(org.apache.http.HttpStatus.SC_OK);
275             response.addHeader("Cache-Control",getCacheContent(request));
276             final byte[] content = getHeaderContent(request);
277             final ByteArrayEntity bae = new ByteArrayEntity(content);
278             response.setHeader("Connection","keep-alive");
279 
280             response.setEntity(bae);
281 
282         } // handle
283 
284 
285         public byte[] getHeaderContent(final HttpRequest request) {
286             final Header contentHeader = request.getFirstHeader(DEFAULT_CLIENT_CONTROLLED_CONTENT_HEADER);
287             if(contentHeader!=null) {
288                 return contentHeader.getValue().getBytes(Consts.UTF_8);
289             } else {
290                 return DEFAULT_CONTENT;
291             }
292         }
293 
294         public String getCacheContent(final HttpRequest request) {
295             final Header contentHeader = request.getFirstHeader(CACHE_CONTROL_HEADER);
296             if(contentHeader!=null) {
297                 return contentHeader.getValue();
298             } else {
299                 return DEFAULT_RESPONSE_CACHE_HEADER;
300             }
301         }
302 
303     } // class EchoHandler
304 
305   }
306