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  
28  package org.apache.hc.client5.http.impl.io;
29  
30  import java.net.InetAddress;
31  import java.net.InetSocketAddress;
32  import java.net.Socket;
33  import java.util.concurrent.ExecutorService;
34  import java.util.concurrent.Executors;
35  import java.util.concurrent.Future;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.TimeoutException;
38  
39  import javax.net.ssl.SSLSocket;
40  
41  import org.apache.hc.client5.http.DnsResolver;
42  import org.apache.hc.client5.http.HttpRoute;
43  import org.apache.hc.client5.http.SchemePortResolver;
44  import org.apache.hc.client5.http.config.ConnectionConfig;
45  import org.apache.hc.client5.http.config.TlsConfig;
46  import org.apache.hc.client5.http.io.ConnectionEndpoint;
47  import org.apache.hc.client5.http.io.DetachedSocketFactory;
48  import org.apache.hc.client5.http.io.LeaseRequest;
49  import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
50  import org.apache.hc.client5.http.protocol.HttpClientContext;
51  import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
52  import org.apache.hc.core5.http.HttpHost;
53  import org.apache.hc.core5.http.config.Lookup;
54  import org.apache.hc.core5.http.io.SocketConfig;
55  import org.apache.hc.core5.pool.PoolEntry;
56  import org.apache.hc.core5.pool.StrictConnPool;
57  import org.apache.hc.core5.util.TimeValue;
58  import org.apache.hc.core5.util.Timeout;
59  import org.junit.jupiter.api.Assertions;
60  import org.junit.jupiter.api.BeforeEach;
61  import org.junit.jupiter.api.Test;
62  import org.mockito.Mock;
63  import org.mockito.Mockito;
64  import org.mockito.MockitoAnnotations;
65  
66  /**
67   * {@link PoolingHttpClientConnectionManager} tests.
68   */
69  public class TestPoolingHttpClientConnectionManager {
70  
71      @Mock
72      private ManagedHttpClientConnection conn;
73      @Mock
74      private Lookup<TlsSocketStrategy> tlsSocketStrategyLookup;
75      @Mock
76      private DetachedSocketFactory detachedSocketFactory;
77      @Mock
78      private TlsSocketStrategy tlsSocketStrategy;
79      @Mock
80      private Socket socket;
81      @Mock
82      private SSLSocket upgradedSocket;
83      @Mock
84      private SchemePortResolver schemePortResolver;
85      @Mock
86      private DnsResolver dnsResolver;
87      @Mock
88      private Future<PoolEntry<HttpRoute, ManagedHttpClientConnection>> future;
89      @Mock
90      private StrictConnPool<HttpRoute, ManagedHttpClientConnection> pool;
91  
92      private PoolingHttpClientConnectionManager mgr;
93  
94      @BeforeEach
95      public void setup() throws Exception {
96          MockitoAnnotations.openMocks(this);
97          mgr = new PoolingHttpClientConnectionManager(new DefaultHttpClientConnectionOperator(
98                  detachedSocketFactory, schemePortResolver, dnsResolver, tlsSocketStrategyLookup), pool,
99                  null);
100     }
101 
102     @Test
103     public void testLeaseRelease() throws Exception {
104         final HttpHost target = new HttpHost("localhost", 80);
105         final HttpRoute route = new HttpRoute(target);
106 
107         final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
108         entry.assignConnection(conn);
109 
110         Mockito.when(conn.isOpen()).thenReturn(true);
111         Mockito.when(conn.isConsistent()).thenReturn(true);
112         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenReturn(entry);
113         Mockito.when(pool.lease(
114                 Mockito.eq(route),
115                 Mockito.eq(null),
116                 Mockito.any(),
117                 Mockito.eq(null)))
118                 .thenReturn(future);
119 
120         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
121         final ConnectionEndpoint endpoint1 = connRequest1.get(Timeout.ofSeconds(1));
122         Assertions.assertNotNull(endpoint1);
123         Assertions.assertNotSame(conn, endpoint1);
124 
125         mgr.release(endpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
126 
127         Mockito.verify(pool).release(entry, true);
128     }
129 
130     @Test
131     public void testReleaseRouteIncomplete() throws Exception {
132         final HttpHost target = new HttpHost("localhost", 80);
133         final HttpRoute route = new HttpRoute(target);
134 
135         final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
136 
137         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenReturn(entry);
138         Mockito.when(pool.lease(
139                 Mockito.eq(route),
140                 Mockito.eq(null),
141                 Mockito.any(),
142                 Mockito.eq(null)))
143                 .thenReturn(future);
144 
145         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
146         final ConnectionEndpoint endpoint1 = connRequest1.get(Timeout.ofSeconds(1));
147         Assertions.assertNotNull(endpoint1);
148         Assertions.assertNotSame(conn, endpoint1);
149 
150         mgr.release(endpoint1, null, TimeValue.NEG_ONE_MILLISECOND);
151 
152         Mockito.verify(pool).release(entry, false);
153     }
154 
155     @Test
156     public void testLeaseFutureTimeout() throws Exception {
157         final HttpHost target = new HttpHost("localhost", 80);
158         final HttpRoute route = new HttpRoute(target);
159 
160         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenThrow(new TimeoutException());
161         Mockito.when(pool.lease(
162                 Mockito.eq(route),
163                 Mockito.eq(null),
164                 Mockito.any(),
165                 Mockito.eq(null)))
166                 .thenReturn(future);
167 
168         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
169         Assertions.assertThrows(TimeoutException.class, () ->
170                 connRequest1.get(Timeout.ofSeconds(1)));
171     }
172 
173     @Test
174     public void testReleaseReusable() throws Exception {
175         final HttpHost target = new HttpHost("localhost", 80);
176         final HttpRoute route = new HttpRoute(target);
177 
178         final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
179         entry.assignConnection(conn);
180 
181         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenReturn(entry);
182         Mockito.when(pool.lease(
183                 Mockito.eq(route),
184                 Mockito.eq(null),
185                 Mockito.any(),
186                 Mockito.eq(null)))
187                 .thenReturn(future);
188         Mockito.when(conn.isOpen()).thenReturn(true);
189         Mockito.when(conn.isConsistent()).thenReturn(true);
190 
191         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
192         final ConnectionEndpoint endpoint1 = connRequest1.get(Timeout.ofSeconds(1));
193         Assertions.assertNotNull(endpoint1);
194         Assertions.assertTrue(endpoint1.isConnected());
195 
196         mgr.release(endpoint1, "some state", TimeValue.NEG_ONE_MILLISECOND);
197 
198         Mockito.verify(pool).release(entry, true);
199         Assertions.assertEquals("some state", entry.getState());
200     }
201 
202     @Test
203     public void testReleaseNonReusable() throws Exception {
204         final HttpHost target = new HttpHost("localhost", 80);
205         final HttpRoute route = new HttpRoute(target);
206 
207         final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
208         entry.assignConnection(conn);
209 
210         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenReturn(entry);
211         Mockito.when(pool.lease(
212                 Mockito.eq(route),
213                 Mockito.eq(null),
214                 Mockito.any(),
215                 Mockito.eq(null)))
216                 .thenReturn(future);
217         Mockito.when(conn.isOpen()).thenReturn(Boolean.FALSE);
218 
219         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
220         final ConnectionEndpoint endpoint1 = connRequest1.get(Timeout.ofSeconds(1));
221         Assertions.assertNotNull(endpoint1);
222         Assertions.assertFalse(endpoint1.isConnected());
223 
224         mgr.release(endpoint1, "some state", TimeValue.NEG_ONE_MILLISECOND);
225 
226         Mockito.verify(pool).release(entry, false);
227         Assertions.assertNull(entry.getState());
228     }
229 
230     @Test
231     public void testTargetConnect() throws Exception {
232         final HttpHost target = new HttpHost("https", "somehost", 443);
233         final InetAddress remote = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
234         final InetAddress local = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
235         final HttpRoute route = new HttpRoute(target, local, true);
236 
237         final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
238         entry.assignConnection(conn);
239 
240         Mockito.when(conn.isOpen()).thenReturn(false);
241         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenReturn(entry);
242         Mockito.when(pool.lease(
243                 Mockito.eq(route),
244                 Mockito.eq(null),
245                 Mockito.any(),
246                 Mockito.eq(null)))
247                 .thenReturn(future);
248 
249         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
250         final ConnectionEndpoint endpoint1 = connRequest1.get(Timeout.ofSeconds(1));
251         Assertions.assertNotNull(endpoint1);
252 
253         final HttpClientContext context = HttpClientContext.create();
254         final SocketConfig sconfig = SocketConfig.custom().build();
255 
256         mgr.setDefaultSocketConfig(sconfig);
257 
258         final ConnectionConfig connectionConfig = ConnectionConfig.custom()
259                 .setConnectTimeout(234, TimeUnit.MILLISECONDS)
260                 .build();
261         mgr.setDefaultConnectionConfig(connectionConfig);
262         final TlsConfig tlsConfig = TlsConfig.custom()
263                 .setHandshakeTimeout(345, TimeUnit.MILLISECONDS)
264                 .build();
265         mgr.setDefaultTlsConfig(tlsConfig);
266 
267         Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[]{remote});
268         Mockito.when(schemePortResolver.resolve(target.getSchemeName(), target)).thenReturn(8443);
269         Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket);
270 
271         Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy);
272         Mockito.when(tlsSocketStrategy.upgrade(
273                 Mockito.same(socket),
274                 Mockito.eq("somehost"),
275                 Mockito.anyInt(),
276                 Mockito.any(),
277                 Mockito.any())).thenReturn(upgradedSocket);
278 
279         mgr.connect(endpoint1, null, context);
280 
281         Mockito.verify(dnsResolver, Mockito.times(1)).resolve("somehost");
282         Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target.getSchemeName(), target);
283         Mockito.verify(detachedSocketFactory, Mockito.times(1)).create(null);
284         Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8443), 234);
285         Mockito.verify(tlsSocketStrategy).upgrade(socket, "somehost", 443, tlsConfig, context);
286 
287         mgr.connect(endpoint1, TimeValue.ofMilliseconds(123), context);
288 
289         Mockito.verify(dnsResolver, Mockito.times(2)).resolve("somehost");
290         Mockito.verify(schemePortResolver, Mockito.times(2)).resolve(target.getSchemeName(), target);
291         Mockito.verify(detachedSocketFactory, Mockito.times(2)).create(null);
292         Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8443), 123);
293         Mockito.verify(tlsSocketStrategy, Mockito.times(2)).upgrade(socket, "somehost", 443, tlsConfig, context);
294     }
295 
296     @Test
297     public void testProxyConnectAndUpgrade() throws Exception {
298         final HttpHost target = new HttpHost("https", "somehost", 443);
299         final HttpHost proxy = new HttpHost("someproxy", 8080);
300         final InetAddress remote = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
301         final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
302         final HttpRoute route = new HttpRoute(target, local, proxy, true);
303 
304         final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = new PoolEntry<>(route, TimeValue.NEG_ONE_MILLISECOND);
305         entry.assignConnection(conn);
306 
307         Mockito.when(conn.isOpen()).thenReturn(false);
308         Mockito.when(future.get(1, TimeUnit.SECONDS)).thenReturn(entry);
309         Mockito.when(pool.lease(
310                 Mockito.eq(route),
311                 Mockito.eq(null),
312                 Mockito.any(),
313                 Mockito.eq(null)))
314                 .thenReturn(future);
315 
316         final LeaseRequest connRequest1 = mgr.lease("some-id", route, null);
317         final ConnectionEndpoint endpoint1 = connRequest1.get(Timeout.ofSeconds(1));
318         Assertions.assertNotNull(endpoint1);
319 
320         final HttpClientContext context = HttpClientContext.create();
321         final SocketConfig sconfig = SocketConfig.custom().build();
322 
323         mgr.setDefaultSocketConfig(sconfig);
324 
325         final ConnectionConfig connectionConfig = ConnectionConfig.custom()
326                 .setConnectTimeout(234, TimeUnit.MILLISECONDS)
327                 .build();
328         mgr.setDefaultConnectionConfig(connectionConfig);
329         final TlsConfig tlsConfig = TlsConfig.custom()
330                 .setHandshakeTimeout(345, TimeUnit.MILLISECONDS)
331                 .build();
332         mgr.setDefaultTlsConfig(tlsConfig);
333 
334         Mockito.when(dnsResolver.resolve("someproxy")).thenReturn(new InetAddress[] {remote});
335         Mockito.when(schemePortResolver.resolve(proxy.getSchemeName(), proxy)).thenReturn(8080);
336         Mockito.when(schemePortResolver.resolve(target.getSchemeName(), target)).thenReturn(8443);
337         Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy);
338         Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket);
339 
340         mgr.connect(endpoint1, null, context);
341 
342         Mockito.verify(dnsResolver, Mockito.times(1)).resolve("someproxy");
343         Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(proxy.getSchemeName(), proxy);
344         Mockito.verify(detachedSocketFactory, Mockito.times(1)).create(null);
345         Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8080), 234);
346 
347         Mockito.when(conn.isOpen()).thenReturn(true);
348         Mockito.when(conn.getSocket()).thenReturn(socket);
349 
350         mgr.upgrade(endpoint1, context);
351 
352         Mockito.verify(tlsSocketStrategy, Mockito.times(1)).upgrade(
353                 socket, "somehost", 443, tlsConfig, context);
354     }
355 
356     @Test
357     public void testIsShutdownInitially() {
358         Assertions.assertFalse(mgr.isClosed(), "Connection manager should not be shutdown initially.");
359     }
360 
361     @Test
362     public void testShutdownIdempotency() {
363         mgr.close();
364         Assertions.assertTrue(mgr.isClosed(), "Connection manager should remain shutdown after the first call to shutdown.");
365         mgr.close(); // Second call to shutdown
366         Assertions.assertTrue(mgr.isClosed(), "Connection manager should still be shutdown after subsequent calls to shutdown.");
367     }
368 
369     @Test
370     public void testLeaseAfterShutdown() {
371         mgr.close();
372         Assertions.assertThrows(IllegalArgumentException.class, () -> {
373             // Attempt to lease a connection after shutdown
374             mgr.lease("some-id", new HttpRoute(new HttpHost("localhost")), null);
375         }, "Attempting to lease a connection after shutdown should throw an exception.");
376     }
377 
378 
379     @Test
380     public void testIsShutdown() {
381         // Setup phase
382         Mockito.when(pool.isShutdown()).thenReturn(false, true); // Simulate changing states
383 
384         // Execution phase: Initially, the manager should not be shutdown
385         Assertions.assertFalse(mgr.isClosed(), "Connection manager should not be shutdown initially.");
386 
387         // Simulate shutting down the manager
388         mgr.close();
389 
390         // Verification phase: Now, the manager should be reported as shutdown
391         Assertions.assertTrue(mgr.isClosed(), "Connection manager should be shutdown after close() is called.");
392     }
393 
394 
395     @Test
396     public void testConcurrentShutdown() throws InterruptedException {
397         final ExecutorService executor = Executors.newFixedThreadPool(2);
398         // Submit two shutdown tasks to be run in parallel, explicitly calling close() with no arguments
399         executor.submit(() -> mgr.close());
400         executor.submit(() -> mgr.close());
401         executor.shutdown();
402         executor.awaitTermination(1, TimeUnit.SECONDS);
403 
404         Assertions.assertTrue(mgr.isClosed(), "Connection manager should be shutdown after concurrent calls to shutdown.");
405     }
406 
407 
408 }