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