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