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.hc.client5.testing.sync;
28  
29  import java.io.IOException;
30  
31  import org.apache.hc.client5.http.HttpRoute;
32  import org.apache.hc.client5.http.UserTokenHandler;
33  import org.apache.hc.client5.http.classic.methods.HttpGet;
34  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
35  import org.apache.hc.client5.http.protocol.HttpClientContext;
36  import org.apache.hc.core5.http.ClassicHttpRequest;
37  import org.apache.hc.core5.http.ClassicHttpResponse;
38  import org.apache.hc.core5.http.EndpointDetails;
39  import org.apache.hc.core5.http.HttpException;
40  import org.apache.hc.core5.http.HttpHost;
41  import org.apache.hc.core5.http.HttpStatus;
42  import org.apache.hc.core5.http.io.HttpRequestHandler;
43  import org.apache.hc.core5.http.io.entity.EntityUtils;
44  import org.apache.hc.core5.http.io.entity.StringEntity;
45  import org.apache.hc.core5.http.protocol.BasicHttpContext;
46  import org.apache.hc.core5.http.protocol.HttpContext;
47  import org.junit.Assert;
48  import org.junit.Test;
49  
50  /**
51   * Test cases for state-ful connections.
52   */
53  public class TestStatefulConnManagement extends LocalServerTestBase {
54  
55      private static class SimpleService implements HttpRequestHandler {
56  
57          public SimpleService() {
58              super();
59          }
60  
61          @Override
62          public void handle(
63                  final ClassicHttpRequest request,
64                  final ClassicHttpResponse response,
65                  final HttpContext context) throws HttpException, IOException {
66              response.setCode(HttpStatus.SC_OK);
67              final StringEntity entity = new StringEntity("Whatever");
68              response.setEntity(entity);
69          }
70      }
71  
72      @Test
73      public void testStatefulConnections() throws Exception {
74  
75          final int workerCount = 5;
76          final int requestCount = 5;
77  
78          this.server.registerHandler("*", new SimpleService());
79  
80          this.connManager.setMaxTotal(workerCount);
81          this.connManager.setDefaultMaxPerRoute(workerCount);
82  
83          final UserTokenHandler userTokenHandler = new UserTokenHandler() {
84  
85              @Override
86              public Object getUserToken(final HttpRoute route, final HttpContext context) {
87                  final String id = (String) context.getAttribute("user");
88                  return id;
89              }
90  
91          };
92          this.clientBuilder.setUserTokenHandler(userTokenHandler);
93  
94          final HttpHost target = start();
95  
96          final HttpClientContext[] contexts = new HttpClientContext[workerCount];
97          final HttpWorker[] workers = new HttpWorker[workerCount];
98          for (int i = 0; i < contexts.length; i++) {
99              final HttpClientContext context = HttpClientContext.create();
100             contexts[i] = context;
101             workers[i] = new HttpWorker(
102                     "user" + i,
103                     context, requestCount, target, this.httpclient);
104         }
105 
106         for (final HttpWorker worker : workers) {
107             worker.start();
108         }
109         for (final HttpWorker worker : workers) {
110             worker.join(LONG_TIMEOUT.toMilliseconds());
111         }
112         for (final HttpWorker worker : workers) {
113             final Exception ex = worker.getException();
114             if (ex != null) {
115                 throw ex;
116             }
117             Assert.assertEquals(requestCount, worker.getCount());
118         }
119 
120         for (final HttpContext context : contexts) {
121             final String state0 = (String) context.getAttribute("r0");
122             Assert.assertNotNull(state0);
123             for (int r = 1; r < requestCount; r++) {
124                 Assert.assertEquals(state0, context.getAttribute("r" + r));
125             }
126         }
127 
128     }
129 
130     static class HttpWorker extends Thread {
131 
132         private final String uid;
133         private final HttpClientContext context;
134         private final int requestCount;
135         private final HttpHost target;
136         private final CloseableHttpClient httpclient;
137 
138         private volatile Exception exception;
139         private volatile int count;
140 
141         public HttpWorker(
142                 final String uid,
143                 final HttpClientContext context,
144                 final int requestCount,
145                 final HttpHost target,
146                 final CloseableHttpClient httpclient) {
147             super();
148             this.uid = uid;
149             this.context = context;
150             this.requestCount = requestCount;
151             this.target = target;
152             this.httpclient = httpclient;
153             this.count = 0;
154         }
155 
156         public int getCount() {
157             return this.count;
158         }
159 
160         public Exception getException() {
161             return this.exception;
162         }
163 
164         @Override
165         public void run() {
166             try {
167                 this.context.setAttribute("user", this.uid);
168                 for (int r = 0; r < this.requestCount; r++) {
169                     final HttpGet httpget = new HttpGet("/");
170                     final ClassicHttpResponse response = this.httpclient.execute(
171                             this.target,
172                             httpget,
173                             this.context);
174                     this.count++;
175 
176                     final EndpointDetails endpointDetails = this.context.getEndpointDetails();
177                     final String connuid = Integer.toHexString(System.identityHashCode(endpointDetails));
178                     this.context.setAttribute("r" + r, connuid);
179                     EntityUtils.consume(response.getEntity());
180                 }
181 
182             } catch (final Exception ex) {
183                 this.exception = ex;
184             }
185         }
186 
187     }
188 
189     @Test
190     public void testRouteSpecificPoolRecylcing() throws Exception {
191         // This tests what happens when a maxed connection pool needs
192         // to kill the last idle connection to a route to build a new
193         // one to the same route.
194 
195         final int maxConn = 2;
196 
197         this.server.registerHandler("*", new SimpleService());
198 
199         this.connManager.setMaxTotal(maxConn);
200         this.connManager.setDefaultMaxPerRoute(maxConn);
201 
202         final UserTokenHandler userTokenHandler = new UserTokenHandler() {
203 
204             @Override
205             public Object getUserToken(final HttpRoute route, final HttpContext context) {
206                 return context.getAttribute("user");
207             }
208 
209         };
210 
211         this.clientBuilder.setUserTokenHandler(userTokenHandler);
212 
213         final HttpHost target = start();
214 
215         // Bottom of the pool : a *keep alive* connection to Route 1.
216         final HttpContext context1 = new BasicHttpContext();
217         context1.setAttribute("user", "stuff");
218         final ClassicHttpResponse response1 = this.httpclient.execute(
219                 target, new HttpGet("/"), context1);
220         EntityUtils.consume(response1.getEntity());
221 
222         // The ConnPoolByRoute now has 1 free connection, out of 2 max
223         // The ConnPoolByRoute has one RouteSpcfcPool, that has one free connection
224         // for [localhost][stuff]
225 
226         Thread.sleep(100);
227 
228         // Send a very simple HTTP get (it MUST be simple, no auth, no proxy, no 302, no 401, ...)
229         // Send it to another route. Must be a keepalive.
230         final HttpContext context2 = new BasicHttpContext();
231         final ClassicHttpResponse response2 = this.httpclient.execute(
232                 new HttpHost("127.0.0.1", this.server.getPort()), new HttpGet("/"), context2);
233         EntityUtils.consume(response2.getEntity());
234         // ConnPoolByRoute now has 2 free connexions, out of its 2 max.
235         // The [localhost][stuff] RouteSpcfcPool is the same as earlier
236         // And there is a [127.0.0.1][null] pool with 1 free connection
237 
238         Thread.sleep(100);
239 
240         // This will put the ConnPoolByRoute to the targeted state :
241         // [localhost][stuff] will not get reused because this call is [localhost][null]
242         // So the ConnPoolByRoute will need to kill one connection (it is maxed out globally).
243         // The killed conn is the oldest, which means the first HTTPGet ([localhost][stuff]).
244         // When this happens, the RouteSpecificPool becomes empty.
245         final HttpContext context3 = new BasicHttpContext();
246         final ClassicHttpResponse response3 = this.httpclient.execute(
247                 target, new HttpGet("/"), context3);
248 
249         // If the ConnPoolByRoute did not behave coherently with the RouteSpecificPool
250         // this may fail. Ex : if the ConnPool discared the route pool because it was empty,
251         // but still used it to build the request3 connection.
252         EntityUtils.consume(response3.getEntity());
253 
254     }
255 
256 }