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