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